Coverage Report

Created: 2026-05-25 08:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/MathCAT/MathCAT/src/xpath_functions.rs
Line
Count
Source
1
#![allow(clippy::needless_return)]
2
//! XPath underlies rule matching and speech generation. The version of xpath used is based on xpath 1.0
3
//! and includes the ability to define functions and variables.
4
//! The variables defined are all the preferences and also variables set in speech rules via the `variables` keyword.
5
//! The function defined here are:
6
//! * `IsNode(node, kind)`:  returns true if the node matches the "kind".
7
//!   Valid values are "leaf", "2D", "simple", "common_fraction", "trig_name".
8
//! * `ToOrdinal(number, fractional, plural)`: converts the number to an ordinal (e.g, third)
9
//!   * `number` -- the number to translate
10
//!   * `fractional` -- true if this is a fractional ordinal (e.g, "half")
11
//!   * `plural` -- true if answer should be plural
12
//! * `ToCommonFraction(mfrac)` -- converts the fraction to an ordinal version (e.g, 2 thirds)
13
//! * `IsLargeOp(node)` -- returns true if the node is a large operator (e.g, integral or sum)
14
//! * `IsBracketed(node, left, right, requires_comma)` -- returns true if the first/last element in the mrow match `left`/`right`.
15
//!   If the optional `requires_comma` argument is given and is `true`, then there also must be a "," in the mrow (e.g., "f(x,y)")
16
//! * `DEBUG(xpath)` -- _Very_ useful function for debugging speech rules.
17
//!   This can be used to surround a whole or part of an xpath expression in a match or output.
18
//!   The result will be printed to standard output and the result returned so that `DEBUG` does not affect the computation.    
19
20
use sxd_document::dom::{Element, ChildOfElement};
21
use sxd_xpath::{Value, Context, context, function::*, nodeset::*};
22
use crate::definitions::{Definitions, SPEECH_DEFINITIONS, BRAILLE_DEFINITIONS};
23
use regex::Regex;
24
use crate::pretty_print::mml_to_string;
25
use std::cell::{Ref, RefCell};
26
use log::{debug, error, warn};
27
use std::sync::LazyLock;
28
use std::thread::LocalKey;
29
use phf::phf_set;
30
use sxd_xpath::function::Error as XPathError;
31
use crate::canonicalize::{as_element, name, get_parent, MATHML_FROM_NAME_ATTR};
32
33
// useful utility functions
34
// note: child of an element is a ChildOfElement, so sometimes it is useful to have parallel functions,
35
//   one for Element and one for ChildOfElement.
36
37
// @returns {String} -- the text of the (leaf) element otherwise an empty string
38
126k
fn get_text_from_element(e: Element) -> String {
39
126k
    if e.children().len() == 1 &&
40
103k
       let ChildOfElement::Text(
t102k
) = e.children()[0] {
41
102k
            return t.text().to_string();
42
23.4k
        }
43
23.4k
    return "".to_string();
44
126k
}
45
46
#[allow(non_snake_case)]
47
// Same as 'is_tag', but for ChildOfElement
48
110k
fn get_text_from_COE(coe: &ChildOfElement) -> String {
49
110k
    coe.element().map_or_else(String::new, get_text_from_element)
50
110k
}
51
52
// make sure that there is only one node in the NodeSet
53
// Returns the node or an Error
54
147k
pub fn validate_one_node<'n>(nodes: Nodeset<'n>, func_name: &str) -> Result<Node<'n>, Error> {
55
147k
    if nodes.size() == 0 {
56
0
        return Err(Error::Other(format!("Missing argument for {func_name}")));
57
147k
    } else if nodes.size() > 1 {
58
0
        return Err( Error::Other(format!("{} arguments for {}; expected 1 argument", nodes.size(), func_name)) );
59
147k
    }
60
147k
    return Ok( nodes.iter().next().unwrap() );
61
147k
}
62
63
// Return true if the element's name is 'name'
64
157k
fn is_tag(e: Element, name: &str) -> bool {
65
    // need to check name before the fallback of where the name came from
66
157k
    return e.name().local_part() == name || 
e47.8k
.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or_default() == name;
67
157k
}
68
69
#[allow(non_snake_case)]
70
// Same as 'is_tag', but for ChildOfElement
71
1.40k
fn is_COE_tag(coe: ChildOfElement, name: &str) -> bool {
72
1.40k
    coe.element().is_some_and(|element| is_tag(element, name))
73
1.40k
}
74
75
/// Should be an internal structure for implementation of the IsNode, but it was useful in one place in a separate module.
76
/// This should probably be restructured slightly.
77
pub struct IsNode;
78
79
impl IsNode {
80
    /// implements ClearSpeak's definition of "simple"
81
    /// this is fairly detailed, so we define a few local functions (at end) to help out
82
    /// Also, it doesn't help that the structure is a bit complicated Elements->ChildOfElement->Element/Text
83
7.43k
    pub fn is_simple(elem: Element) -> bool {
84
7.43k
        if is_trivially_simple(elem) {
85
3.62k
            return true;
86
3.81k
        }
87
88
3.81k
        if is_negative_of_trivially_simple(elem) {
89
            // -3 or -x
90
41
            return true;
91
3.76k
        }
92
93
3.76k
        if !is_tag(elem, "mrow") || 
elem.children()867
.
is_empty867
() {
94
2.90k
            return false;
95
867
        }
96
97
        // x y or -x or -3 x or -x y or -3 x y or x° or n° or -x° or -n°
98
        #[allow(clippy::if_same_then_else)]
99
867
        if is_times_mi(elem) {
100
42
            return true;    // x y
101
825
        } else if is_degrees(elem) {
102
0
            return true;    // x° or n°
103
825
        } else if is_function(elem) {
104
44
            return true;
105
781
        }
106
107
781
        return false;
108
109
110
        // returns the element's text value
111
5.71k
        fn to_str(e: Element<'_>) -> &str {
112
            // typically usage assumes 'e' is a leaf
113
            // bad MathML is the following isn't true
114
5.71k
            if e.children().len() == 1 {
115
5.71k
                let text_node = e.children()[0];
116
5.71k
                if let Some(t) = text_node.text() {
117
5.71k
                    return t.text();
118
0
                }
119
0
            }               
120
0
            return "";
121
5.71k
        }
122
123
        // same as 'to_str' but for ChildOfElement
124
1.01k
        fn coe_to_str(coe: ChildOfElement<'_>) -> &str {
125
            // typically usage assumes 'coe' is a leaf
126
1.01k
            let element_node = coe.element();
127
1.01k
            if let Some(e) = element_node {
128
                // bad MathML is the following isn't true
129
1.01k
                if e.children().len() == 1 {
130
1.01k
                    let text_node = e.children()[0];
131
1.01k
                    if let Some(t) = text_node.text() {
132
1.01k
                        return t.text();
133
0
                    }
134
8
                }
135
0
            }               
136
8
            return "";
137
1.01k
        }
138
139
        // returns true if the string is just a single *char* (which can be multiple bytes)
140
5.71k
        fn is_single_char(str: &str) -> bool {
141
5.71k
            let mut chars =  str.chars();
142
5.71k
            return chars.next().is_some() && chars.next().is_none();
143
5.71k
        }
144
145
        // checks the single element to see if it is simple (mn, mi that is a single char, common fraction)
146
8.33k
        fn is_trivially_simple(elem: Element) -> bool {
147
8.33k
            if is_tag(elem, "mn")  {
148
914
                return true;
149
7.41k
            }
150
7.41k
            if is_tag(elem, "mi") && 
is_single_char5.71k
(
to_str(elem)5.71k
) {
151
                // "simple" only if it is a single char (which can be multiple bytes)
152
3.14k
                return true;
153
4.27k
            }
154
155
            // FIX: need to consult preference Fraction_Ordinal
156
4.27k
            if IsNode::is_common_fraction(elem, 10, 19) {
157
66
                return true;
158
4.21k
            }
159
4.21k
            return false;
160
8.33k
        }
161
162
        // true if the negative of a single element that is simple
163
4.20k
        fn is_negative_of_trivially_simple(elem: Element) -> bool {
164
4.20k
            if is_tag(elem, "mrow") && 
elem.children().len() == 2933
{
165
38
                let children = elem.children();
166
                // better be negative of something at this point...
167
38
                if is_COE_tag(children[0], "mo") && 
is_equal11
(
children[0]11
, '-') &&
168
6
                   children[1].element().is_some() && is_trivially_simple(children[1].element().unwrap()) {
169
6
                    return true;
170
32
                }
171
4.16k
            }
172
4.20k
            if is_tag(elem, "minus") && 
elem.children().len() == 154
{
173
54
                let child = elem.children()[0];
174
54
                if let Some(e) = child.element() {
175
54
                    return is_trivially_simple(e);
176
0
                }
177
4.14k
            }
178
179
4.14k
            return false;
180
4.20k
        }
181
182
        // return true if ChildOfElement has exactly text 'ch'
183
967
        fn is_equal(coe: ChildOfElement, ch: char) -> bool {
184
967
            return coe_to_str(coe).starts_with(ch);
185
967
        }
186
187
        // true if mrow(xxx, &it;, mi) or mrow(xxx, &it; mi, &it;, mi) where mi's have len==1
188
867
        fn is_times_mi(mrow: Element) -> bool {
189
867
            assert!( is_tag(mrow, "mrow") );
190
867
            let children = mrow.children();
191
867
            if !(children.len() == 3 || 
children.len() == 541
) {
192
34
                return false;
193
833
            }
194
833
            if children[0].element().is_none() {
195
0
                return false;
196
833
            }
197
198
833
            let first_child = children[0].element().unwrap();
199
833
            if !is_trivially_simple(first_child) {
200
396
                if !is_negative_of_trivially_simple(first_child) {
201
382
                    return false;
202
14
                }
203
14
                if children.len() == 5 && 
204
2
                   ( (name(first_child) == "minus" && 
first_child.children().len() == 10
&&
!0
is_COE_tag0
(first_child.children()[0], "mn")) ||
205
2
                     (name(first_child) == "mrow"  && !is_COE_tag(first_child.children()[1], "mn")) ) {
206
1
                    return false;      // '-x y z' is too complicated () -- -2 x y is ok
207
13
                }
208
437
            }
209
210
450
            if !(is_COE_tag(children[1], "mo") && 
211
450
                    is_equal(children[1], '\u{2062}') &&
212
63
                 is_COE_tag(children[2], "mi") &&
213
51
                    coe_to_str(children[2]).len()==1 ) {
214
408
                return false;
215
42
            }
216
217
42
            if children.len() == 3 {
218
41
                return true;
219
1
            }
220
221
            // len == 5
222
1
            return  is_COE_tag(children[3], "mo") && 
223
1
                        is_equal(children[3], '\u{2062}') &&       // invisible times
224
1
                    is_COE_tag(children[4], "mi") &&
225
1
                        coe_to_str(children[4]).len()==1 ;
226
867
        }
227
228
        // return true if the mrow is var° or num°
229
825
        fn is_degrees(mrow: Element) -> bool {
230
825
            assert!( is_tag(mrow, "mrow") );
231
825
            let children = mrow.children();
232
825
            return children.len() == 2 &&
233
32
                is_equal(children[1], '°') &&
234
0
                (is_COE_tag(children[0], "mi") ||
235
0
                 is_COE_tag(children[0], "mn") );
236
825
        }
237
238
        // fn_name &af; [simple arg or (simple arg)]
239
825
        fn is_function(mrow: Element) -> bool {
240
825
            assert!( is_tag(mrow, "mrow") );
241
825
            let children = mrow.children();
242
825
            if children.len() != 3 {
243
40
                return false;
244
785
            }
245
785
            if !(is_COE_tag(children[1], "mo") && 
246
473
                 is_equal(children[1], '\u{2061}') ) {    // invisible function application
247
717
                return false;
248
68
            }
249
68
            if !is_COE_tag(children[0], "mi") {
250
0
                return false;
251
68
            }
252
68
            let function_arg = children[2].element().unwrap();
253
68
            if IsBracketed::is_bracketed(function_arg, "(", ")", false, false) {
254
60
                return IsNode::is_simple(function_arg.children()[1].element().unwrap());
255
            } else {
256
8
                return IsNode::is_simple(function_arg);
257
            }
258
825
        }
259
7.43k
    }
260
261
    // Returns true if 'frac' is a common fraction
262
    // In this case, the numerator and denominator can be no larger than 'num_limit' and 'denom_limit'
263
4.31k
    fn is_common_fraction(frac: Element, num_limit: usize, denom_limit: usize) -> bool {
264
2
        static ALL_DIGITS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\d+").unwrap()); // match one or more digits
265
266
4.31k
        if !is_tag(frac, "mfrac") &&  
!4.12k
is_tag4.12k
(frac, "fraction"){
267
4.12k
            return false;
268
188
        }
269
188
        let children = frac.children();
270
188
        if children.len() != 2 {
271
0
            return false;
272
188
        }
273
274
188
        let num = children[0].element();
275
188
        let denom = children[1].element();
276
188
        if num.is_none() || denom.is_none() {
277
0
            return false;
278
188
        };
279
280
188
        let num = num.unwrap();
281
188
        let denom = denom.unwrap();
282
188
        if !is_tag(num, "mn") || 
!115
is_tag115
(denom, "mn") {
283
87
            return false
284
101
        };
285
286
101
        let num = get_text_from_element(num);
287
101
        let denom = get_text_from_element(denom);
288
101
        if num.is_empty() || denom.is_empty() {
289
0
            return false;
290
101
        }
291
292
101
        return ALL_DIGITS.is_match(&num)   && is_small_enough(&num, num_limit) &&
293
100
               ALL_DIGITS.is_match(&denom) && is_small_enough(&denom, denom_limit);
294
295
201
        fn is_small_enough(val: &str, upper_bound: usize) -> bool {
296
201
            return if let Ok(value) = val.parse::<usize>() { value <= upper_bound } else { 
false0
};
297
201
        }
298
4.31k
    }
299
300
14.2k
    pub fn is_mathml(elem: Element) -> bool {
301
        // doesn't check MATHML_FROM_NAME_ATTR because we are interested in if it is an intent.
302
14.2k
        return ALL_MATHML_ELEMENTS.contains(name(elem));
303
14.2k
    }
304
305
    #[allow(non_snake_case)]
306
14.3k
    pub fn is_2D(elem: Element) -> bool {
307
14.3k
        return MATHML_2D_NODES.contains(elem.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(elem)));
308
14.3k
    }
