## 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 && !0is_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.12kis_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") || !115is_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 && !116kis_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
    }