309
310
37.8k
    pub fn is_scripted(elem: Element) -> bool {
311
37.8k
        return MATHML_SCRIPTED_NODES.contains(elem.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(elem)));
312
37.8k
    }
313
314
138k
    pub fn is_modified(elem: Element) -> bool {
315
138k
        return MATHML_MODIFIED_NODES.contains(elem.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(elem)));
316
138k
    }
317
    }
318
319
/// All MathML elements, including a few that get cleaned away
320
/// "semantics", "annotation-xml", "annotation" and Content MathML are not included
321
static ALL_MATHML_ELEMENTS: phf::Set<&str> = phf_set!{
322
    "mi", "mo", "mn", "mtext", "ms", "mspace", "mglyph",
323
    "mfrac", "mroot", "msub", "msup", "msubsup","munder", "mover", "munderover", "mmultiscripts",
324
    "mstack", "mlongdiv", "msgroup", "msrow", "mscarries", "mscarry", "msline",
325
    "none", "mprescripts", "malignmark", "maligngroup",
326
    "math", "msqrt", "merror", "mpadded", "mphantom", "menclose", "mtd", "mstyle",
327
    "mrow", "a", "mfenced", "mtable", "mtr", "mlabeledtr",
328
};
329
330
static MATHML_LEAF_NODES: phf::Set<&str> = phf_set! {
331
  "mi", "mo", "mn", "mtext", "ms", "mspace", "mglyph",
332
    "none", "annotation", "ci", "cn", "csymbol",    // content could be inside an annotation-xml (faster to allow here than to check lots of places)
333
};
334
335
336
// Should mstack and mlongdiv be included here?
337
static MATHML_2D_NODES: phf::Set<&str> = phf_set! {
338
    "mfrac", "msqrt", "mroot", "menclose",
339
    "msub", "msup", "msubsup", "munder", "mover", "munderover", "mmultiscripts",
340
    "mtable", "mtr", "mlabeledtr", "mtd",
341
};
342
343
// Should mstack and mlongdiv be included here?
344
static MATHML_MODIFIED_NODES: phf::Set<&str> = phf_set! {
345
    "msub", "msup", "msubsup", "munder", "mover", "munderover", "mmultiscripts",
346
};
347
348
// Should mstack and mlongdiv be included here?
349
static MATHML_SCRIPTED_NODES: phf::Set<&str> = phf_set! {
350
    "msub", "msup", "msubsup", "mmultiscripts",
351
};
352
353
1.07M
pub fn is_leaf(element: Element) -> bool {
354
1.07M
    return MATHML_LEAF_NODES.contains(name(element));
355
1.07M
}
356
357
impl Function for IsNode {
358
    // eval function for IsNode
359
    // errors happen for wrong number/kind of arg
360
5.12k
    fn evaluate<'d>(&self,
361
5.12k
                        _context: &context::Evaluation<'_, 'd>,
362
5.12k
                        args: Vec<Value<'d>>)
363
5.12k
                        -> Result<Value<'d>, Error>
364
    {
365
366
5.12k
        let mut args = Args(args);
367
5.12k
        args.exactly(2)
?0
;
368
5.12k
        let kind = args.pop_string()
?0
;
369
        // FIX: there is some conflict problem with xpath errors and error-chain
370
        //                .chain_err(|e| format!("Second arg to is_leaf is not a string: {}", e.to_string()))?;
371
5.12k
        match kind.as_str() {
372
5.12k
            "simple" | 
"leaf"3.09k
|
"common_fraction"849
|
"2D"849
|
"modified"162
|
"scripted"140
|
"mathml"49
=> (),
373
0
            _ => return Err( Error::Other(format!("Unknown argument value '{}' for IsNode",  kind.as_str())) ),
374
        };
375
376
5.12k
        let nodes = args.pop_nodeset()
?0
;
377
5.12k
        if nodes.size() == 0 {
378
0
            return Ok (Value::Boolean(false));  // like xpath, don't make this an error
379
5.12k
        };
380
        return Ok(
381
            Value::Boolean( 
382
5.12k
                nodes.iter()
383
5.12k
                    .all(|node|
384
5.39k
                        if let Node::Element(e) = node {
385
5.39k
                            match kind.as_str() {
386
5.39k
                                "simple" => 
IsNode::is_simple2.29k
(
e2.29k
),
387
3.09k
                                "leaf"   => 
is_leaf_any_name2.25k
(
e2.25k
),
388
849
                                "2D" => 
IsNode::is_2D687
(
e687
),
389
162
                                "modified" => 
IsNode::is_modified22
(
e22
),
390
140
                                "scripted" => 
IsNode::is_scripted91
(
e91
),
391
49
                                "mathml" => IsNode::is_mathml(e),
392
0
                                "common_fraction" => IsNode::is_common_fraction(e, usize::MAX, usize::MAX), 
393
0
                                _        => true,       // can't happen due to check above
394
                            }    
395
                        } else {
396
                            // xpath is something besides an element, so no match
397
0
                            false
398
5.39k
                        }
399
                    )
400
            )
401
        );
402
403
2.25k
        fn is_leaf_any_name(e: Element) -> bool {
404
2.25k
            let children = e.children();
405
2.25k
            if children.is_empty() {
406
0
                return true;
407
2.25k
            } else if children.len() == 1 &&
408
1.24k
                      let ChildOfElement::Text(_) = children[0] {
409
1.17k
                    return true;
410
1.07k
                }
411
1.07k
            return false
412
2.25k
        }
413
5.12k
    }
414
}
415
416
struct ToOrdinal;
417
impl ToOrdinal {
418
    // ordinals often have an irregular start (e.g., "half") before becoming regular.
419
    // if the number is irregular, return the ordinal form, otherwise return 'None'.
420
353
    fn compute_irregular_fractional_speech(number: &str, plural: bool) -> Option<String> {
421
353
        SPEECH_DEFINITIONS.with(|definitions| {
422
353
            let definitions = definitions.borrow();
423
353
            let words = if plural {
424
208
                definitions.get_vec("NumbersOrdinalFractionalPluralOnes")
?0
425
            } else {
426
145
                definitions.get_vec("NumbersOrdinalFractionalOnes")
?0
427
            };
428
353
            let number_as_int: usize = number.parse().unwrap(); // already verified it is only digits
429
353
            if number_as_int < words.len() {
430
                // use the words associated with this irregular pattern.
431
291
                return Some( words[number_as_int].clone() );
432
62
            };
433
62
            return None;
434
353
        })
435
353
    }
436
437
    /**
438
     * Translates a number of up to twelve digits into a string representation.
439
     *   number -- the number to translate
440
     *   fractional -- true if this is a fractional ordinal (e.g, "half")
441
     *   plural -- true if answer should be plural
442
     * Returns the string representation of that number or an error message
443
     */
444
416
    fn convert(number: &str, fractional: bool, plural: bool) -> Option<String> {
445
2
        static NO_DIGIT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^\d]").unwrap()); // match anything except a digit
446
416
        return SPEECH_DEFINITIONS.with(|definitions| {
447
416
            let definitions = definitions.borrow();
448
416
            let numbers_large = definitions.get_vec("NumbersLarge")
?0
;
449
450
416
            let pref_manager = crate::prefs::PreferenceManager::get();
451
416
            let pref_manager = pref_manager.borrow();
452
416
            let block_separators = pref_manager.pref_to_string("BlockSeparators");
453
416
            let decimal_separator = pref_manager.pref_to_string("DecimalSeparators");
454
            // check number validity (has digits, not a decimal)
455
416
            if number.is_empty() ||  number.contains(&decimal_separator) {
456
0
                return Some(String::from(number));
457
416
            }
458
            // remove any block separators
459
416
            let number = match clean_number(number, &block_separators) {
460
0
                None => return Some(String::from(number)),
461
416
                Some(num) => num,
462
            };
463
    
464
            // check to see if the number is too big or is not an integer or has non-digits
465
416
            if number.len() > 3*numbers_large.len() {
466
0
                return Some(number);
467
416
            }
468
416
            if NO_DIGIT.is_match(&number) {
469
                // this shouldn't have been part of an mn, so likely an error. Log a warning
470
                // FIX: log a warning that a non-number was passed to convert()
471
0
                return Some(number);
472
416
            }
473
474
            // first deal with the abnormalities of fractional ordinals (one half, etc). That simplifies what remains
475
416
            if fractional &&
476
353
               let Some(
string291
) = ToOrdinal::compute_irregular_fractional_speech(&number, plural) {
477
291
                    return Some(string);
478
125
                }
479
480
            // at this point, we only need to worry about singular/plural distinction
481
482
            // break into groups of three digits and add 10^3 word (thousands, millions, ...) after each chunk
483
            // FIX: add a pause between groups of three -- need to use TTS-specific pause
484
485
            // handle special case of trailing zeros
486
            // num_thousands_at_end represents the amount to shift NumbersLarge... (e.g., millions->thousands)
487
243
            let 
num_thousands_at_end125
= match
number125
.
rfind125
(|ch| ch > '0') { // last non-0 on right
488
122
                Some(n) => (number.len() - 1 - n) / 3 ,
489
3
                None => 0
490
            };
491
125
            let (number,_) = number.split_at(number.len() - 3 * num_thousands_at_end); // drop the 0s
492
493
            // everything is simplified if we add zeros at the start so that block size is a factor of 3
494
125
            let number = match number.len() % 3 {
495
18
                0 => "".to_string() + number,
496
69
                1 => "00".to_string() + number,
497
38
                _ => "0".to_string() + number, // can only be "2" -- compiler doesn't know there aren't other options
498
            };
499
500
            // At this point we have at least three "digits", and length is a multiple of 3
501
            // We have already verified that there are only ASCII digits, so we can subtract '0' to get an index
502
            const ASCII_0: usize = 48;
503
125
            let digits = number.as_bytes()
504
125
                        .iter()
505
411
                        .
map125
(|&byte| byte as usize - ASCII_0)
506
125
                        .collect::<Vec<usize>>();
507
508
125
            let mut answer = String::with_capacity(255);  // reasonable max most of the time
509
125
            let large_words = numbers_large;
510
125
            if digits.len() > 3 { 
511
                // speak this first groups as cardinal numbers
512
7
                let words = [
513
7
                    definitions.get_vec("NumbersHundreds")
?0
,
514
7
                    definitions.get_vec("NumbersTens")
?0
,
515
7
                    definitions.get_vec("NumbersOnes")
?0
,
516
                ];
517
7
                answer = digits[0..digits.len()-3]
518
7
                            .chunks(3)
519
7
                            .enumerate()
520
12
                            .
map7
(|(i, chunk)| {
521
12
                                if chunk[0] != 0 || 
chunk[1] != 08
||
chunk[2] != 08
{
522
7
                                    Some(ToOrdinal::hundreds_to_words(chunk, &words)
?0
+ " " +
523
7
                                        &large_words[num_thousands_at_end + digits.len()/3 - 1 - i] + " ")
524
                                } else {
525
5
                                    Some("".to_string())
526
                                }
527
12
                            })
528
7
                            .collect::<Option<Vec<String>>>()
?0
529
7
                            .join("");  // can't use " " because 1000567 would get extra space in the middle
530
7
                if num_thousands_at_end > 0 {
531
                    // add on "billionths", etc and we are done
532
0
                    let large_words = if plural {
533
0
                        definitions.get_vec("NumbersOrdinalPluralLarge")
534
                    } else {
535
0
                        definitions.get_vec("NumbersOrdinalLarge")
536
                    };
537
0
                    return Some(answer + &large_words?[num_thousands_at_end]);
538
7
                }
539
118
            };
540
541
            // all that is left is to speak the hundreds part, possibly followed by "thousands", "billions", etc
542
125
            let words = match (num_thousands_at_end > 0, plural) {
543
                (true, _) => [
544
10
                    definitions.get_vec("NumbersHundreds")
?0
,
545
10
                    definitions.get_vec("NumbersTens")
?0
,
546
10
                    definitions.get_vec("NumbersOnes")
?0
,
547
                ],
548
                (false, true) => [
549
54
                    definitions.get_vec("NumbersOrdinalPluralHundreds")
?0
,
550
54
                    definitions.get_vec("NumbersOrdinalPluralTens")
?0
,
551
54
                    definitions.get_vec("NumbersOrdinalPluralOnes")
?0
,
552
                ],
553
                (false, false) => [
554
61
                    definitions.get_vec("NumbersOrdinalHundreds")
?0
,
555
61
                    definitions.get_vec("NumbersOrdinalTens")
?0
,
556
61
                    definitions.get_vec("NumbersOrdinalOnes")
?0
,
557
                ],
558
            };
559
125
            answer += &ToOrdinal::hundreds_to_words(&digits[digits.len()-3..], &words)
?0
;
560
125
            if num_thousands_at_end > 0 {
561
10
                let large_words = if plural {
562
3
                    definitions.get_vec("NumbersOrdinalPluralLarge")
?0
563
                } else {
564
7
                    definitions.get_vec("NumbersOrdinalLarge")
?0
565
                };
566
10
                answer = answer + " " + &large_words[num_thousands_at_end];
567
115
            }
568
125
            return Some(answer);
569
416
        });
570
571
        /// Remove block separators and convert alphanumeric digits to ascii digits
572
416
        fn clean_number(number: &str, block_separators: &str) -> Option<String> {
573
416
            let mut answer = String::with_capacity(number.len());
574
617
            for ch in 
number416
.
chars416
() {
575
617
                if block_separators.contains(ch) {
576
0
                    continue;
577
617
                }
578
617
                if ch.is_ascii_digit() {
579
615
                    answer.push(ch);
580
615
                } else {
581
2
                    let shifted_ch = match ch {
582
2
                        '𝟎'..='𝟗' => ch as u32 -'𝟎' as u32 + '0' as u32,
583
0
                        '𝟘'..='𝟡' => ch as u32 -'𝟘' as u32 + '0' as u32,
584
0
                        '𝟢'..='𝟫' => ch as u32 -'𝟢' as u32 + '0' as u32,
585
0
                        '𝟬'..='𝟵' => ch as u32 -'𝟬' as u32 + '0' as u32,
586
0
                        '𝟶'..='𝟿' => ch as u32 -'𝟶' as u32 + '0' as u32,
587
0
                        _ => return None,
588
                    };
589
2
                    answer.push(char::from_u32(shifted_ch).unwrap());
590
                }
591
            }
592
416
            return Some(answer);
593
416
        }
594
416
    }
595
596
597
132
    fn hundreds_to_words(number: &[usize], words: &[Ref<Vec<String>>; 3]) -> Option<String> {
598
132
        assert!( number.len() == 3 );
599
132
        return SPEECH_DEFINITIONS.with(|definitions| {
600
132
            let definitions = definitions.borrow();
601
132
            if number[0] != 0 && 
number[1] == 024
&&
number[2] == 012
{
602
6
                return Some(words[0][number[0]].clone());
603
126
            }
604
605
126
            let mut hundreds = definitions.get_vec("NumbersHundreds")
?0
[number[0]].clone();
606
126
            if !hundreds.is_empty() {
607
18
                hundreds += " ";
608
108
            }
609
610
126
            if number[1] != 0 && 
number[2] == 049
{
611
26
                return Some(hundreds + &words[1][number[1]]);
612
100
            }
613
614
100
            if 10*number[1] < words[2].len() {
615
                // usurp regular ordering to handle something like '14'
616
85
                return Some(hundreds + &words[2][10*number[1] + number[2]]);
617
            } else {
618
15
                return Some(hundreds + &definitions.get_vec("NumbersTens")
?0
[number[1]] + " " + &words[2][number[2]]);
619
            }
620
132
        });
621
132
    }
622
}
623
624
impl Function for ToOrdinal {
625
    // convert a node to an ordinal number
626
320
    fn evaluate<'d>(&self,
627
320
                        _context: &context::Evaluation<'_, 'd>,
628
320
                        args: Vec<Value<'d>>)
629
320
                        -> Result<Value<'d>, Error>
630
    {
631
320
        let mut args = Args(args);
632
320
        if let Err(
e0
) = args.exactly(1).or_else(|_|
args288
.
exactly288
(3)) {
633
0
            return Err( XPathError::Other(format!("ToOrdinal requires 1 or 3 args: {e}")));
634
320
        };
635
320
        let mut fractional = false;
636
320
        let mut plural = false;
637
320
        if args.len() == 3 {
638
288
            plural = args.pop_boolean()
?0
;
639
288
            fractional = args.pop_boolean()
?0
;
640
32
        }
641
320
        let node = validate_one_node(args.pop_nodeset()
?0
, "ToOrdinal")
?0
;
642
320
        return match node {
643
0
            Node::Text(t) =>  Ok( Value::String(
644
0
                match ToOrdinal::convert(t.text(), fractional, plural) {
645
0
                    None => t.text().to_string(),
646
0
                    Some(ord) => ord,
647
                } ) ),
648
320
            Node::Element(e) => Ok( Value::String(
649
320
                match ToOrdinal::convert(&get_text_from_element(e), fractional, plural) {
650
0
                    None => get_text_from_element(e).to_string(),
651
320
                    Some(ord) => ord,
652
                } ) ),
653
0
            _   =>  Err( Error::ArgumentNotANodeset{actual: ArgumentType::String} ),
654
        }
655
320
    }
656
}
657
658
659
struct ToCommonFraction;
660
661
impl Function for ToCommonFraction {
662
    // convert a node to a common fraction (if the numerator and denominator are within given limits)
663
34
    fn evaluate<'d>(&self,
664
34
                        _context: &context::Evaluation<'_, 'd>,
665
34
                        args: Vec<Value<'d>>)
666
34
                        -> Result<Value<'d>, Error>
667
    {
668
34
        let mut args = Args(args);
669
34
        args.exactly(1)
?0
;
670
671
        // FIX: should probably handle errors by logging them and then trying to evaluate any children
672
34
        let node = validate_one_node(args.pop_nodeset()
?0
, "ToCommonFraction")
?0
;
673
34
        if let Node::Element(frac) = node {
674
34
            if !IsNode::is_common_fraction(frac, usize::MAX, usize::MAX) {
675
0
                return Err( Error::Other( format!("ToCommonFraction -- argument is not an 'mfrac': {}': ", mml_to_string(frac))) );
676
34
            }
677
    
678
            // everything has been verified, so we can just get the pieces and ignore potential error results
679
34
            let children = frac.children();
680
34
            let num = children[0].element().unwrap();
681
34
            let num =   get_text_from_element( num );
682
34
            let denom = children[1].element().unwrap();
683
34
            let denom = get_text_from_element( denom );
684
34
            let mut answer = num.clone() + " ";
685
34
            answer += &match ToOrdinal::convert(&denom, true, num!="1") {
686
0
                None => denom,
687
34
                Some(ord) => ord,
688
            };
689
690
34
            return Ok( Value::String( answer ) )
691
        } else {
692
0
            return Err( Error::Other( "ToCommonFraction -- argument is not an element".to_string()) );
693
        }
694
34
    }
695
}
696
697
struct Min;
698
/**
699
 * Returns true the smallest of the two args
700
 * @param(num1) 
701
 * @param(num2)
702
 */
703
 impl Function for Min {
704
705
0
    fn evaluate<'d>(&self,
706
0
                        _context: &context::Evaluation<'_, 'd>,
707
0
                        args: Vec<Value<'d>>)
708
0
                        -> Result<Value<'d>, Error>
709
    {
710
0
        let mut args = Args(args);
711
0
        args.exactly(2)?;
712
0
        let num1 = args.pop_number()?;
713
0
        let num2 = args.pop_number()?;
714
0
        return Ok( Value::Number( num1.min(num2) ) );
715
0
    }
716
}
717
718
struct Max;
719
720
impl Function for Max {
721
722
0
    fn evaluate<'d>(&self,
723
0
                        _context: &context::Evaluation<'_, 'd>,
724
0
                        args: Vec<Value<'d>>)
725
0
                        -> Result<Value<'d>, Error>
726
    {
727
0
        let mut args = Args(args);
728
0
        args.exactly(2)?;
729
0
        let num1 = args.pop_number()?;
730
0
        let num2 = args.pop_number()?;
731
0
        return Ok( Value::Number( num1.max(num2) ) );
732
0
    }
733
}
734
735
736
struct BaseNode;
737
/**
738
 * Returns true if the node is a large op
739
 * @param(node)     -- node(s) to test -- should be an <mo>
740
 */
741
 impl BaseNode {
742
    /// Recursively find the base node
743
    /// The base node of a non scripted element is the element itself
744
1.26k
    fn base_node(node: Element) -> Element {
745
1.26k
        let name = node.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(node));
746
1.26k
        if ["msub", "msup", "msubsup", "munder", "mover", "munderover", "mmultiscripts"].contains(&name) {
747
97
            return BaseNode::base_node(as_element(node.children()[0]));
748
        } else {
749
1.16k
            return node;
750
        }
751
1.26k
    }
752
 }
753
 impl Function for BaseNode {
754
755
1.16k
    fn evaluate<'d>(&self,
756
1.16k
                        _context: &context::Evaluation<'_, 'd>,
757
1.16k
                        args: Vec<Value<'d>>)
758
1.16k
                        -> Result<Value<'d>, Error>
759
    {
760
1.16k
        let mut args = Args(args);
761
1.16k
        args.exactly(1)
?0
;
762
1.16k
        let node = validate_one_node(args.pop_nodeset()
?0
, "BaseNode")
?0
;
763
1.16k
        if let Node::Element(e) = node {
764
1.16k
            let mut node_set = Nodeset::new();
765
1.16k
            node_set.add(BaseNode::base_node(e));
766
1.16k
            return Ok( Value::Nodeset(node_set) );
767
        } else {
768
            // xpath is something besides an element, so no match
769
0
            return Err( Error::Other("Argument other than a node given to BaseNode".to_string()) );
770
        }
771
1.16k
    }
772
}
773
774
775
struct IfThenElse;
776
 impl Function for IfThenElse {
777
36.2k
    fn evaluate<'d>(&self,
778
36.2k
                        _context: &context::Evaluation<'_, 'd>,
779
36.2k
                        args: Vec<Value<'d>>)
780
36.2k
                        -> Result<Value<'d>, Error>
781
    {
782
36.2k
        let args = Args(args);
783
36.2k
        args.exactly(3)
?0
;
784
36.2k
        let if_val = &args[0];
785
36.2k
        let then_val = &args[1];
786
36.2k
        let else_val = &args[2];
787
36.2k
        let is_true = match if_val {
788
14.5k
            Value::Nodeset(nodes) => nodes.size() > 0,
789
21.7k
            Value::Boolean(b) => *b,
790
0
            Value::Number(f) => *f != 0.0,
791
0
            Value::String(s) => !s.is_empty(),
792
        };
793
36.2k
        return Ok( if is_true {
then_val4.13k
.
clone4.13k
()} else {
else_val32.1k
.
clone32.1k
()});
794
36.2k
    }
795
}
796
797
798
struct Debug;
799
/**
800
 * Prints it's argument along with the string that was evaluated
801
 * @param(node)     -- node(s) to be evaluated/printed
802
 * @param(string)   -- string showing what is being evaluated
803
 */
804
 impl Function for Debug {
805
806
348
    fn evaluate<'d>(&self,
807
348
                        _context: &context::Evaluation<'_, 'd>,
808
348
                        args: Vec<Value<'d>>)
809
348
                        -> Result<Value<'d>, Error>
810
    {
811
348
        let mut args = Args(args);
812
348
        args.exactly(2)
?0
;
813
348
        let xpath_str = args.pop_string()
?0
;
814
348
        let eval_result = &args[0];
815
348
        debug!("  -- Debug: value of '{xpath_str}' is ");
816
348
        match eval_result {
817
78
            Value::Nodeset(nodes) => {
818
78
                if nodes.size() == 0 {
819
0
                    debug!("0 nodes (false)");
820
                } else {
821
78
                    let singular = nodes.size()==1;
822
78
                    debug!("{} node{}. {}:", 
nodes0
.
size0
(),
823
0
                        if singular {""} else {"s"},
824
0
                        if singular {"Node is"} else {"Nodes are"});
825
78
                    nodes.document_order()
826
78
                        .iter()
827
78
                        .enumerate()
828
78
                        .for_each(|(i, node)| {
829
78
                            match node {
830
78
                                Node::Element(mathml) => debug!("#{}:\n{}",
831
0
                                        i, mml_to_string(*mathml)),
832
0
                                _ => debug!("'{node:?}'"),
833
                            }   
834
78
                        })    
835
                }
836
            },
837
270
            _ => debug!("'{eval_result:?}'"),
838
        }
839
348
        return Ok( eval_result.clone() );
840
348
    }
841
}
842
843
844
/// Should be an internal structure for implementation of the IsBracketed, but it was useful in one place in a separate module.
845
/// This should probably be restructured slightly.
846
pub struct IsBracketed;
847
impl IsBracketed {
848
139k
    pub fn is_bracketed(element: Element, left: &str, right: &str, requires_comma: bool, requires_mrow: bool) -> bool {
849
        use crate::canonicalize::is_fence;
850
139k
        if requires_mrow && 
!116k
is_tag116k
(element, "mrow") {
851
18.8k
            return false;
852
120k
        }
853
120k
        let children = element.children();
854
120k
        let n_children = children.len();
855
120k
        if (n_children == 0 ||
856
120k
            !left.is_empty() && 
!right.is_empty()108k
&&
n_children < 2108k
) ||
857
116k
            requires_comma && 
element.children().len() < 34.04k
{
858
            // not enough argument for there to be a match
859
4.44k
            return false;
860
115k
        }
861
862
115k
        let first_child = as_element(children[0]);
863
115k
        let last_child = as_element(children[children.len()-1]);
864
        // debug!("first_child: {}", crate::pretty_print::mml_to_string(first_child));
865
        // debug!("last_child: {}", crate::pretty_print::mml_to_string(last_child));
866
115k
        if (left.is_empty()  && (
name(first_child) != "mo"11.2k
||
!is_fence(first_child)2.26k
)) ||
867
106k
           (right.is_empty() && (
name(last_child) != "mo"639
||
!is_fence(last_child)629
)) {
868
9.61k
            return false;
869
106k
        }
870
871
106k
        if !left.is_empty() && 
get_text_from_COE104k
(&children[0]) != left ||
872
6.14k
           !right.is_empty() && 
get_text_from_COE5.51k
(&
children5.51k
[children.len()-1]) != right {
873
            // left or right don't match
874
101k
            return false;
875
5.12k
        }
876
877
5.12k
        if requires_comma {
878
445
            if let ChildOfElement::Element(contents) = children[1] {
879
445
                let children = contents.children();
880
445
                if !is_tag(contents, "mrow") || 
children.len() <= 1248
{
881
197
                    return false;
882
248
                }
883
                // finally, we can check for a comma -- we might not have operands, so we to check first and second entry
884
248
                if get_text_from_COE(&children[0]).as_str() == "," {
885
1
                    return true;
886
247
                }
887
247
                if children.len() > 1 && get_text_from_COE(&children[1]).as_str() == "," {
888
133
                    return true;
889
114
                }
890
0
            }
891
114
            return false;
892
        } else {
893
4.67k
            return true;
894
        }
895
139k
    }
896
}
897
898
/**
899
 * Returns true if the node is a bracketed expr with the indicated left/right chars
900
 * node -- node(s) to test
901
 * left -- string (like "[") or empty
902
 * right -- string (like "]") or empty
903
 * requires_comma - boolean, optional (check the top level of 'node' for commas)
904
 */
905
// 'requiresComma' is useful for checking parenthesized expressions vs function arg lists and other lists
906
 impl Function for IsBracketed {
907
115k
    fn evaluate<'d>(&self,
908
115k
                        _context: &context::Evaluation<'_, 'd>,
909
115k
                        args: Vec<Value<'d>>)
910
115k
                        -> Result<Value<'d>, Error>
911
    {
912
115k
        let mut args = Args(args);
913
115k
        args.at_least(3)
?0
;
914
115k
        args.at_most(5)
?0
;
915
115k
        let mut requires_comma = false;
916
115k
        let mut requires_mrow = true;
917
115k
        if args.len() == 5 {
918
0
            requires_mrow = args.pop_boolean()?;
919
115k
        }
920
115k
        if args.len() >= 4 {
921
15
            requires_comma = args.pop_boolean()
?0
;
922
115k
        }
923
115k
        let right = args.pop_string()
?0
;
924
115k
        let left = args.pop_string()
?0
;
925
        return Ok( Value::Boolean(
926
115k
            match validate_one_node(args.pop_nodeset()
?0
, "IsBracketed") {
927
0
                Err(_) => false,  // be fault tolerant, like xpath,
928
115k
                Ok(node) => {
929
115k
                    if let Node::Element(e) = node {
930
115k
                        IsBracketed::is_bracketed(e, &left, &right, requires_comma, requires_mrow)
931
                    } else {
932
0
                        false
933
                    }
934
                }
935
            }) );
936
115k
        }
937
}
938
939
pub struct IsInDefinition;
940
impl IsInDefinition {
941
    /// Returns true if `test_str` is in `set_name`
942
    /// Returns an error if `set_name` is not defined
943
11.0k
    pub fn is_defined_in(test_str: &str, defs: &'static LocalKey<RefCell<Definitions>>, set_name: &str) -> Result<bool, Error> {
944
11.0k
        return defs.with(|definitions| {
945
11.0k
            if let Some(
set11.0k
) = definitions.borrow().get_hashset(set_name) {
946
11.0k
                return Ok( set.contains(test_str) );
947
12
            }
948
12
            if let Some(hashmap) = definitions.borrow().get_hashmap(set_name) {
949
12
                return Ok( hashmap.contains_key(test_str) );
950
0
            }
951
0
            return Err( Error::Other( format!("\n  IsInDefinition: '{set_name}' is not defined in definitions.yaml") ) );
952
11.0k
        });
953
11.0k
    }
954
}
955
956
/**
957
 * Returns true if the text is contained in the set defined in Speech or Braille.
958
 * element/string -- element (converted to string)/string to test
959
 * speech or braille
960
 * set_name -- the set in which the string is to be searched
961
 */
962
// 'requiresComma' is useful for checking parenthesized expressions vs function arg lists and other lists
963
 impl Function for IsInDefinition {
964
12.0k
    fn evaluate<'d>(&self,
965
12.0k
                        _context: &context::Evaluation<'_, 'd>,
966
12.0k
                        args: Vec<Value<'d>>)
967
12.0k
                        -> Result<Value<'d>, Error>
968
    {
969
12.0k
        let mut args = Args(args);
970
        // FIX: temporarily accept two args as assume SPEECH_DEFINITIONS until the Rule files are fixed
971
12.0k
        args.at_least(2)
?0
;
972
12.0k
        args.at_most(3)
?0
;
973
12.0k
        let set_name = args.pop_string()
?0
;
974
        // FIX: this (len == 1) is temporary until all the usages are switched to the (new) 3-arg form
975
12.0k
        let definitions = if args.len() == 2 {
976
10.4k
            match args.pop_string()
?0
.as_str() {
977
10.4k
                "Speech" => 
&SPEECH_DEFINITIONS1.35k
,
978
9.09k
                "Braille" => &BRAILLE_DEFINITIONS,
979
0
                _ => return Err( Error::Other("IsInDefinition:: second argument must be either 'Speech' or 'Braille'".to_string()) )
980
            }
981
        } else {
982
1.61k
            &SPEECH_DEFINITIONS
983
        };
984
12.0k
        match &args[0] {
985
5.04k
            Value::String(str) => return match IsInDefinition::is_defined_in(str, definitions, &set_name) {
986
5.04k
                Ok(result) => Ok( Value::Boolean( result ) ),
987
0
                Err(e) => Err(e),
988
            },
989
7.02k
            Value::Nodeset(nodes) => {
990
7.02k
                return if nodes.size() == 0 {
991
0
                    Ok( Value::Boolean(false) )    // trivially not in definition
992
                } else {
993
7.02k
                    let node = validate_one_node(nodes.clone(), "IsInDefinition")
?0
;
994
7.02k
                    if let Node::Element(e) = node {
995
7.02k
                        let text = get_text_from_element(e);
996
7.02k
                        if text.is_empty() {
997
979
                            Ok( Value::Boolean(false) )
998
                        } else {
999
6.04k
                            match IsInDefinition::is_defined_in(&text, definitions, &set_name) {
1000
6.04k
                                Ok(result) => Ok( Value::Boolean( result ) ),
1001
0
                                Err(e) => Err(e),
1002
                            }          
1003
                        }
1004
                    } else {
1005
0
                        Ok( Value::Boolean(false))       // trivially not in definition                    }
1006
                    }
1007
                }
1008
            },
1009
0
            _ => Err( Error::Other("IsInDefinition:: neither a node nor a string is passed for first argument".to_string()) ),
1010
        }
1011
12.0k
    }
1012
}
1013
1014
1015
pub struct DefinitionValue;
1016
impl DefinitionValue {
1017
    /// Returns the value associated with `key` in `set_name`. If `key` is not in `set_name`, an empty string is returned
1018
    /// Returns an error if `set_name` is not defined
1019
12.7k
    pub fn definition_value(key: &str, defs: &'static LocalKey<RefCell<Definitions>>, set_name: &str) -> Result<String, Error> {
1020
12.7k
        return defs.with(|definitions| {
1021
12.7k
            if let Some(map) = definitions.borrow().get_hashmap(set_name) {
1022
12.7k
                return Ok( match map.get(key) {
1023
5.64k
                    None => "".to_string(),
1024
7.09k
                    Some(str) => str.clone(),
1025
                });
1026
0
            }
1027
0
            return Err( Error::Other( format!("\n  DefinitionValue: '{set_name}' is not defined in definitions.yaml") ) );
1028
12.7k
        });
1029
12.7k
    }
1030
}
1031
1032
/**
1033
 * Returns true if the node is a bracketed expr with the indicated left/right chars
1034
 * element/string -- element (converted to string)/string to test
1035
 * left -- string (like "[") or empty
1036
 * right -- string (like "]") or empty
1037
 * requires_comma - boolean, optional (check the top level of 'node' for commas
1038
 */
1039
// 'requiresComma' is useful for checking parenthesized expressions vs function arg lists and other lists
1040
 impl Function for DefinitionValue {
1041
13.1k
    fn evaluate<'d>(&self,
1042
13.1k
                        _context: &context::Evaluation<'_, 'd>,
1043
13.1k
                        args: Vec<Value<'d>>)
1044
13.1k
                        -> Result<Value<'d>, Error>
1045
    {
1046
13.1k
        let mut args = Args(args);
1047
13.1k
        args.exactly(3)
?0
;
1048
13.1k
        let set_name = args.pop_string()
?0
;
1049
13.1k
        let definitions = match args.pop_string()
?0
.as_str() {
1050
13.1k
            "Speech" => 
&SPEECH_DEFINITIONS13.1k
,
1051
12
            "Braille" => &BRAILLE_DEFINITIONS,
1052
0
            _ => return Err( Error::Other("IsInDefinition:: second argument must be either 'Speech' or 'Braille'".to_string()) )
1053
        };
1054
13.1k
        match &args[0] {
1055
5.04k
            Value::String(str) => return match DefinitionValue::definition_value(str, definitions, &set_name) {
1056
5.04k
                Ok(result) => Ok( Value::String( result ) ),
1057
0
                Err(e) => Err(e),
1058
            },
1059
8.10k
            Value::Nodeset(nodes) => {
1060
8.10k
                return if nodes.size() == 0 {
1061
0
                    Ok( Value::String("".to_string()) )    // trivially not in definition
1062
                } else {
1063
8.10k
                    let node = validate_one_node(nodes.clone(), "DefinitionValue")
?0
;
1064
8.10k
                    if let Node::Element(
e8.10k
) = node {
1065
8.10k
                        let text = get_text_from_element(e);
1066
8.10k
                        if text.is_empty() {
1067
410
                            Ok( Value::String("".to_string()) )
1068
                        } else {
1069
7.69k
                            match DefinitionValue::definition_value(&text, definitions, &set_name) {
1070
7.69k
                                Ok(result) => Ok( Value::String( result ) ),
1071
0
                                Err(e) => Err(e),
1072
                            }          
1073
                        }
1074
                    } else {
1075
3
                        Ok( Value::String("".to_string()) )       // trivially not in definition                    }
1076
                    }
1077
                }
1078
            },
1079
0
            _ => Err( Error::Other("DefinitionValue:: neither a node nor a string is passed for first argument".to_string()) ),
1080
        }
1081
13.1k
    }
1082
}
1083
1084
pub struct DistanceFromLeaf;
1085
impl DistanceFromLeaf {
1086
240
    fn distance(element: Element, use_left_side: bool, treat_2d_elements_as_tokens: bool) -> usize {
1087
        // FIX: need to handle char level (i.e., chars in a leaf element)
1088
240
        let mut element = element;
1089
240
        let mut distance = 1;
1090
        loop {
1091
            // debug!("distance={} -- element: {}", distance, mml_to_string(element));
1092
361
            if MATHML_LEAF_NODES.contains(element.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(element))) {
1093
199
                return distance;
1094
162
            }
1095
162
            if treat_2d_elements_as_tokens && (
IsNode::is_2D60
(
element60
) ||
!IsNode::is_mathml(element)20
) {
1096
41
                return distance;
1097
121
            }
1098
121
            let children = element.children();
1099
121
            assert!(!children.is_empty());
1100
121
            element = as_element( if use_left_side {
children[0]0
} else {children[children.len()-1]} );
1101
121
            distance += 1;
1102
        }
1103
240
    }
1104
}
1105
1106
/**
1107
 * Returns distance from the current node to the leftmost/rightmost leaf (if char, then = 0, if token, then 1).
1108
 * If the node is a bracketed expr with the indicated left/right chars
1109
 * node -- node(s) to test
1110
 * left_side -- (bool) traverse leftmost child to leaf
1111
 * treat2D_elements_as_tokens -- (bool) 2D notations such as fractions are treated like leaves 
1112
 */
1113
impl Function for DistanceFromLeaf {
1114
240
    fn evaluate<'d>(&self,
1115
240
                        _context: &context::Evaluation<'_, 'd>,
1116
240
                        args: Vec<Value<'d>>)
1117
240
                        -> Result<Value<'d>, Error>
1118
    {
1119
240
        let mut args = Args(args);
1120
240
        args.exactly(3)
?0
;
1121
240
        let treat_2d_elements_as_tokens = args.pop_boolean()
?0
;
1122
240
        let use_left_side = args.pop_boolean()
?0
;
1123
240
        let node = validate_one_node(args.pop_nodeset()
?0
, "DistanceFromLeaf")
?0
;
1124
240
        if let Node::Element(e) = node {
1125
240
            return Ok( Value::Number( DistanceFromLeaf::distance(e, use_left_side, treat_2d_elements_as_tokens) as f64) );
1126
0
        }
1127
1128
        // FIX: should having a non-element be an error instead??
1129
0
        return Err(Error::Other(format!("DistanceFromLeaf: first arg '{node:?}' is not a node")));
1130
240
    }
1131
}
1132
1133
1134
1135
pub struct EdgeNode;
1136
impl EdgeNode {
1137
    // Return the root of the ancestor tree if we are at the left/right side of a path from that to 'element'
1138
2.09k
    fn edge_node<'a>(element: Element<'a>, use_left_side: bool, stop_node_name: &str) -> Option<Element<'a>> {
1139
2.09k
        let element_name = element.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(element));
1140
2.09k
        if element_name == "math" {
1141
86
            return Some(element);
1142
2.00k
        };
1143
1144
2.00k
        let parent = get_parent(element);   // there is always a "math" node
1145
2.00k
        let parent_name = parent.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(parent));
1146
1147
        // first check to see if we have the special case of punctuation as last child of math/mrow element
1148
        // it only matters if we are looking at the right edge
1149
1150
        // debug!("EdgeNode: there are {} preceding siblings",element.preceding_siblings().len() );
1151
2.00k
        if use_left_side  && 
!element.preceding_siblings().is_empty()1.15k
{// not at left side
1152
587
            return None;
1153
1.41k
        };
1154
1155
1.41k
        if !use_left_side && 
!element.following_siblings().is_empty()848
{ // not at right side
1156
            // check for the special case that the parent is an mrow and the grandparent is <math> and we have punctuation
1157
574
            let grandparent = get_parent(parent);
1158
574
            let grandparent_name = grandparent.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(grandparent));
1159
574
            if grandparent_name == "math" &&
1160
105
               parent_name == "mrow" && 
parent.children().len() == 289
{ // right kind of mrow
1161
11
                let text = get_text_from_element( as_element(parent.children()[1]) );
1162
11
                if text == "," || text == "." || 
text == ";"10
||
text == "?"10
{
1163
1
                    return Some(grandparent);
1164
10
                }
1165
563
            }
1166
573
             return None;
1167
843
        };
1168
1169
        // at an edge -- check to see the parent is desired root
1170
843
        if parent_name == stop_node_name || 
1171
735
           (stop_node_name == "2D" && 
IsNode::is_2D338
(
parent338
)) {
1172
176
            return Some(parent);
1173
667
        };
1174
        
1175
        // debug!("EdgeNode: recurse to {}", parent_name);
1176
667
        return EdgeNode::edge_node(parent, use_left_side, stop_node_name)
1177
2.09k
    }
1178
}
1179
1180
// EdgeNode(node, "left"/"right", stopNodeName)
1181
//    -- returns the stopNode if at left/right edge of named ancestor node. "stopNodeName' can also be "2D'
1182
//       returns original node match isn't found
1183
//  Note: if stopNodeName=="math", then punctuation is taken into account since it isn't really part of the math
1184
impl Function for EdgeNode {
1185
1.41k
    fn evaluate<'d>(&self,
1186
1.41k
                        _context: &context::Evaluation<'_, 'd>,
1187
1.41k
                        args: Vec<Value<'d>>)
1188
1.41k
                        -> Result<Value<'d>, Error>
1189
    {
1190
1.41k
        let mut args = Args(args);
1191
1.41k
        args.exactly(3)
?0
;
1192
1.41k
        let stop_node_name = args.pop_string()
?0
;
1193
1.41k
        let use_left_side = args.pop_string()
?0
.to_lowercase() == "left";
1194
1.41k
        let node = validate_one_node(args.pop_nodeset()
?0
, "EdgeNode")
?0
;
1195
1.41k
        if let Node::Element(e) = node {
1196
1.41k
            let result = match EdgeNode::edge_node(e, use_left_side, &stop_node_name) {
1197
260
                Some(found) => found,
1198
1.15k
                None => e,
1199
            };
1200
1.41k
            let mut node_set = Nodeset::new();
1201
1.41k
            node_set.add(result);
1202
1.41k
            return Ok( Value::Nodeset(node_set) );
1203
0
        }
1204
1205
        // FIX: should having a non-element be an error instead??
1206
0
        return Err(Error::Other(format!("EdgeNode: first arg '{node:?}' is not a node")));
1207
1.41k
    }
1208
}
1209
1210
pub struct SpeakIntentName;
1211
/// SpeakIntentName(intent, verbosity)
1212
///   Returns a string corresponding to the intent name with the indicated verbosity
1213
impl Function for SpeakIntentName {
1214
340
    fn evaluate<'d>(&self,
1215
340
                        _context: &context::Evaluation<'_, 'd>,
1216
340
                        args: Vec<Value<'d>>)
1217
340
                        -> Result<Value<'d>, Error>
1218
    {
1219
340
        let mut args = Args(args);
1220
340
        args.exactly(3)
?0
;
1221
340
        let fixity = args.pop_string()
?0
;
1222
340
        let verbosity = args.pop_string()
?0
;
1223
340
        let intent_name = args.pop_string()
?0
;
1224
340
        return Ok( Value::String(crate::infer_intent::intent_speech_for_name(&intent_name, &verbosity, &fixity)) );
1225
340
    }
1226
}
1227
1228
pub struct GetBracketingIntentName;
1229
/// GetBracketingIntentName(name, verbosity, at_start_or_end)
1230
///   Returns a potentially empty string to use to bracket an intent expression (start foo... end foo)
1231
/// 
1232
impl GetBracketingIntentName {
1233
61
    fn bracketing_words(intent_name: &str, verbosity: &str, fixity: &str, at_start: bool) -> String {
1234
61
        crate::definitions::SPEECH_DEFINITIONS.with(|definitions| {
1235
61
            let definitions = definitions.borrow();
1236
61
            if let Some(
intent_name_pattern57
) = definitions.get_hashmap("IntentMappings").unwrap().get(intent_name) {
1237
                // Split the pattern is: fixity-def [|| fixity-def]*
1238
                //   fixity-def := fixity=open; verbosity; close
1239
                //   verbosity := terse | medium | verbose
1240
68
                if let Some(
matched_intent57
) =
intent_name_pattern.split("||")57
.
find57
(|&entry| entry.trim().starts_with(fixity)) {
1241
57
                    let (_, matched_intent) = matched_intent.split_once("=").unwrap_or_default();
1242
57
                    let parts = matched_intent.trim().split(";").collect::<Vec<&str>>();
1243
57
                    if parts.len() == 1 {
1244
30
                        return "".to_string();
1245
27
                    }
1246
27
                    if parts.len() != 3 {
1247
0
                        error!("Intent '{}' has {} ';' separated parts, should have 3", intent_name, parts.len());
1248
0
                        return "".to_string();
1249
27
                    }
1250
27
                    let mut speech = (if at_start {
parts[0]4
} else {
parts[2]23
}).split(":").collect::<Vec<&str>>();
1251
27
                    match speech.len() {
1252
20
                        1 => return speech[0].to_string(),
1253
                        2 | 3 => {
1254
7
                            if speech.len() == 2 {
1255
0
                                warn!("Intent '{intent_name}'  has only two ':' separated parts, but should have three");
1256
0
                                speech.push(speech[1]);
1257
7
                            }
1258
7
                            let bracketing_words = match verbosity {
1259
7
                                "Terse" => 
speech[0]0
,
1260
7
                                "Medium" => speech[1],
1261
0
                                _ => speech[2],
1262
                            };
1263
7
                            return bracketing_words.to_string();
1264
                        },
1265
                        _ => {
1266
0
                            error!("Intent '{}' has too many ({}) operator names, should only have 2", intent_name, speech.len());
1267
                        },
1268
                    }
1269
0
                }   
1270
4
            };
1271
4
            return "".to_string();
1272
61
        })
1273
61
    }
1274
}
1275
1276
impl Function for GetBracketingIntentName {
1277
61
    fn evaluate<'d>(&self,
1278
61
                        _context: &context::Evaluation<'_, 'd>,
1279
61
                        args: Vec<Value<'d>>)
1280
61
                        -> Result<Value<'d>, Error>
1281
    {
1282
61
        let mut args = Args(args);
1283
61
        args.exactly(4)
?0
;
1284
61
        let start_or_end = args.pop_string()
?0
;
1285
61
        if start_or_end != "start" && 
start_or_end != "end"57
{
1286
0
            return Err( Error::Other("GetBracketingIntentName: first argument must be either 'start' or 'end'".to_string()) );
1287
61
        }
1288
61
        let fixity = args.pop_string()
?0
;
1289
61
        let verbosity = args.pop_string()
?0
;
1290
61
        let name = args.pop_string()
?0
;
1291
61
        return Ok( Value::String(GetBracketingIntentName:: bracketing_words(&name, &verbosity, &fixity, start_or_end == "start")) );
1292
61
    }
1293
}
1294
1295
pub struct GetNavigationPartName;
1296
/// GetNavigationPartName(name, index)
1297
/// Returns the name to use to speak the part of a navigation expression (e.g., 'numerator', 'denominator', 'base', 'exponent', ...).
1298
/// If there is no match, an empty string is returned.
1299
/// 'index' is 0-based
1300
/// 
1301
impl GetNavigationPartName {
1302
129
    fn navigation_part_name(intent_name: &str, index: usize) -> String {
1303
129
        crate::definitions::SPEECH_DEFINITIONS.with(|definitions| {
1304
129
            let definitions = definitions.borrow();
1305
129
            if let Some(navigation_names) = definitions.get_hashmap("NavigationParts") &&
1306
129
               let Some(
nav_part_names105
) = navigation_names.get(intent_name) {
1307
                    // Split the pattern is: part [; part]*
1308
105
                    if let Some(part_name) = nav_part_names.trim().split(";").nth(index) {
1309
105
                        return part_name.trim().to_string();
1310
0
                    }
1311
24
                }
1312
24
            return "".to_string();
1313
129
        })
1314
129
    }
1315
}
1316
1317
impl Function for GetNavigationPartName {
1318
129
    fn evaluate<'d>(&self,
1319
129
                        _context: &context::Evaluation<'_, 'd>,
1320
129
                        args: Vec<Value<'d>>)
1321
129
                        -> Result<Value<'d>, Error>
1322
    {
1323
129
        let mut args = Args(args);
1324
129
        args.exactly(2)
?0
;
1325
129
        let index = args.pop_number()
?0
as usize;
1326
129
        let name = args.pop_string()
?0
;
1327
129
        return Ok( Value::String(GetNavigationPartName:: navigation_part_name(&name, index)) );
1328
129
    }
1329
}
1330
1331
pub struct FontSizeGuess;
1332
/// FontSizeGuess(size_string)
1333
///   returns a guess of the size in "ems"
1334
/// Examples:
1335
///    "0.278em" -> 0.278
1336
///    ""
1337
//       returns original node match isn't found
1338
impl FontSizeGuess {
1339
224
    pub fn em_from_value(value_with_unit: &str) -> f64 {
1340
        // match one or more digits followed by a unit -- there are many more units, but they tend to be large and rarer(?)
1341
3
        static FONT_VALUE: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"(-?[0-9]*\.?[0-9]*)(px|cm|mm|Q|in|ppc|pt|ex|em|rem)").unwrap() });
1342
224
        let cap = FONT_VALUE.captures(value_with_unit);
1343
224
        if let Some(
cap200
) = cap {
1344
200
            if cap.len() == 3 {
1345
200
                let multiplier = match &cap[2] {    // guess based on 12pt font to convert to ems
1346
200
                    "px" => 
1.0/12.00
,
1347
200
                    "cm" => 
2.370
,
1348
200
                    "mm" => 
0.2370
,
1349
200
                    "Q" => 
0.0590
, // 1/4 mm
1350
200
                    "in" => 
6.0223
,
1351
177
                    "pc" => 
1.00
,
1352
177
                    "pt" => 
1.0/12.06
,
1353
171
                    "ex" => 
0.50
,
1354
171
                    "em" => 1.0,
1355
0
                    "rem" => 16.0/12.0,
1356
0
                    default => {debug!("unit='{default}'"); 10.0}
1357
                };
1358
                // debug!("FontSizeGuess: {}->{}, val={}, multiplier={}", value_with_unit, value*multiplier, value, multiplier);
1359
200
                return cap[1].parse::<f64>().unwrap_or(0.0) * multiplier;
1360
            }  else {
1361
0
                return 0.0;             // something bad happened
1362
            }
1363
        }else {
1364
24
            let multiplier = match value_with_unit {    // guess based on 12pt font to convert to ems
1365
24
                "veryverythinspace" => 
1.0/18.00
,
1366
24
                "verythinspace" => 
2.0/18.00
,
1367
24
                "thinspace" => 
3.0/18.00
,
1368
24
                "mediumspace" => 
4.0/18.00
,
1369
24
                "thickspace" => 
5.0/18.00
,
1370
24
                "verythickspace" => 
6.0/18.00
,
1371
24
                "veryverythickspace" => 
7.0/18.00
,
1372
24
                _ => 0.0,
1373
            };
1374
24
            return multiplier;
1375
        }
1376
224
    }
1377
}
1378
impl Function for FontSizeGuess {
1379
0
    fn evaluate<'d>(&self,
1380
0
                        _context: &context::Evaluation<'_, 'd>,
1381
0
                        args: Vec<Value<'d>>)
1382
0
                        -> Result<Value<'d>, Error>
1383
    {
1384
0
        let mut args = Args(args);
1385
0
        args.exactly(1)?;
1386
0
        let value_with_unit = args.pop_string()?;
1387
0
        let em_value = FontSizeGuess::em_from_value(&value_with_unit);
1388
0
        return Ok( Value::Number(em_value) );
1389
0
    }
1390
}
1391
1392
pub struct ReplaceAll;
1393
/// ReplaceAll(haystack, needle, replacement)
1394
///   Returns a string with all occurrences of 'needle' replaced with 'replacement'
1395
impl Function for ReplaceAll {
1396
0
    fn evaluate<'d>(&self,
1397
0
                        _context: &context::Evaluation<'_, 'd>,
1398
0
                        args: Vec<Value<'d>>)
1399
0
                        -> Result<Value<'d>, Error>
1400
    {
1401
0
        let mut args = Args(args);
1402
0
        args.exactly(3)?;
1403
0
        let replacement = args.pop_string()?;
1404
0
        let needle = args.pop_string()?;
1405
0
        let haystack = args.pop_string()?;
1406
0
        return Ok( Value::String(haystack.replace(&needle, &replacement)) );
1407
0
    }
1408
}
1409
1410
/// Add all the functions defined in this module to `context`.
1411
22.7k
pub fn add_builtin_functions(context: &mut Context) {
1412
22.7k
    context.set_function("NestingChars", crate::braille::NemethNestingChars);
1413
22.7k
    context.set_function("BrailleChars", crate::braille::BrailleChars);
1414
22.7k
    context.set_function("NeedsToBeGrouped", crate::braille::NeedsToBeGrouped);
1415
22.7k
    context.set_function("IsNode", IsNode);
1416
22.7k
    context.set_function("ToOrdinal", ToOrdinal);
1417
22.7k
    context.set_function("ToCommonFraction", ToCommonFraction);
1418
22.7k
    context.set_function("IsBracketed", IsBracketed);
1419
22.7k
    context.set_function("IsInDefinition", IsInDefinition);
1420
22.7k
    context.set_function("DefinitionValue", DefinitionValue);
1421
22.7k
    context.set_function("BaseNode", BaseNode);
1422
22.7k
    context.set_function("IfThenElse", IfThenElse);
1423
22.7k
    context.set_function("IFTHENELSE", IfThenElse);
1424
22.7k
    context.set_function("DistanceFromLeaf", DistanceFromLeaf);
1425
22.7k
    context.set_function("EdgeNode", EdgeNode);
1426
22.7k
    context.set_function("SpeakIntentName", SpeakIntentName);
1427
22.7k
    context.set_function("GetBracketingIntentName", GetBracketingIntentName);
1428
22.7k
    context.set_function("GetNavigationPartName", GetNavigationPartName);
1429
22.7k
    context.set_function("DEBUG", Debug);
1430
1431
    // Not used: remove??
1432
22.7k
    context.set_function("min", Min);       // missing in xpath 1.0
1433
22.7k
    context.set_function("max", Max);       // missing in xpath 1.0
1434
22.7k
    context.set_function("FontSizeGuess", FontSizeGuess);
1435
22.7k
    context.set_function("ReplaceAll", ReplaceAll);
1436
22.7k
}
1437
1438
1439
#[cfg(test)]
1440
mod tests {
1441
    use super::*;
1442
    use sxd_document::parser;
1443
    use crate::interface::{trim_element, get_element};
1444
1445
1446
4
    fn init_word_list() {
1447
4
        crate::interface::set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
1448
4
        crate::interface::set_preference("Language", "en").unwrap();
1449
4
        let result = crate::definitions::read_definitions_file(true);
1450
4
        if let Err(
e0
) = result {
1451
0
            panic!("unable to read 'Rules/Languages/en/definitions.yaml\n{e}");
1452
4
        }
1453
4
    }
1454
1455
    #[test]
1456
1
    fn ordinal_one_digit() {
1457
1
        init_word_list();
1458
1
        assert_eq!("zeroth", ToOrdinal::convert("0", false, false).unwrap());
1459
1
        assert_eq!("second", ToOrdinal::convert("2", false, false).unwrap());
1460
1
        assert_eq!("ninth", ToOrdinal::convert("9", false, false).unwrap());
1461
1462
1
        assert_eq!("zeroth", ToOrdinal::convert("0", false, true).unwrap());
1463
1
        assert_eq!("seconds", ToOrdinal::convert("2", false, true).unwrap());
1464
1
        assert_eq!("ninths", ToOrdinal::convert("9", false, true).unwrap());
1465
1466
1
        assert_eq!("first", ToOrdinal::convert("1", true, false).unwrap());
1467
1
        assert_eq!("half", ToOrdinal::convert("2", true, false).unwrap());
1468
1
        assert_eq!("half", ToOrdinal::convert("02", true, false).unwrap());
1469
1
        assert_eq!("ninth", ToOrdinal::convert("9", true, false).unwrap());
1470
1471
1
        assert_eq!("halves", ToOrdinal::convert("2", true, true).unwrap());
1472
1
        assert_eq!("halves", ToOrdinal::convert("002", true, true).unwrap());
1473
1
        assert_eq!("ninths", ToOrdinal::convert("9", true, true).unwrap());
1474
1
    }
1475
1476
    #[test]
1477
1
    fn ordinal_two_digit() {
1478
1
        init_word_list();
1479
1
        assert_eq!("tenth", ToOrdinal::convert("10", false, false).unwrap());
1480
1
        assert_eq!("seventeenth", ToOrdinal::convert("17", false, false).unwrap());
1481
1
        assert_eq!("thirty second", ToOrdinal::convert("32", false, false).unwrap());
1482
1
        assert_eq!("fortieth", ToOrdinal::convert("40", false, false).unwrap());
1483
1484
1
        assert_eq!("tenths", ToOrdinal::convert("10", false, true).unwrap());
1485
1
        assert_eq!("sixteenths", ToOrdinal::convert("16", false, true).unwrap());
1486
1
        assert_eq!("eighty eighths", ToOrdinal::convert("88", false, true).unwrap());
1487
1
        assert_eq!("fiftieths", ToOrdinal::convert("50", false, true).unwrap());
1488
1489
1
        assert_eq!("eleventh", ToOrdinal::convert("11", true, false).unwrap());
1490
1
        assert_eq!("forty fourth", ToOrdinal::convert("44", true, false).unwrap());
1491
1
        assert_eq!("ninth", ToOrdinal::convert("9", true, false).unwrap());
1492
1
        assert_eq!("ninth", ToOrdinal::convert("00000009", true, false).unwrap());
1493
1
        assert_eq!("sixtieth", ToOrdinal::convert("60", true, false).unwrap());
1494
1495
1
        assert_eq!("tenths", ToOrdinal::convert("10", true, true).unwrap());
1496
1
        assert_eq!("tenths", ToOrdinal::convert("0010", true, true).unwrap());
1497
1
        assert_eq!("elevenths", ToOrdinal::convert("11", true, true).unwrap());
1498
1
        assert_eq!("nineteenths", ToOrdinal::convert("19", true, true).unwrap());
1499
1
        assert_eq!("twentieths", ToOrdinal::convert("20", true, true).unwrap());
1500
1
        assert_eq!("nineteenths", ToOrdinal::convert("𝟏𝟗", true, true).unwrap());
1501
1
    }
1502
1503
    #[test]
1504
1
    fn ordinal_three_digit() {
1505
1
        init_word_list();
1506
1
        assert_eq!("one hundred first", ToOrdinal::convert("101", false, false).unwrap());
1507
1
        assert_eq!("two hundred tenth", ToOrdinal::convert("210", false, false).unwrap());
1508
1
        assert_eq!("four hundred thirty second", ToOrdinal::convert("432", false, false).unwrap());
1509
1
        assert_eq!("four hundred second", ToOrdinal::convert("402", false, false).unwrap());
1510
1511
1
        assert_eq!("one hundred first", ToOrdinal::convert("101", true, false).unwrap());
1512
1
        assert_eq!("two hundred second", ToOrdinal::convert("202", true, false).unwrap());
1513
1
        assert_eq!("four hundred thirty second", ToOrdinal::convert("432", true, false).unwrap());
1514
1
        assert_eq!("five hundred third", ToOrdinal::convert("503", true, false).unwrap());
1515
1516
1
        assert_eq!("three hundred elevenths", ToOrdinal::convert("311", false, true).unwrap());
1517
1
        assert_eq!("four hundred ninety ninths", ToOrdinal::convert("499", false, true).unwrap());
1518
1
        assert_eq!("nine hundred ninetieths", ToOrdinal::convert("990", false, true).unwrap());
1519
1
        assert_eq!("six hundred seconds", ToOrdinal::convert("602", false, true).unwrap());
1520
1521
1
        assert_eq!("seven hundredths", ToOrdinal::convert("700", true, true).unwrap());
1522
1
        assert_eq!("one hundredths", ToOrdinal::convert("100", true, true).unwrap());
1523
1
        assert_eq!("eight hundred seventeenths", ToOrdinal::convert("817", true, true).unwrap());
1524
1
    }
1525
    #[test]
1526
1
    fn ordinal_large() {
1527
1
        init_word_list();
1528
1
        assert_eq!("one thousandth", ToOrdinal::convert("1000", false, false).unwrap());
1529
1
        assert_eq!("two thousand one hundredth", ToOrdinal::convert("2100", false, false).unwrap());
1530
1
        assert_eq!("thirty thousandth", ToOrdinal::convert("30000", false, false).unwrap());
1531
1
        assert_eq!("four hundred thousandth", ToOrdinal::convert("400000", false, false).unwrap());
1532
1533
1
        assert_eq!("four hundred thousandth", ToOrdinal::convert("400000", true, false).unwrap());
1534
1
        assert_eq!("five hundred thousand second", ToOrdinal::convert("500002", true, false).unwrap());
1535
1
        assert_eq!("six millionth", ToOrdinal::convert("6000000", true, false).unwrap());
1536
1
        assert_eq!("sixty millionth", ToOrdinal::convert("60000000", true, false).unwrap());
1537
1538
1
        assert_eq!("seven billionths", ToOrdinal::convert("7000000000", false, true).unwrap());
1539
1
        assert_eq!("eight trillionths", ToOrdinal::convert("8000000000000", false, true).unwrap());
1540
1
        assert_eq!("nine quadrillionths", ToOrdinal::convert("9000000000000000", false, true).unwrap());
1541
1
        assert_eq!("one quintillionth", ToOrdinal::convert("1000000000000000000", false, false).unwrap());
1542
1543
1
        assert_eq!("nine billion eight hundred seventy six million five hundred forty three thousand two hundred tenths", ToOrdinal::convert("9876543210", true, true).unwrap());
1544
1
        assert_eq!("nine billion five hundred forty three thousand two hundred tenths", ToOrdinal::convert("9000543210", true, true).unwrap());
1545
1
        assert_eq!("zeroth", ToOrdinal::convert("00000", false, false).unwrap());
1546
1
    }
1547
1548
1549
11
    fn test_is_simple(message: &'static str, mathml_str: &'static str) {
1550
    // this forces initialization
1551
11
    crate::speech::SPEECH_RULES.with(|_| true);
1552
11
        let package = parser::parse(mathml_str)
1553
11
        .expect("failed to parse XML");
1554
11
        let mathml = get_element(&package);
1555
11
        trim_element(mathml, false);
1556
11
        assert!(IsNode::is_simple(mathml), "{}", message);
1557
11
    }
1558
1559
7
    fn test_is_not_simple(message: &'static str, mathml_str: &'static str) {
1560
    // this forces initialization
1561
7
    crate::speech::SPEECH_RULES.with(|_| true);
1562
7
        let package = parser::parse(mathml_str)
1563
7
        .expect("failed to parse XML");
1564
7
        let mathml = get_element(&package);
1565
7
        trim_element(mathml, false);
1566
7
        assert!(!IsNode::is_simple(mathml), "{}", message);
1567
7
    }
1568
    #[test]
1569
1
    fn is_simple() {
1570
1
        test_is_simple("single variable", "<mi>x</mi>");
1571
1
        test_is_simple("single number", "<mn>1.2</mn>");
1572
1
        test_is_simple("negative number", "<mrow><mo>-</mo><mn>10</mn></mrow>");
1573
1
        test_is_simple("negative variable", "<mrow><mo>-</mo><mi>x</mi></mrow>");
1574
1
        test_is_simple("ordinal fraction", "<mfrac><mn>3</mn><mn>4</mn></mfrac>");
1575
1
        test_is_simple("x y", "<mrow><mi>x</mi><mo>&#x2062;</mo><mi>y</mi></mrow>");
1576
1
        test_is_simple("negative two vars", 
1577
                "<mrow><mrow><mo>-</mo><mi>x</mi></mrow><mo>&#x2062;</mo><mi>y</mi></mrow>");
1578
1
        test_is_simple("-2 x y", 
1579
                "<mrow><mrow><mo>-</mo><mn>2</mn></mrow>
1580
                             <mo>&#x2062;</mo><mi>x</mi><mo>&#x2062;</mo><mi>z</mi></mrow>");
1581
1
        test_is_simple("sin x", "<mrow><mi>sin</mi><mo>&#x2061;</mo><mi>x</mi></mrow>");
1582
1
        test_is_simple("f(x)", "<mrow><mi>f</mi><mo>&#x2061;</mo><mrow><mo>(</mo><mi>x</mi><mo>)</mo></mrow></mrow>");
1583
1
        test_is_simple("f(x+y)",
1584
         "<mrow><mi>f</mi><mo>&#x2061;</mo>\
1585
            <mrow><mo>(</mo><mi>x</mi><mo>+</mo><mi>y</mi><mo>)</mo></mrow></mrow>");
1586
        
1587
1
    }
1588
1589
    #[test]
1590
1
    fn is_not_simple() {
1591
1
        test_is_not_simple("multi-char variable", "<mi>rise</mi>");
1592
1
        test_is_not_simple("large ordinal fraction", "<mfrac><mn>30</mn><mn>4</mn></mfrac>");
1593
1
        test_is_not_simple("fraction with var in numerator", "<mfrac><mi>x</mi><mn>4</mn></mfrac>");
1594
1
        test_is_not_simple("square root", "<msqrt><mi>x</mi></msqrt>");
1595
1
        test_is_not_simple("subscript", "<msub><mi>x</mi><mn>4</mn></msub>");
1596
1
        test_is_not_simple("-x y z", 
1597
                "<mrow><mrow><mo>-</mo><mi>x</mi></mrow>
1598
                            <mo>&#x2062;</mo><mi>y</mi><mo>&#x2062;</mo><mi>z</mi></mrow>");
1599
1
        test_is_not_simple("C(-2,1,4)",             // github.com/NSoiffer/MathCAT/issues/199
1600
                    "<mrow><mi>C</mi><mrow><mo>(</mo><mo>−</mo><mn>2</mn><mo>,</mo><mn>1</mn><mo>,</mo><mn>4</mn><mo>)</mo></mrow></mrow>");
1601
                   
1602
1
    }
1603
1604
    #[test]
1605
1
    fn at_left_edge() {
1606
1
        let mathml = "<math><mfrac><mrow><mn>30</mn><mi>x</mi></mrow><mn>4</mn></mfrac></math>";
1607
1
        let package = parser::parse(mathml).expect("failed to parse XML");
1608
1
        let mathml = get_element(&package);
1609
1
        trim_element(mathml, false);
1610
1
        let fraction = as_element(mathml.children()[0]);
1611
1
        let mn = as_element(as_element(fraction.children()[0]).children()[0]);
1612
1
        assert_eq!(EdgeNode::edge_node(mn, true, "2D"), Some(fraction));
1613
1
        assert_eq!(EdgeNode::edge_node(mn, false, "2D"), None);
1614
1615
1
        let mi = as_element(as_element(fraction.children()[0]).children()[1]);
1616
1
        assert_eq!(EdgeNode::edge_node(mi, true, "2D"), None);
1617
1
    }
1618
1619
    #[test]
1620
1
    fn at_right_edge() {
1621
1
        let mathml = "<math><mrow><mfrac><mn>4</mn><mrow><mn>30</mn><mi>x</mi></mrow></mfrac><mo>.</mo></mrow></math>";
1622
1
        let package = parser::parse(mathml).expect("failed to parse XML");
1623
1
        let mathml = get_element(&package);
1624
1
        trim_element(mathml, false);
1625
1
        let fraction = as_element(as_element(mathml.children()[0]).children()[0]);
1626
1
        let mi = as_element(as_element(fraction.children()[1]).children()[1]);
1627
1
        assert_eq!(EdgeNode::edge_node(mi, true, "2D"), None);
1628
1
        assert_eq!(EdgeNode::edge_node(mi, false, "2D"), Some(fraction));
1629
1
        assert_eq!(EdgeNode::edge_node(mi, false, "math"), Some(mathml));
1630
1631
1
        let mn = as_element(as_element(fraction.children()[1]).children()[0]);
1632
1
        assert_eq!(EdgeNode::edge_node(mn, true, "2D"), None);
1633
1
    }
1634
}