## 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/canonicalize.rs

    Line
    Count
    Source
    1
    //! Converts the MathML to some sort of canonical MathML.
    2
    //!
    3
    //! Some changes made:
    4
    //! * &extra whitespace at the start/end of tokens is trimmed.
    5
    //! * "equivalent" characters are converted to a chosen character.
    6
    //! * known "bad" MathML is cleaned up (this will likely be an ongoing effort)
    7
    //! * mrows are added based on operator priorities from the MathML Operator Dictionary
    8
    #![allow(clippy::needless_return)]
    9
    use crate::errors::*;
    10
    use std::rc::Rc;
    11
    use std::cell::RefCell;
    12
    use sxd_document::dom::{Element, Document, ChildOfElement, Attribute};
    13
    use sxd_document::QName;
    14
    use phf::{phf_map, phf_set};
    15
    use crate::xpath_functions::{IsBracketed, is_leaf, IsNode};
    16
    use std::ptr::eq as ptr_eq;
    17
    use crate::pretty_print::*;
    18
    use regex::Regex;
    19
    use std::fmt;
    20
    use crate::chemistry::*;
    21
    use unicode_script::Script;
    22
    use roman_numerals_rs::RomanNumeral;
    23
    use std::sync::LazyLock;
    24
    use log::{debug};
    25
    use bitflags::bitflags;
    26
    27
    // FIX: DECIMAL_SEPARATOR should be set by env, or maybe language
    28
    const DECIMAL_SEPARATOR: &str = ".";
    29
    pub const CHANGED_ATTR: &str = "data-changed";
    30
    pub const ADDED_ATTR_VALUE: &str = "added";
    31
    pub const INTENT_ATTR: &str = "intent";
    32
    pub const MATHML_FROM_NAME_ATTR: &str = "data-from-mathml";
    33
    const MFENCED_ATTR_VALUE: &str = "from_mfenced";
    34
    const EMPTY_IN_2D: &str = "data-empty-in-2D";
    35
    const SPACE_AFTER: &str = "data-space-after";
    36
    const ACT_AS_OPERATOR: &str = "data-acts_as_operator";
    37
    // character to use instead of the text content for priority, etc.
    38
    pub const CHEMICAL_BOND: &str ="data-chemical-bond";
    39
    40
    41
    /// Used when mhchem is detected and we should favor postscripts rather than prescripts in constructing an mmultiscripts
    42
    const MHCHEM_MMULTISCRIPTS_HACK: &str = "MHCHEM_SCRIPT_HACK";
    43
    44
    // (perfect) hash of operators built from MathML's operator dictionary
    45
    static OPERATORS: phf::Map<&str, OperatorInfo> = include!("operator-info.in");
    46
    47
    48
    // The set of fence operators that can being either a left or right fence (or infix). For example: "|".
    49
    static AMBIGUOUS_OPERATORS: phf::Set<&str> = phf_set! {
    50
      "|", "∥", "\u{2016}"
    51
    };
    52
    53
    // static vars used when canonicalizing
    54
    // lowest priority operator so it is never popped off the stack
    55
    static LEFT_FENCEPOST: OperatorInfo = OperatorInfo{ op_type: OperatorTypes::LEFT_FENCE, priority: 0, next: &None };
    56
    57
    3
    static INVISIBLE_FUNCTION_APPLICATION: LazyLock<&'static OperatorInfo> = LazyLock::new(|| OPERATORS.get("\u{2061}").unwrap());
    58
    3
    static IMPLIED_TIMES: LazyLock<&'static OperatorInfo> = LazyLock::new(|| OPERATORS.get("\u{2062}").unwrap());
    59
    2
    static IMPLIED_INVISIBLE_COMMA: LazyLock<&'static OperatorInfo> = LazyLock::new(|| OPERATORS.get("\u{2063}").unwrap());
    60
    3
    static IMPLIED_INVISIBLE_PLUS: LazyLock<&'static OperatorInfo> = LazyLock::new(|| OPERATORS.get("\u{2064}").unwrap());
    61
    62
    // FIX: any other operators that should act the same (e.g, plus-minus and minus-plus)?
    63
    3
    static PLUS: LazyLock<&'static OperatorInfo> = LazyLock::new(|| OPERATORS.get("+").unwrap());
    64
    3
    static MINUS: LazyLock<&'static OperatorInfo> = LazyLock::new(|| OPERATORS.get("-").unwrap());
    65
    3
    static PREFIX_MINUS: LazyLock<&'static OperatorInfo> = LazyLock::new(|| MINUS.next.as_ref().unwrap());
    66
    67
    3
    static TIMES_SIGN: LazyLock<&'static OperatorInfo> = LazyLock::new(|| OPERATORS.get("×").unwrap());
    68
    69
    // IMPLIED_TIMES_HIGH_PRIORITY -- used in trig functions for things like sin 2x cos 2x where want > function app priority
    70
    static IMPLIED_TIMES_HIGH_PRIORITY: OperatorInfo = OperatorInfo{
    71
      op_type: OperatorTypes::INFIX, priority: 851, next: &None
    72
    };
    73
    // IMPLIED_SEPARATOR_HIGH_PRIORITY -- used for Geometry points like ABC
    74
    static IMPLIED_SEPARATOR_HIGH_PRIORITY: OperatorInfo = OperatorInfo{
    75
      op_type: OperatorTypes::INFIX, priority: 901, next: &None
    76
    };
    77
    // IMPLIED_CHEMICAL_BOND -- used for implicit and explicit bonds
    78
    static IMPLIED_CHEMICAL_BOND: OperatorInfo = OperatorInfo{
    79
      op_type: OperatorTypes::INFIX, priority: 905, next: &None
    80
    };
    81
    static IMPLIED_PLUS_SLASH_HIGH_PRIORITY: OperatorInfo = OperatorInfo{ // (linear) mixed fraction 2 3/4
    82
      op_type: OperatorTypes::INFIX, priority: 881, next: &None
    83
    };
    84
    85
    // Useful static defaults to have available if there is no character match
    86
    static DEFAULT_OPERATOR_INFO_PREFIX: OperatorInfo = OperatorInfo{
    87
      op_type: OperatorTypes::PREFIX, priority: 260, next: &None
    88
    };
    89
    static DEFAULT_OPERATOR_INFO_INFIX: OperatorInfo = OperatorInfo{
    90
      op_type: OperatorTypes::INFIX, priority: 260, next:& None
    91
    };
    92
    static DEFAULT_OPERATOR_INFO_POSTFIX: OperatorInfo = OperatorInfo{
    93
      op_type: OperatorTypes::POSTFIX, priority: 260, next: &None
    94
    };
    95
    96
    // avoids having to use Option<OperatorInfo> in some cases
    97
    static ILLEGAL_OPERATOR_INFO: OperatorInfo = OperatorInfo{
    98
      op_type: OperatorTypes::INFIX, priority: 999, next: &None
    99
    };
    100
    101
    // used to tell if an operator is a relational operator
    102
    1
    static EQUAL_PRIORITY: LazyLock<usize> = LazyLock::new(|| OPERATORS.get("=").unwrap().priority);
    103
    104
    // useful for detecting whitespace
    105
    3
    static IS_WHITESPACE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s+$").unwrap());    // only Unicode whitespace
    106
    107
    // Operators are either PREFIX, INFIX, or POSTFIX, but can also have other properties such as LEFT_FENCE
    108
    bitflags! {
    109
      #[derive(Clone, Debug, Copy, PartialEq, Eq)]
    110
      struct OperatorTypes: u32 {
    111
        const NONE    = 0x0;
    112
        const PREFIX  = 0x1;
    113
        const INFIX   = 0x2;
    114
        const POSTFIX = 0x4;
    115
        const FENCE   = 0x8;
    116
        const LEFT_FENCE= 0x9;
    117
        const RIGHT_FENCE=0xc;
    118
        const UNSPECIFIED=0xf;    // 'and-ing will match anything
    119
      }
    120
    }
    121
    // OperatorInfo is a key structure for parsing.
    122
    // They OperatorInfo is this program's representation of MathML's Operator Dictionary.
    123
    // The OperatorTypes say how the operator can group (can be overridden with @form="..." on an element).
    124
    //   Basically, it says the operator can be at the start, middle, or end of an mrow.
    125
    // The priority field gives the relationships between operators so that lower priority operators are towards the root of the tree.
    126
    //   E.g.,  '=' is lower priority than (infix) '+', which in turn is lower priority than multiplication.
    127
    // The operator info is a linked list because some operators (not many) have alternatives (e.g, '+' is both prefix and infix)
    128
    // All OperatorInfo is static info, with some special static defaults to capture when it is not listed in the operator dictionary.
    129
    #[derive(Clone, Debug)]
    130
    struct OperatorInfo {
    131
      op_type: OperatorTypes,   // can be set on <mo>
    132
      priority: usize,      // not settable on an element
    133
      next: &'static Option<OperatorInfo>,  // can be both prefix & infix (etc) -- chain of options
    134
    }
    135
    136
    // The character is separated out from the OperatorInfo as this allows the OperatorInfo to be static (can use default values)
    137
    #[derive(Clone, Debug)]
    138
    struct OperatorPair<'op> {
    139
      ch: &'op str,
    140
      op: &'static OperatorInfo
    141
    }
    142
    143
    impl<'op> OperatorPair<'op> {
    144
    57.3k
      fn new() -> OperatorPair<'op> {
    145
    57.3k
        return OperatorPair{
    146
    57.3k
          ch: "illegal",          // value 'illegal' used only in debugging, if then
    147
    57.3k
          op: &ILLEGAL_OPERATOR_INFO,   // ILLEGAL_OPERATOR_INFO avoids using <Option>
    148
    57.3k
        };
    149
    57.3k
      }
    150
    }
    151
    152
    // OperatorVersions is a convenient data structure when looking to see whether the operator should be prefix, infix, or postfix.
    153
    // It is only used in one place in the code, so this could maybe be eliminated and the code localized to where it is used.
    154
    #[derive(Debug)]
    155
    struct OperatorVersions {
    156
      prefix: Option<&'static OperatorInfo>,
    157
      infix: Option<&'static OperatorInfo>,
    158
      postfix: Option<&'static OperatorInfo>,
    159
    }
    160
    161
    impl OperatorVersions {
    162
    401
      fn new(op: &'static OperatorInfo) -> OperatorVersions {
    163
    401
        let mut op = op;
    164
    401
        let mut prefix = None;
    165
    401
        let mut infix = None;
    166
    401
        let mut postfix = None;
    167
        loop {
    168
    1.10k
          if op.is_prefix() {
    169
    360
            prefix = Some( op );
    170
    745
          } else if op.is_infix() {
    171
    385
            infix = Some( op )
    172
    360
          } else if op.is_postfix() {
    173
    360
            postfix = Some( op );
    174
    360
          } else {
    175
    0
            panic!("OperatorVersions::new: operator is not prefix, infix, or postfix")
    176
          }
    177
          //let another_op = op.next;
    178
    1.10k
          match &op.next {
    179
    401
            None => break,
    180
    704
            Some(alt_op) => op = alt_op,
    181
          }
    182
        }
    183
    401
        return OperatorVersions{prefix, infix, postfix};
    184
    401
      }
    185
    }
    186
    187
    188
    impl OperatorInfo {
    189
    13.1k
      fn is_prefix(&self) -> bool {
    190
    13.1k
        return (self.op_type & OperatorTypes::PREFIX) != OperatorTypes::NONE;
    191
    13.1k
      }
    192
    193
    805
      fn is_infix(&self) -> bool {
    194
    805
        return (self.op_type & OperatorTypes::INFIX) != OperatorTypes::NONE;
    195
    805
      }
    196
    197
    14.2k
      fn is_postfix(&self) -> bool {
    198
    14.2k
        return (self.op_type & OperatorTypes::POSTFIX) != OperatorTypes::NONE;
    199
    14.2k
      }
    200
    201
    13.9k
      fn is_left_fence(&self) -> bool {
    202
    13.9k
        return self.op_type & OperatorTypes::LEFT_FENCE == OperatorTypes::LEFT_FENCE;
    203
    13.9k
      }
    204
    205
    12.9k
      fn is_right_fence(&self) -> bool {
    206
    12.9k
        return self.op_type & OperatorTypes::RIGHT_FENCE ==OperatorTypes::RIGHT_FENCE;
    207
    12.9k
      }
    208
    209
    4.84k
      fn is_fence(&self) -> bool {
    210
    4.84k
        return (self.op_type & (OperatorTypes::LEFT_FENCE | OperatorTypes::RIGHT_FENCE)) != OperatorTypes::NONE;
    211
    4.84k
      }
    212
    213
    21.3k
      fn is_operator_type(&self, op_type: OperatorTypes) -> bool {
    214
    21.3k
        return self.op_type & op_type != OperatorTypes::NONE;
    215
    21.3k
      }
    216
    217
    13.5k
      fn is_plus_or_minus(&self) -> bool {
    218
    13.5k
        return ptr_eq(self, *PLUS) || ptr_eq13.0k(self13.0k, *MINUS13.0k);
    219
    13.5k
      }
    220
    221
    13.2k
      fn is_times(&self) -> bool {
    222
    13.2k
        return ptr_eq(self, *IMPLIED_TIMES) || ptr_eq13.0k(self13.0k, *TIMES_SIGN13.0k);
    223
    13.2k
      }
    224
    225
    17.7k
      fn is_nary(&self, previous_op: &OperatorInfo) -> bool {
    226
    17.7k
        return  ptr_eq(previous_op,self) ||
    227
    13.0k
            (previous_op.is_plus_or_minus() && self506.is_plus_or_minus506()) ||
    228
    13.0k
            (previous_op.is_times() && self163.is_times163());
    229
    17.7k
      }
    230
    }
    231
    232
    // StackInfo contains all the needed information for deciding shift/reduce during parsing.
    233
    // The stack itself is just a Vec of StackInfo (since we only push, pop, and look at the top)
    234
    // There are a number of useful functions defined on StackInfo.
    235
    struct StackInfo<'a, 'op>{
    236
      mrow: Element<'a>,      // mrow being built
    237
      op_pair: OperatorPair<'op>, // last operator placed on stack
    238
      is_operand: bool,     // true if child at end of mrow is an operand (as opposed to an operator)
    239
    }
    240
    241
    impl fmt::Display for StackInfo<'_, '_> {
    242
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    243
    0
            write!(f, "StackInfo(op={}/{}, is_operand={}, mrow({}",
    244
    0
            show_invisible_op_char(self.op_pair.ch), self.op_pair.op.priority, self.is_operand,
    245
    0
            if self.mrow.children().is_empty() {")"} else {""})?;
    246
    0
        for child in self.mrow.children() {
    247
    0
          let child = as_element(child);
    248
    0
          write!(f, "{}{}", name(child), if child.following_siblings().is_empty() {")"} else {","})?;
    249
        }
    250
    0
            return Ok( () );
    251
    0
        }
    252
    }
    253
    254
    impl<'a, 'op:'a> StackInfo<'a, 'op> {
    255
    10.6k
      fn new(doc: Document<'a>) -> StackInfo<'a, 'op> {
    256
        // debug!("  new empty StackInfo");
    257
    10.6k
        let mrow = create_mathml_element(&doc, "mrow") ;
    258
    10.6k
        mrow.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    259
    10.6k
        return StackInfo{
    260
    10.6k
          mrow,
    261
    10.6k
          op_pair: OperatorPair{ ch: "\u{E000}", op: &LEFT_FENCEPOST },
    262
    10.6k
          is_operand: false,
    263
    10.6k
        }
    264
    10.6k
      }
    265
    266
    10.9k
      fn with_op<'d>(doc: &'d Document<'a>, node: Element<'a>, op_pair: OperatorPair<'op>) -> StackInfo<'a, 'op> {
    267
        // debug!("  new StackInfo with '{}' and operator {}/{}", name(node), show_invisible_op_char(op_pair.ch), op_pair.op.priority);
    268
    10.9k
        let mrow = create_mathml_element(doc, "mrow");
    269
    10.9k
        mrow.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    270
    10.9k
        mrow.append_child(node);
    271
    10.9k
        return StackInfo {
    272
    10.9k
          mrow,
    273
    10.9k
          op_pair,
    274
    10.9k
          is_operand: false,
    275
    10.9k
        }
    276
    10.9k
      }
    277
    278
    37.2k
      fn priority(&self) -> usize {
    279
    37.2k
        return self.op_pair.op.priority;
    280
    37.2k
      }
    281
    282
    37.1k
      fn last_child_in_mrow(&self) -> Option<Element<'a>> {
    283
    37.1k
        let children = self.mrow.children();
    284
    37.1k
        for &child29.5k in children.iter().rev() {
    285
    29.5k
          let child = as_element(child);
    286
    29.5k
          if let Some(value687) = child.attribute_value(CHANGED_ATTR)
    287
    687
            && value == "empty_content" {
    288
    0
              continue;
    289
    29.5k
            }
    290
    29.5k
          return Some(child);
    291
        }
    292
    7.58k
        return None;
    293
    37.1k
      }
    294
    295
    57.6k
      fn add_child_to_mrow(&mut self, child: Element<'a>, child_op: OperatorPair<'op>) {
    296
        // debug!("  adding '{}' to mrow[{}], operator '{}/{}'",
    297
        //    element_summary(child), self.mrow.children().len(), show_invisible_op_char(child_op.ch), child_op.op.priority);
    298
    57.6k
        self.mrow.append_child(child);
    299
    57.6k
        if ptr_eq(child_op.op, &ILLEGAL_OPERATOR_INFO) {
    300
    36.8k
          assert!(!self.is_operand);  // should not have two operands in a row (ok to add whitespace)
    301
    36.8k
          self.is_operand = true;
    302
    20.7k
        } else {
    303
    20.7k
          self.op_pair = child_op;
    304
    20.7k
          self.is_operand = false;
    305
    20.7k
        }
    306
    57.6k
      }
    307
    308
    18.4k
      fn remove_last_operand_from_mrow(&mut self) -> Element<'a> {
    309
    18.4k
        let children = self.mrow.children();
    310
    18.4k
        assert!( !children.is_empty() );
    311
    18.4k
        assert!( self.is_operand || children.len()==163 );   // could be operator that is forced to be interpreted as operand -- eg, bad input like "x+("
    312
    18.4k
        self.is_operand = false;
    313
    18.4k
        let last_operand = as_element(children[children.len()-1]);
    314
        // debug!("  Removing last element '{}' from mrow[{}]",element_summary(last_operand), children.len());
    315
    18.4k
        last_operand.remove_from_parent();
    316
    18.4k
        return last_operand;
    317
    18.4k
      }
    318
    319
    }
    320
    321
    322
    117k
    pub fn create_mathml_element<'a>(doc: &Document<'a>, name: &str) -> Element<'a> {
    323
    117k
      return doc.create_element(sxd_document::QName::with_namespace_uri(
    324
    117k
        Some("http://www.w3.org/1998/Math/MathML"),
    325
    117k
        name));
    326
    117k
    }
    327
    328
    4.84k
    pub fn is_fence(mo: Element) -> bool {
    329
    4.84k
      return CanonicalizeContext::find_operator(None, mo, None, None, None).is_fence();
    330
    4.84k
    }
    331
    332
    664
    pub fn is_relational_op(mo: Element) -> bool {
    333
    664
      return CanonicalizeContext::find_operator(None, mo, None, None, None).priority == *EQUAL_PRIORITY;
    334
    664
    }
    335
    336
    113k
    pub fn set_mathml_name(element: Element, new_name: &str) {
    337
    113k
      element.set_name(QName::with_namespace_uri(Some("http://www.w3.org/1998/Math/MathML"), new_name));
    338
    113k
    }
    339
    340
    /// Replace 'mathml' in the parent (must exist since this only happens for leaves) with the 'replacements' (new children).
    341
    /// This handles adding mrows if needed.
    342
    ///
    343
    /// Returns first replacement
    344
    2.47k
    pub fn replace_children<'a>(mathml: Element<'a>, replacements: Vec<Element<'a>>) -> Element<'a> {
    345
    2.47k
      let parent = get_parent(mathml);
    346
    2.47k
      let parent_name = name(parent);
    347
      // debug!("\nreplace_children: mathml\n{}", mml_to_string(mathml));
    348
      // debug!("replace_children: parent before replace\n{}", mml_to_string(parent));
    349
      // debug!("{} replacements:\n{}", replacements.len(), replacements.iter().map(|e| mml_to_string(e)).collect::<Vec<String>>().join("\n"));
    350
    2.47k
      if ELEMENTS_WITH_FIXED_NUMBER_OF_CHILDREN.contains(parent_name) ||
    351
    2.44k
         parent_name == "mmultiscripts" {     // each child acts like the parent has a fixed number of children
    352
        // gather up the preceding/following siblings before mucking with the tree structure (mrow.append_children below)
    353
    32
        let mut new_children = mathml.preceding_siblings();
    354
    32
        let mut following_siblings = mathml.following_siblings();
    355
    356
        // debug!("\nreplace_children: mathml\n{}", mml_to_string(mathml));
    357
        // debug!("replace_children: parent before replace\n{}", mml_to_string(parent));
    358
        // wrap an mrow around the replacements and then replace 'mathml' with that
    359
    32
        let mrow = create_mathml_element(&mathml.document(), "mrow");
    360
    32
        add_attrs(mrow, &replacements[0].attributes());
    361
    32
        mrow.append_children(replacements);
    362
    32
        new_children.push(ChildOfElement::Element(mrow));
    363
    32
        new_children.append(&mut following_siblings);
    364
    32
        parent.replace_children(new_children);
    365
        // debug!("replace_children parent after: parent\n{}", mml_to_string(parent));
    366
        // debug!("replace_children: returned mrow\n{}", mml_to_string(mrow));
    367
    32
        return mrow;
    368
      } else {
    369
        // replace the children of the parent with 'replacements' inserted in place of 'mathml'
    370
    2.44k
        let mut new_children = mathml.preceding_siblings();
    371
    2.44k
        let i_first_new_child = new_children.len();
    372
    6.54k
        let mut replacements2.44k = replacements.iter()2.44k.map2.44k(|&el| ChildOfElement::Element(el)).collect2.44k::<Vec<ChildOfElement>>();
    373
    2.44k
        new_children.append(&mut replacements);
    374
    2.44k
        new_children.append(&mut mathml.following_siblings());
    375
    2.44k
        parent.replace_children(new_children);
    376
        // debug!("replace_children: (will return child[{}]) parent after replace\n{}", i_first_new_child, mml_to_string(parent));
    377
    2.44k
        return as_element(parent.children()[i_first_new_child]);
    378
      }
    379
    2.47k
    }
    380
    381
    // returns the presentation element of a "semantics" element
    382
    22
    pub fn get_presentation_element(element: Element) -> (usize, Element) {
    383
    22
      assert_eq!(name(element), "semantics");
    384
    22
      let children = element.children();
    385
    22
      if let Some( (i20, child20) ) = children.iter().enumerate().find(|&(_, &child)|
    386
    48
          if let Some(encoding46) = as_element(child).attribute_value("encoding") {
    387
    46
            encoding == "MathML-Presentation"
    388
          } else {
    389
    2
            false
    390
    48
          })
    391
      {
    392
    20
        let presentation_annotation = as_element(*child);
    393
        // debug!("get_presentation_element:\n{}", mml_to_string(presentation_annotation));
    394
    20
        assert_eq!(presentation_annotation.children().len(), 1);
    395
    20
        return (i, as_element(presentation_annotation.children()[0]));
    396
      } else {
    397
    2
        return (0, as_element(children[0]));
    398
      }
    399
    22
    }
    400
    401
    /// Canonicalize does several things:
    402
    /// 1. cleans up the tree so all extra white space is removed (should only have element and text nodes)
    403
    /// 2. normalize the characters
    404
    /// 3. clean up "bad" MathML based on known output from some converters (TODO: still a work in progress)
    405
    /// 4. the tree is "parsed" based on the mo (priority)/mi/mn's in an mrow
    406
    ///    *  this adds mrows and some invisible operators (implied times, function app, ...)
    407
    ///    * extra mrows are removed
    408
    ///    * implicit mrows are turned into explicit mrows (e.g, there will be a single child of 'math')
    409
    ///
    410
    /// Canonicalize is pretty conservative in adding new mrows and won't do it if:
    411
    /// * there is an intent attr
    412
    /// * if the mrow starts and ends with a fence (e.g, French open interval "]0,1[")
    413
    ///
    414
    /// An mrow is never deleted unless it is redundant.
    415
    ///
    416
    /// Whitespace handling:
    417
    /// Whitespace complicates parsing and also pattern matching (e.g., is it a mixed number which tests for a number preceding a fraction)
    418
    /// The first attempt which mostly worked was to shove whitespace into adjacent mi/mn/mtext. That has a problem with distinguish different uses for whitespace
    419
    /// The second attempt was to leave it in the parse and make it an mo when appropriate, but there were some cases where it should be prefix and wasn't caught
    420
    /// The third attempt (and the current one) is to make it an attribute on adjacent elements.
    421
    ///   This preserves the data-width attr (with new name) added in the second attempt that helps resolve whether something is tweaking, a real space, or an omission.
    422
    ///   It adds data-previous-space-width/data-following-space-width with values to indicate with the space was on the left or right (typically it placed on the previous token because that's easier)
    423
    5.06k
    pub fn canonicalize(mathml: Element) -> Result<Element> {
    424
    5.06k
      let context = CanonicalizeContext::new();
    425
    5.06k
      return context.canonicalize(mathml);
    426
    5.06k
    }
    427
    428
    #[derive(Debug, PartialEq)]
    429
    enum FunctionNameCertainty {
    430
      True,
    431
      Maybe,
    432
      False
    433
    }
    434
    435
    436
    static ELEMENTS_WITH_ONE_CHILD: phf::Set<&str> = phf_set! {
    437
      "math", "msqrt", "merror", "mpadded", "mphantom", "menclose", "mtd", "mscarry"
    438
    };
    439
    440
    static ELEMENTS_WITH_FIXED_NUMBER_OF_CHILDREN: phf::Set<&str> = phf_set! {
    441
      "mfrac", "mroot", "msub", "msup", "msubsup","munder", "mover", "munderover"
    442
    };
    443
    444
    static EMPTY_ELEMENTS: phf::Set<&str> = phf_set! {
    445
      "mspace", "none", "mprescripts", "mglyph", "malignmark", "maligngroup", "msline",
    446
    };
    447
    448
    // turns out Roman Numerals tests aren't needed, but we do want to block VII from being a chemical match
    449
    // two cases because we don't want to have a match for 'Cl', etc.
    450
    3
    static UPPER_ROMAN_NUMERAL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})\s*$").unwrap());
    451
    3
    static LOWER_ROMAN_NUMERAL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*^m{0,3}(cm|cd|d?c{0,3})(xc|xl|l?x{0,3})(ix|iv|v?i{0,3})\s*$").unwrap());
    452
    453
    454
    struct CanonicalizeContextPatterns {
    455
      decimal_separator: Regex,
    456
      block_separator: Regex,
    457
      digit_only_decimal_number: Regex,
    458
      block_3digit_pattern: Regex,
    459
      block_3_5digit_pattern: Regex,
    460
      block_4digit_hex_pattern: Regex,
    461
      block_1digit_pattern: Regex,    // used when generator puts each digit into a single mn
    462
    }
    463
    464
    impl CanonicalizeContextPatterns {
    465
    4.10k
      fn new(block_separator_pref: &str, decimal_separator_pref: &str) -> CanonicalizeContextPatterns {
    466
    4.10k
        let block_separator = Regex::new(&format!("[{}]", regex::escape(block_separator_pref))).unwrap();
    467
    4.10k
        let decimal_separator = Regex::new(&format!("[{}]", regex::escape(decimal_separator_pref))).unwrap();
    468
        // allows just "." and also matches an empty string, but those are ruled out elsewhere
    469
    4.10k
        let digit_only_decimal_number = Regex::new(&format!(r"^\d*{}?\d*$", regex::escape(decimal_separator_pref))).unwrap();
    470
    4.10k
        let block_3digit_pattern = get_number_pattern_regex(block_separator_pref, decimal_separator_pref, 3, 3);
    471
    4.10k
        let block_3_5digit_pattern = get_number_pattern_regex(block_separator_pref, decimal_separator_pref, 3, 5);
    472
        // Note: on en.wikipedia.org/wiki/Decimal_separator, show '3.14159 26535 89793 23846'
    473
    4.10k
        let block_4digit_hex_pattern =  Regex::new(r"^[0-9a-fA-F]{4}([ \u00A0\u202F][0-9a-fA-F]{4})*$").unwrap();
    474
    4.10k
        let block_1digit_pattern =  Regex::new(r"^((\d(\uFFFF\d)?)(\d([, \u00A0\u202F]\d){2})*)?([\.](\d(\uFFFF\d)*)?)?$").unwrap();
    475
    476
    4.10k
        return CanonicalizeContextPatterns {
    477
    4.10k
          block_separator,
    478
    4.10k
          decimal_separator,
    479
    4.10k
          digit_only_decimal_number,
    480
    4.10k
          block_3digit_pattern,
    481
    4.10k
          block_3_5digit_pattern,
    482
    4.10k
          block_4digit_hex_pattern,
    483
    4.10k
          block_1digit_pattern
    484
    4.10k
        };
    485
    486
    487
    8.21k
        fn get_number_pattern_regex(block_separator: &str, decimal_separator: &str, n_sep_before: usize, n_sep_after: usize) -> Regex {
    488
          // the following is a generalization of a regex like ^(\d*|\d{1,3}([, ]?\d{3})*)(\.(\d*|(\d{3}[, ])*\d{1,3}))?$
    489
          // that matches something like '1 234.567 8' and '1,234.', but not '1,234.12,34
    490
    8.21k
          return Regex::new(&format!(r"^(\d*|\d{{1,{}}}([{}]?\d{{{}}})*)([{}](\d*|(\d{{{}}}[{}])*\d{{1,{}}}))?$",
    491
    8.21k
                  n_sep_before, regex::escape(block_separator), n_sep_before, regex::escape(decimal_separator),
    492
    8.21k
                  n_sep_after, regex::escape(block_separator), n_sep_after) ).unwrap();
    493
    8.21k
        }
    494
    4.10k
      }
    495
    }
    496
    497
    /// Profiling showed that creating new contexts was very time consuming because creating the RegExs is very expensive
    498
    /// Profiling set_mathml (which does the canonicalization) spends 65% of the time in Regex::new, of which half of it is spent in this initialization.
    499
    struct CanonicalizeContextPatternsCache {
    500
      block_separator_pref: String,
    501
      decimal_separator_pref: String,
    502
      patterns: Rc<CanonicalizeContextPatterns>,
    503
    }
    504
    505
    thread_local!{
    506
        static PATTERN_CACHE: RefCell<CanonicalizeContextPatternsCache> = RefCell::new(CanonicalizeContextPatternsCache::new());
    507
    }
    508
    509
    impl CanonicalizeContextPatternsCache {
    510
    4.10k
      fn new() -> CanonicalizeContextPatternsCache {
    511
    4.10k
        let pref_manager = crate::prefs::PreferenceManager::get();
    512
    4.10k
        let pref_manager = pref_manager.borrow();
    513
    4.10k
        let block_separator_pref = pref_manager.pref_to_string("BlockSeparators");
    514
    4.10k
        let decimal_separator_pref = pref_manager.pref_to_string("DecimalSeparators");
    515
    4.10k
        return CanonicalizeContextPatternsCache {
    516
    4.10k
          patterns: Rc::new( CanonicalizeContextPatterns::new(&block_separator_pref, &decimal_separator_pref) ),
    517
    4.10k
          block_separator_pref,
    518
    4.10k
          decimal_separator_pref
    519
    4.10k
        }
    520
    4.10k
      }
    521
    522
    5.06k
      fn get() -> Rc<CanonicalizeContextPatterns> {
    523
    5.06k
        return PATTERN_CACHE.with( |cache| {
    524
    5.06k
          let pref_manager_rc = crate::prefs::PreferenceManager::get();
    525
    5.06k
          let pref_manager = pref_manager_rc.borrow();
    526
    5.06k
          let block_separator_pref = pref_manager.pref_to_string("BlockSeparators");
    527
    5.06k
          let decimal_separator_pref = pref_manager.pref_to_string("DecimalSeparators");
    528
    529
    5.06k
          let mut cache = cache.borrow_mut();
    530
    5.06k
          if block_separator_pref != cache.block_separator_pref || decimal_separator_pref != cache.decimal_separator_pref {
    531
    0
            // update the cache
    532
    0
            cache.patterns = Rc::new( CanonicalizeContextPatterns::new(&block_separator_pref, &decimal_separator_pref) );
    533
    0
            cache.block_separator_pref = block_separator_pref;
    534
    0
            cache.decimal_separator_pref = decimal_separator_pref;
    535
    5.06k
          }
    536
    5.06k
          return cache.patterns.clone();
    537
    5.06k
        })
    538
    5.06k
      }
    539
    }
    540
    541
    struct CanonicalizeContext {
    542
      patterns: Rc<CanonicalizeContextPatterns>,
    543
    }
    544
    545
    546
    impl CanonicalizeContext {
    547
    5.06k
      fn new() -> CanonicalizeContext {
    548
    5.06k
        return CanonicalizeContext {
    549
    5.06k
          patterns: CanonicalizeContextPatternsCache::get(),
    550
    5.06k
        };
    551
    5.06k
      }
    552
    553
    5.06k
      fn canonicalize<'a>(&self, mut mathml: Element<'a>) -> Result<Element<'a>> {
    554
        // debug!("MathML before canonicalize:\n{}", mml_to_string(mathml));
    555
    556
    5.06k
        if name(mathml) != "math" {
    557
    0
          // debug!("Didn't start with <math> element -- attempting repair");
    558
    0
          let math_element = create_mathml_element(&mathml.document(), "math");
    559
    0
          math_element.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    560
    0
          math_element.append_child(mathml);
    561
    0
          let root = math_element.document().root();
    562
    0
          root.clear_children();
    563
    0
          root.append_child(math_element);
    564
    0
          mathml = root.children()[0].element().unwrap();
    565
    5.06k
        }
    566
    5.06k
        CanonicalizeContext::assure_mathml(mathml, 0)?4;
    567
    5.05k
        let mathml = self.clean_mathml(mathml).unwrap(); // 'math' is never removed
    568
    5.05k
        self.assure_nary_tag_has_one_child(mathml);
    569
        // debug!("Not chemistry -- retry:\n{}", mml_to_string(mathml));
    570
    5.05k
        let mut converted_mathml = self.canonicalize_mrows(mathml)
    571
    5.05k
            .with_context(|| format!0("while processing\n{}", mml_to_string0(mathml0)))?0;
    572
        // debug!("canonicalize before canonicalize_mrows:\n{}", mml_to_string(converted_mathml));
    573
    5.05k
        if !crate::chemistry::scan_and_mark_chemistry(converted_mathml) {
    574
    869
          self.assure_nary_tag_has_one_child(converted_mathml);
    575
    869
          converted_mathml = self.canonicalize_mrows(mathml)
    576
    869
            .with_context(|| format!0("while processing\n{}", mml_to_string0(mathml0)))?0;
    577
    4.18k
        }
    578
    5.05k
        debug!("\nMathML after canonicalize:\n{}", mml_to_string0(converted_mathml0));
    579
    5.05k
        return Ok(converted_mathml);
    580
    5.06k
      }
    581
    582
      /// Make sure there is exactly one child
    583
    19.1k
      fn assure_nary_tag_has_one_child(&self, mathml: Element) {
    584
    19.1k
        let children = mathml.children();
    585
    19.1k
        if !ELEMENTS_WITH_ONE_CHILD.contains(name(mathml)) {
    586
    6.43k
          return;
    587
    12.7k
        }
    588
    589
    12.7k
        if children.is_empty() {
    590
    3
          // make sure there is content
    591
    3
          let child = CanonicalizeContext::create_empty_element(&mathml.document());
    592
    3
          mathml.append_child(child);
    593
    12.7k
        } else if children.len() > 1 {
    594
    2.34k
          // wrap the children in an mrow
    595
    2.34k
          let mrow = create_mathml_element(&mathml.document(), "mrow");
    596
    2.34k
          mrow.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    597
    2.34k
          mrow.append_children(children);
    598
    2.34k
          mathml.replace_children(vec![ChildOfElement::Element(mrow)]);
    599
    10.3k
        }
    600
    19.1k
      }
    601
    602
      /// Return an error if some element is not MathML (only look at first child of <semantics>) or if it has the wrong number of children
    603
    52.9k
      fn assure_mathml(mathml: Element, depth: usize) -> Result<()> {
    604
    52.9k
        if depth > crate::interface::MAX_DEPTH {
    605
    1
          bail!("MathML is too deeply nested to process");
    606
    52.9k
        }
    607
    52.9k
        let n_children = mathml.children().len();
    608
    52.9k
        let element_name = name(mathml);
    609
    52.9k
        if is_leaf(mathml) {
    610
    33.1k
          if EMPTY_ELEMENTS.contains(element_name) {
    611
    464
            if n_children != 0 {
    612
    0
              bail!("{} should only have one child:\n{}", element_name, mml_to_string(mathml));
    613
    464
            }
    614
    32.7k
          } else if element_name == "annotation" {
    615
    0
            bail!("'annotation' element is not child of 'semantics' element");
    616
    32.7k
          } else if (n_children == 1 && mathml.children()[0].text()32.6k.is_some32.6k()) || n_children == 018 {  // allow empty children such as mtext
    617
    32.7k
            return Ok( () );
    618
          } else {
    619
    0
            bail!("Not a valid MathML leaf element:\n{}", mml_to_string(mathml));
    620
          };
    621
    19.7k
        }
    622
    623
    20.2k
        if ELEMENTS_WITH_FIXED_NUMBER_OF_CHILDREN.contains(element_name) {
    624
    3.90k
          match element_name {
    625
    3.90k
            "munderover" | "msubsup"3.84k => if n_children != 3277 {
    626
    0
              bail!("{} should have 3 children:\n{}", element_name, mml_to_string(mathml));
    627
    277
            },
    628
    3.62k
            _ => if n_children != 2 {
    629
    0
              bail!("{} should have 2 children:\n{}", element_name, mml_to_string(mathml));
    630
    3.62k
            },
    631
          }
    632
    16.3k
        } else if matches!2.18k(element_name, "mtd" | "mtr"14.8k | "mlabeledtr"14.1k)  {
    633
    2.18k
          let parent_name = name(get_parent(mathml));
    634
    2.18k
          if (element_name == "mtr" || element_name == "mlabeledtr"1.47k) && parent_name != "mtable"722 {
    635
    0
            bail!("Illegal MathML: {} is not a child of mtable. Parent is {}", element_name, mml_to_string(get_parent(mathml)));
    636
    2.18k
          } else if element_name == "mtd" && !(parent_name == "mtr"1.45k || parent_name == "mlabeledtr"57) {
    637
    1
            bail!("Illegal MathML: mtd is not a child of {}. Parent is {}", parent_name, mml_to_string(get_parent(mathml)));
    638
    2.17k
          }
    639
        }
    640
    14.1k
        else if element_name == "mmultiscripts" {
    641
    182
          let has_prescripts = mathml.children().iter()
    642
    649
              .any182(|&child| name(as_element(child)) == "mprescripts");
    643
    182
          if has_prescripts ^ (n_children.is_multiple_of(2)) {
    644
    1
            bail!("{} has the wrong number of children:\n{}", element_name, mml_to_string(mathml));
    645
    181
          }
    646
    13.9k
        } else if element_name == "mlongdiv" {
    647
    0
          if n_children < 3 {
    648
    0
            bail!("{} should have at least 3 children:\n{}", element_name, mml_to_string(mathml));
    649
    0
          }
    650
    13.9k
        } else if element_name == "semantics" {
    651
    11
          let children = mathml.children();
    652
    11
          if children.is_empty() {
    653
    0
            return Ok( () );
    654
          } else {
    655
    11
            let (i_presentation, presentation_element) = get_presentation_element(mathml);
    656
            // make sure only 'annotation' and 'annotation-xml' elements are children of the non-presentation element
    657
    24
            for (i, child) in children.iter()11.enumerate11() {
    658
    24
              if i != i_presentation {
    659
    13
                let child = as_element(*child);
    660
    13
                if name(child)!="annotation" && name(child)!="annotation-xml"1 {
    661
    0
                  bail!("Illegal MathML: {} is child of 'semantic'", name(child));
    662
    13
                }
    663
    11
              }
    664
            }
    665
    11
            return CanonicalizeContext::assure_mathml(presentation_element, depth + 1);
    666
          }
    667
    13.9k
        } else if !IsNode::is_mathml(mathml) {
    668
    1
          if element_name == "annotation-xml" {
    669
    0
            bail!("'annotation-xml' element is not child of 'semantics' element");
    670
          } else {
    671
    1
            bail!("'{}' is not a valid MathML element", element_name);
    672
          }
    673
    13.9k
        }
    674
    675
        // valid MathML element and not a leaf -- check the children
    676
    47.8k
        for child in mathml20.2k.children20.2k() {
    677
    47.8k
          CanonicalizeContext::assure_mathml( as_element(child), depth + 1)?520;
    678
        }
    679
    19.6k
        return Ok( () );
    680
    52.9k
      }
    681
    682
    283
      fn make_empty_element(mathml: Element) -> Element {
    683
    283
        set_mathml_name(mathml, "mtext");
    684
    283
        mathml.clear_children();
    685
    283
        mathml.set_text("\u{00A0}");
    686
    283
        mathml.set_attribute_value("data-changed", "empty_content");
    687
    283
        mathml.set_attribute_value("data-width", "0");
    688
    283
        return mathml;
    689
    283
      }
    690
    691
    24
      fn create_empty_element<'a>(doc: &Document<'a>) -> Element<'a> {
    692
    24
        let mtext = create_mathml_element(doc, "mtext");
    693
    24
        mtext.set_text("\u{00A0}");
    694
    24
        mtext.set_attribute_value("data-added", "missing-content");
    695
    24
        mtext.set_attribute_value("data-width", "0");
    696
    24
        return mtext;
    697
    24
      }
    698
    699
    11.5k
      fn is_empty_element(el: Element) -> bool {
    700
    11.5k
        return (is_leaf(el) && as_text(el).trim()7.55k.is_empty7.55k()) ||
    701
    11.0k
             (name(el) == "mrow" && el.children()1.33k.is_empty1.33k() && el.attribute(INTENT_ATTR)0.is_none0());
    702
    11.5k
      }
    703
    704
    705
      // this should only be called for 2D elements
    706
    4.48k
      fn mark_empty_content(two_d_element: Element) {
    707
    7.32k
        for child in two_d_element4.48k.children4.48k() {
    708
    7.32k
          let child = as_element(child);
    709
    7.32k
          if CanonicalizeContext::is_empty_element(child) {
    710
    20
            child.set_attribute_value(EMPTY_IN_2D, "true");
    711
    7.30k
          }
    712
        }
    713
    4.48k
      }
    714
    715
      /// Turn leaf into an 'mn' and set attributes appropriately
    716
    34
      fn make_roman_numeral(leaf: Element) {
    717
    34
        assert!(is_leaf(leaf));
    718
    34
        set_mathml_name(leaf, "mn");
    719
    34
        leaf.set_attribute_value("data-roman-numeral", "true");  // mark for easy detection
    720
    34
        let as_number = match as_text(leaf).parse::<RomanNumeral>() {
    721
    34
          Ok(roman) => roman.as_u16().to_string(),
    722
    0
          Err(_) => as_text(leaf).to_string(),
    723
        };
    724
    34
        leaf.set_attribute_value("data-number", &as_number);
    725
    34
      }
    726
    727
      /// most of the time it is ok to merge the mrow with its singleton child, but there are some exceptions:
    728
      ///   mrow has 'intent' -- this might reference the child and you aren't allowed to self reference
    729
    2.82k
      fn is_ok_to_merge_mrow_child(mrow: Element) -> bool {
    730
    2.82k
        assert_eq!(name(mrow), "mrow");
    731
    2.82k
        assert!(mrow.children().len() == 1);
    732
    2.82k
        return mrow.attribute(INTENT_ATTR).is_none();   // could check if child is referenced, but that's a chunk of code
    733
    2.82k
      }
    734
    735
      /// This function does some cleanup of MathML (mostly fixing bad MathML)
    736
      /// Unlike the main canonicalization routine, significant tree changes happen here
    737
      /// Changes to "good" MathML:
    738
      /// 1. mfenced -> mrow, a => mrow
    739
      /// 2. mspace and mtext with only whitespace are canonicalized to a non-breaking space and merged in with
    740
      ///    an adjacent non-mo element unless in a required element position (need to keep for braille)
    741
      ///
    742
      /// Note: mspace that is potentially part of a number that was split apart is merged into a number as a single space char
    743
      ///
    744
      /// mstyle, mpadded, and mphantom, malignmark, maligngroup are removed (but children might be kept)
    745
      ///
    746
      /// Significant changes are made cleaning up empty bases of scripts, looking for chemistry, merging numbers with commas,
    747
      ///   "arg trig" functions, pseudo scripts, and others
    748
      ///
    749
      /// Returns 'None' if the element should not be in the tree.
    750
    52.3k
      fn clean_mathml<'a>(&self, mathml: Element<'a>) -> Option<Element<'a>> {
    751
        // Note: this works bottom-up (clean the children first, then this element)
    752
    3
        static IS_PRIME: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"['′″‴⁗]").unwrap());
    753
    754
        // Note: including intervening spaces in what is likely a symbol of omission preserves any notion of separate digits (e.g., "_ _ _")
    755
    3
        static IS_UNDERSCORE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[_\u{00A0}]+$").unwrap());
    756
    757
    758
    23.5k
        fn is_currency_symbol(ch: char) -> bool {
    759
    23.5k
          matches!23.5k(ch, '$' | '¢' | '€' | '£' | '₡' | '₤' | '₨' | '₩' | '₪' | '₱' | '₹' | '₺' | '₿')
    760
    23.5k
        }
    761
    762
    20.0k
        fn contains_currency(s: &str) -> bool {
    763
    20.0k
          s.chars().any(is_currency_symbol)
    764
    20.0k
        }
    765
    766
        // begin by cleaning up empty elements
    767
        // debug!("clean_mathml\n{}", mml_to_string(mathml));
    768
    52.3k
        let element_name = name(mathml);
    769
    52.3k
        let parent_name = if element_name == "math" {
    770
    5.09k
          "math".to_string()
    771
        } else {
    772
    47.2k
          let parent = get_parent(mathml);
    773
    47.2k
          name(parent).to_string()
    774
        };
    775
    52.3k
        let parent_requires_child = ELEMENTS_WITH_FIXED_NUMBER_OF_CHILDREN.contains(&parent_name) ||
    776
    44.0k
                          matches!2.18k(parent_name.as_ref(), "mtr" | "mlabeledtr"42.6k | "mtable"42.5k) ||
    777
    41.8k
                          parent_name == "mmultiscripts";
    778
    779
        // handle empty leaves -- leaving it empty causes problems with the speech rules
    780
    52.3k
        if is_leaf(mathml) && !32.8kEMPTY_ELEMENTS32.8k.contains(element_name) && as_text(mathml)32.3k.is_empty32.3k() {
    781
    32
          return if parent_requires_child {Some( CanonicalizeContext::make_empty_element(mathml) )4} else {None28};
    782
    52.3k
        };
    783
    784
    52.3k
        if mathml.children().is_empty() && !734EMPTY_ELEMENTS734.contains(element_name) {
    785
    158
          if element_name == "mrow" && mathml.attribute(INTENT_ATTR)143.is_none143() {
    786
            // if it is an empty mrow that doesn't need to be there, get rid of it. Otherwise, replace it with an mtext
    787
    142
            if parent_name == "mmultiscripts" && !mathml.preceding_siblings().is_empty()5 {
    788
              // MathML Core dropped "none" in favor of <mrow/>, but MathCAT is written with <none/>
    789
              // Do substitutions for the scripts, not the base
    790
    4
              set_mathml_name(mathml, "none");
    791
    4
              return Some(mathml);
    792
    138
            }
    793
    138
            if parent_requires_child {
    794
    14
              return Some( CanonicalizeContext::make_empty_element(mathml) );
    795
            } else {
    796
    124
              return None;
    797
            }
    798
    16
          } else {
    799
    16
            // create some content so that speech rules don't require special cases
    800
    16
            let mtext = CanonicalizeContext::create_empty_element(&mathml.document());
    801
    16
            mathml.append_child(mtext);
    802
    16
            // return Some(mathml);
    803
    16
          }
    804
    52.1k
        };
    805
    806
    52.1k
        match element_name {
    807
    52.1k
          "mn" => {
    808
    9.08k
            let text = as_text(mathml);
    809
    9.08k
            let mut chars = text.chars();
    810
    9.08k
            let first_char = chars.next().unwrap();   // we have already made sure it is non-empty
    811
    9.08k
            if !text.trim().is_empty() && is_roman_number_match(text) {
    812
    2
              // people tend to set them in a non-italic font and software makes that 'mtext'
    813
    2
              CanonicalizeContext::make_roman_numeral(mathml);
    814
    9.08k
            } else if matches!9.08k(first_char, '-' | '\u{2212}') {
    815
    5
              let doc = mathml.document();
    816
    5
              let mo = create_mathml_element(&doc, "mo");
    817
    5
              let mn = create_mathml_element(&doc, "mn");
    818
    5
              mo.set_text("-");
    819
    5
              mn.set_text(&text[first_char.len_utf8()..]);
    820
    5
              set_mathml_name(mathml, "mrow");
    821
    5
              mathml.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    822
    5
              mathml.replace_children([mo,mn]);
    823
    9.08k
            } else if contains_currency(text) && let Some(result3) = split_currency_symbol(mathml)3 {
    824
    3
              return Some(result);
    825
    9.07k
            }
    826
    9.08k
            if let Some((idx, last_char)) = text.char_indices().next_back() {
    827
              // look for something like 12°
    828
    9.08k
              if is_pseudo_script_char(last_char) {
    829
    1
                let doc = mathml.document();
    830
    1
                let mn = create_mathml_element(&doc, "mn");
    831
    1
                let mo = create_mathml_element(&doc, "mo");
    832
    1
                mn.set_text(&text[..idx]);
    833
    1
                mo.set_text(last_char.to_string().as_str());
    834
    1
                set_mathml_name(mathml, "msup");
    835
    1
                mathml.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    836
    1
                mathml.replace_children([mn, mo]);
    837
    9.08k
              }
    838
    0
            }
    839
    9.08k
            return Some(mathml);
    840
          },
    841
    43.0k
          "ms" | "mglyph"43.0k => {
    842
    3
            return Some(mathml);
    843
          },
    844
    43.0k
          "mi" => {
    845
    11.6k
            let text = as_text(mathml);
    846
    11.6k
            if !text.trim().is_empty() && is_roman_number_match(text) && is_roman_numeral_number_context3.32k(mathml3.32k) {
    847
              // people tend to set them in a non-italic font and software makes that 'mtext'
    848
    28
              CanonicalizeContext::make_roman_numeral(mathml);
    849
    28
              return Some(mathml);
    850
    11.5k
            }
    851
    11.5k
            if let Some(dash1) = canonicalize_dash(text) {    // needs to be before OPERATORS.get due to "--"
    852
    1
              mathml.set_text(dash);
    853
    1
              return Some(mathml);
    854
    11.5k
            } else if text.contains('_') {
    855
              // if left or right are an mo, leave as is. Otherwise convert to an mo.
    856
    6
              let preceding_siblings = mathml.preceding_siblings();
    857
    6
              let following_siblings = mathml.following_siblings();
    858
    6
              if preceding_siblings.is_empty() || following_siblings.is_empty() {
    859
    4
                return Some(mathml);
    860
    2
              }
    861
    2
              if name(as_element(preceding_siblings[preceding_siblings.len()-1])) != "mo" &&
    862
    2
                 name(as_element(following_siblings[0])) != "mo" {
    863
    2
                set_mathml_name(mathml, "mo");
    864
    2
              }0
    865
    2
              return Some(mathml);
    866
    11.5k
            } else if OPERATORS.get(text).is_some() {
    867
    118
              if  let Some(intent_value88) = mathml.attribute_value(INTENT_ATTR) {
    868
                // if it is a unit, it might be seconds, minutes, feet, ... not an operator
    869
    88
                if intent_value.contains(":unit") {
    870
    88
                  return Some(mathml);
    871
    0
                }
    872
    30
              }
    873
    30
              set_mathml_name(mathml, "mo");
    874
    875
              // For at least pandoc, ∇ is an 'mi' and it sometimes adds an invisible times -- remove them
    876
    30
              let op = OPERATORS.get(text).unwrap();
    877
    30
              let preceding_siblings = mathml.preceding_siblings();
    878
    30
              if (op.is_infix() || op17.is_postfix17()) &&
    879
    16
                 !preceding_siblings.is_empty() && CanonicalizeContext::is_invisible_char_element15(as_element15(preceding_siblings[0]15)) {
    880
    0
                as_element(preceding_siblings[0]).remove_from_parent();
    881
    30
              }
    882
    30
              let following_siblings = mathml.following_siblings();
    883
    30
              if (op.is_infix() || op17.is_prefix17()) &&
    884
    27
                 !following_siblings.is_empty() && CanonicalizeContext::is_invisible_char_element(as_element(following_siblings[0])) {
    885
    0
                as_element(following_siblings[0]).remove_from_parent();
    886
    30
              }
    887
    30
              return Some(mathml);
    888
    11.4k
            } else if let Some(result1) = split_apart_pseudo_scripts(mathml) {
    889
    1
                return Some(result);
    890
    11.4k
            } else if let Some(result0) = merge_arc_trig(mathml) {
    891
    0
                return Some(result);
    892
    11.4k
            } else if IS_PRIME.is_match(text) {
    893
    0
              let new_text = merge_prime_text(text);
    894
    0
              mathml.set_text(&new_text);
    895
    0
              return Some(mathml);
    896
    11.4k
            } else if text == "..." {
    897
    1
              mathml.set_text("…");
    898
    1
              return Some(mathml);
    899
    11.4k
            } else if let Some(result27) = split_points(mathml) {
    900
    27
              return Some(result);
    901
    11.4k
            } else if let Some(result11) = merge_mi_sequence(mathml) {
    902
    11
              return Some(result);
    903
            } else {
    904
    11.4k
              return Some(mathml);
    905
            };
    906
          },
    907
    31.4k
          "mtext" => {
    908
            // debug!("before merge_arc_trig: {}", mml_to_string(mathml));
    909
    910
    401
            if let Some(result2) = merge_arc_trig(mathml) {
    911
    2
              return Some(result);
    912
    399
            } else if let Some(result11) = split_points(mathml) {
    913
    11
              return Some(result);
    914
    388
            }
    915
    916
    388
            let text = as_text(mathml);
    917
    388
            if !text.trim().is_empty() && is_roman_number_match317(text317) && is_roman_numeral_number_context33(mathml33) {
    918
              // people tend to set them in a non-italic font and software makes that 'mtext'
    919
    4
              CanonicalizeContext::make_roman_numeral(mathml);
    920
    4
              return Some(mathml);
    921
    449
            } else if text.chars()384.all384(|c| c.is_ascii_digit() || matches!332(c445, '.' | ',' | ' ' | '\u{00A0}')) &&
    922
    58
                      text.chars()52.any52(|c| c.is_ascii_digit()){  // does it look like a number?
    923
    1
              mathml.set_name("mn");
    924
    1
              return Some(mathml);
    925
    383
            } else if contains_currency(text) && let Some(result0) = split_currency_symbol(mathml)0 {
    926
    0
              return Some(result);
    927
    383
            }
    928
            // common bug: trig functions, lim, etc., should be mi
    929
    383
            if ["…", "⋯", "∞"].contains(&text) ||
    930
    383
               crate::definitions::SPEECH_DEFINITIONS.with(|definitions|
    931
    383
              if let Some(hashset382) = definitions.borrow().get_hashset("FunctionNames") {
    932
    382
                hashset.contains(text)
    933
              } else {
    934
    1
                false
    935
    383
              }
    936
            ) {
    937
    6
              set_mathml_name(mathml, "mi");
    938
    6
              return Some(mathml);
    939
    377
            }
    940
    941
            // allow non-breaking whitespace to stay -- needed by braille
    942
    377
            if IS_WHITESPACE.is_match(text) {
    943
              // normalize to just a single non-breaking space
    944
    71
              mathml.set_attribute_value("data-width", &format!("{:.3}", white_space_em_width(text)));
    945
    71
              mathml.set_text("\u{00A0}");
    946
    71
              return Some(mathml);
    947
    306
            } else if let Some(dash2) = canonicalize_dash(text) {
    948
    2
              mathml.set_text(dash);
    949
    304
            } else if OPERATORS.get(text).is_some() {
    950
    11
              set_mathml_name(mathml, "mo");
    951
    11
              return Some(mathml);
    952
    293
            }
    953
    295
            return if parent_requires_child || !text.is_empty()220 {Some(mathml)} else {None0};
    954
          },
    955
    31.0k
          "mo" => {
    956
            // WIRIS editor puts non-breaking whitespace as standalone in 'mo'
    957
    11.2k
            let text = as_text(mathml);
    958
    11.2k
            if !text.is_empty() && IS_WHITESPACE.is_match(text) {
    959
              // can't throw it out because it is needed by braille -- change to what it really is
    960
    78
              set_mathml_name(mathml, "mtext");
    961
    78
              mathml.set_attribute_value("data-width", &format!("{:.3}", white_space_em_width(text)));
    962
    78
              mathml.set_text("\u{00A0}");
    963
    78
              mathml.set_attribute_value(CHANGED_ATTR, "data-was-mo");
    964
    78
              return Some(mathml);
    965
            } else {
    966
    11.1k
              match text {
    967
    11.1k
                "arc" | "arc " | "arc " /* non-breaking space */ => {
    968
    0
                  if let Some(result) = merge_arc_trig(mathml) {
    969
    0
                    return Some(result);
    970
    0
                  }
    971
                },
    972
    11.1k
                "..." => {0mathml0.set_text("…");},  // name might need to change -- checked below
    973
    11.1k
                ":" => {
    974
    94
                  if is_ratio(mathml) {
    975
    8
                    mathml.set_text("∶"); // ratio U+2236
    976
    86
                  }
    977
    94
                  return Some(mathml);
    978
                },
    979
    11.0k
                "::" =>{9mathml9.set_text("∷");},
    980
    11.0k
                "│" => {0mathml0.set_text("|");}, // ASCII vertical bar
    981
    11.0k
                "|" | "||"10.7k => if let Some(result6) = merge_vertical_bars(mathml)305 {
    982
    6
                  return Some(result);
    983
                } else {
    984
    299
                  return Some(mathml);
    985
                },
    986
    10.7k
                _ => (),
    987
              }
    988
            }
    989
    990
            // common bug: trig functions, lim, etc., should be mi
    991
            // same for ellipsis ("…")
    992
    10.7k
            return crate::definitions::SPEECH_DEFINITIONS.with(|definitions| {
    993
    10.7k
              if ["…", "⋯", "∞"].contains(&text) ||
    994
    10.7k
                 definitions.borrow().get_hashset("FunctionNames").unwrap().contains(text) ||
    995
    10.6k
                 definitions.borrow().get_hashset("GeometryShapes").unwrap().contains(text) {
    996
    83
                set_mathml_name(mathml, "mi");
    997
    83
                return Some(mathml);
    998
    10.6k
              }
    999
    10.6k
              if IS_PRIME.is_match(text) {
    1000
    66
                let new_text = merge_prime_text(text);
    1001
    66
                mathml.set_text(&new_text);
    1002
    66
                return Some(mathml);
    1003
    10.5k
              }
    1004
    10.5k
              if contains_currency(text) && let Some(result9) = split_currency_symbol(mathml)9 {
    1005
    9
                return Some(result);
    1006
    10.5k
              }
    1007
    10.5k
              return Some(mathml);
    1008
    10.7k
            });
    1009
            // note: chemistry test is done later as part of another phase of chemistry cleanup
    1010
          },
    1011
    19.8k
          "mfenced" => {return self40.clean_mathml40( convert_mfenced_to_mrow40(mathml40) )},
    1012
    19.8k
          "a" => {
    1013
            // convert 'a' into 'mrow'
    1014
    2
            set_mathml_name(mathml, "mrow");
    1015
    2
            return self.clean_mathml(mathml);
    1016
          }
    1017
    19.8k
          "mstyle" | "mpadded"19.7k => {
    1018
            // Throw out mstyle and mpadded -- to do this, we need to avoid mstyle being the arg of clean_mathml
    1019
            // FIX: should probably push the attrs down to the children (set in 'self')
    1020
    714
            merge_adjacent_similar_mstyles(mathml);
    1021
    714
            let children = mathml.children();
    1022
    714
            if children.is_empty() {
    1023
    0
              return if parent_requires_child {Some( CanonicalizeContext::make_empty_element(mathml) )} else {None};
    1024
    714
            } else if children.len() == 1 {
    1025
    678
              let is_from_mhchem = element_name == "mpadded" && is_from_mhchem_hack588(mathml588);
    1026
    678
              if let Some(new_mathml269) = self.clean_mathml( as_element(children[0]) ) {
    1027
                // "lift" the child up so all the links (e.g., siblings) are correct
    1028
    269
                mathml.replace_children(new_mathml.children());
    1029
    269
                set_mathml_name(mathml, name(new_mathml));
    1030
    269
                add_attrs(mathml, &new_mathml.attributes());
    1031
    269
                return Some(mathml);
    1032
    409
              } else if parent_requires_child {
    1033
                // need a placeholder -- make it empty mtext
    1034
    31
                let empty = CanonicalizeContext::make_empty_element(mathml);
    1035
    31
                if is_from_mhchem {
    1036
    27
                  empty.set_attribute_value(MHCHEM_MMULTISCRIPTS_HACK, "true");
    1037
    27
                }4
    1038
    31
                return Some(empty);
    1039
              } else {
    1040
    378
                return None;
    1041
              }
    1042
            } else {
    1043
              // wrap the children in an mrow, but maintain tree siblings by changing mpadded/mstyle to mrow
    1044
    36
              set_mathml_name(mathml, "mrow");
    1045
    36
              mathml.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    1046
    36
              return self.clean_mathml(mathml);  // now it's an mrow so a different path next time
    1047
            }
    1048
          },
    1049
    19.0k
          "mphantom" | "malignmark"18.7k | "maligngroup"18.7k=> {
    1050
    364
            return if parent_requires_child {Some( CanonicalizeContext::make_empty_element(mathml) )0} else {None};
    1051
          },
    1052
    18.7k
          "mspace" => {
    1053
            // need to hold onto space for braille
    1054
    224
            set_mathml_name(mathml, "mtext");
    1055
    224
            mathml.set_text("\u{00A0}");
    1056
    224
            mathml.set_attribute_value(CHANGED_ATTR, "was-mspace");
    1057
    1058
            // normalize width ems
    1059
    224
            let width = mathml.attribute_value("width").unwrap_or("0em");
    1060
    224
            let normalized_width = crate::xpath_functions::FontSizeGuess::em_from_value(width);
    1061
    224
            mathml.set_attribute_value("data-width", &normalized_width.to_string());
    1062
    224
            return Some(mathml);
    1063
          },
    1064
    18.5k
          "semantics" => {
    1065
            // The semantics tag, like the style tag, can mess with pattern matching.
    1066
            // However, it may be the case that having the annotations could aid in determining intent, so we want to keep them.
    1067
            // The compromise is to move the annotations into an attr named data-annotation[-xml]-<encoding-name>
    1068
            // The attribute is put on presentation element root
    1069
    11
            let presentation = get_presentation_element(mathml).1;
    1070
    11
            let new_presentation = if let Some(presentation) = self.clean_mathml(presentation) {
    1071
    11
              presentation
    1072
            } else {
    1073
              // probably shouldn't happen, but just in case
    1074
    0
              CanonicalizeContext::create_empty_element(&mathml.document())
    1075
            };
    1076
    11
            set_annotation_attrs(new_presentation, mathml);
    1077
    11
            return Some(new_presentation);
    1078
          },
    1079
          _  => {
    1080
    18.5k
            let children = mathml.children();
    1081
    18.5k
            if element_name == "mrow" {
    1082
              // handle special cases of empty mrows and mrows which just one element
    1083
    6.04k
              if children.is_empty() && mathml.attribute(INTENT_ATTR)0.is_none0() {
    1084
    0
                return if parent_requires_child {Some(mathml)} else {None};
    1085
    6.04k
              } else if children.len() == 1 && CanonicalizeContext::is_ok_to_merge_mrow_child2.58k(mathml2.58k) {
    1086
    2.56k
                let is_from_mhchem = is_from_mhchem_hack(mathml);
    1087
    2.56k
                if let Some(new_mathml1.95k) = self.clean_mathml(as_element(children[0])) {
    1088
                  // "lift" the child up so all the links (e.g., siblings) are correct
    1089
    1.95k
                  mathml.replace_children(new_mathml.children());
    1090
    1.95k
                  set_mathml_name(mathml, name(new_mathml));
    1091
    1.95k
                  add_attrs(mathml, &new_mathml.attributes());
    1092
    1.95k
                  return Some(mathml);
    1093
    607
                } else if parent_requires_child {
    1094
    234
                  let empty = CanonicalizeContext::make_empty_element(mathml);
    1095
    234
                  if is_from_mhchem {
    1096
    142
                    empty.set_attribute_value(MHCHEM_MMULTISCRIPTS_HACK, "true");
    1097
    142
                  }92
    1098
    234
                  return Some(empty);
    1099
                } else {
    1100
    373
                  return None;
    1101
                }
    1102
    3.48k
              }
    1103
    12.4k
            }
    1104
    1105
            // FIX: this should be setting children, not mathml
    1106
    15.9k
            let mathml =  if element_name == "mrow" ||
    1107
    12.4k
                  (children.len() > 1 && ELEMENTS_WITH_ONE_CHILD7.31k.contains7.31k(element_name7.31k)) {
    1108
    5.90k
              let merged = merge_dots(mathml);  // FIX -- switch to passing in children
    1109
    5.90k
              let merged = merge_primes(merged);
    1110
    5.90k
              let merged = merge_degrees_C_F(merged);
    1111
    5.90k
              let merged = merge_chars(merged, &IS_UNDERSCORE);
    1112
    5.90k
              handle_pseudo_scripts(merged)
    1113
            } else {
    1114
    10.0k
              mathml
    1115
            };
    1116
    1117
            // cleaning children can add or delete subsequent children, so we need to constantly update the children (and mathml)
    1118
    15.9k
            let mut children = mathml.children();
    1119
    15.9k
            let mut i = 0;
    1120
    1121
    59.7k
            while i < children.len() {
    1122
    43.9k
              if let Some(child) = children[i].element() {
    1123
    43.9k
                match self.clean_mathml(child) {
    1124
    299
                  None => {
    1125
    299
                    mathml.remove_child(child);
    1126
    299
                    // don't increment 'i' because there is one less child now and so everything shifted left
    1127
    299
                  },
    1128
    43.6k
                  Some(new_child) => {
    1129
                    // debug!("new_child (i={})\n{}", i, mml_to_string(new_child));
    1130
    43.6k
                    let new_child_name = name(new_child);
    1131
    43.6k
                    children = mathml.children();       // clean_mathml(child) may have changed following siblings
    1132
    43.6k
                    children[i] = ChildOfElement::Element(new_child);
    1133
    43.6k
                    mathml.replace_children(children);
    1134
    43.6k
                    if new_child_name == "mi" || new_child_name == "mtext"31.9k {
    1135
    12.5k
                      // can't do this above in 'match' because this changes the tree and
    1136
    12.5k
                      // lifting single element mrows messes with structure in a conflicting way
    1137
    12.5k
                      // Note: if clean_chemistry_leaf() made changes, they don't need cleaning because they will be "ok" mi's
    1138
    12.5k
                      clean_chemistry_leaf(as_element(mathml.children()[i]));
    1139
    12.5k
                    } else {
    1140
                      // If the attach call does something, children are inserted *before* child (i.e., into parent)
    1141
                      // We return the new start at the expense of re-cleaning the script
    1142
                      // This is needed because anything before the returned element will be lost
    1143
    31.0k
                      let start_of_change = attach_scripts_to_split_element(new_child);
    1144
    31.0k
                      if name(start_of_change) == "mrow" {
    1145
    3.43k
                        start_of_change.remove_attribute(MAYBE_CHEMISTRY);   // was lifted, and not set -- remove and it will be computed later
    1146
    27.6k
                      }
    1147
                      // crate::canonicalize::assure_mathml(get_parent(start_of_change)).unwrap();    // FIX: find a recovery -- we're in deep trouble if this isn't true
    1148
    31.0k
                      if start_of_change != child {
    1149
                        // debug!("clean_mathml: start_of_change != mathml -- mathml={}", mml_to_string(mathml));
    1150
    49
                        return self.clean_mathml(mathml);  // restart cleaning
    1151
    30.9k
                      }
    1152
                    }
    1153
    43.5k
                    i += 1;
    1154
                  }
    1155
                }
    1156
    43.8k
                children = mathml.children();           // 'children' moved above, so need new values
    1157
    0
              } else {
    1158
    0
                // bad mathml such as '<annotation-xml> </annotation-xml>' -- don't add to new_children
    1159
    0
                i += 1;
    1160
    0
              }
    1161
            }
    1162
    1163
            // could have deleted children so only one child remains -- need to lift it
    1164
    15.8k
            if element_name == "mrow" && children.len() == 13.47k && CanonicalizeContext::is_ok_to_merge_mrow_child122(mathml122) {
    1165
              // "lift" the child up so all the links (e.g., siblings) are correct
    1166
    108
              let child = as_element(children[0]);
    1167
    108
              mathml.replace_children(child.children());
    1168
    108
              set_mathml_name(mathml, name(child));
    1169
    108
              add_attrs(mathml, &child.attributes());
    1170
    108
              return Some(mathml);   // child has already been cleaned, so we can return
    1171
    15.7k
            }
    1172
    1173
    15.7k
            if element_name == "mrow" || ELEMENTS_WITH_ONE_CHILD12.4k.contains12.4k(element_name12.4k) {
    1174
    10.1k
              merge_number_blocks(self, mathml, &mut children);
    1175
    10.1k
              merge_whitespace(&mut children);
    1176
    10.1k
              merge_cross_or_dot_product_elements(&mut children);
    1177
    10.1k
              handle_convert_to_mmultiscripts(&mut children);
    1178
    10.1k
            } else if element_name == "msub"5.59k || element_name == "msup"4.81k ||
    1179
    3.48k
                  element_name == "msubsup" || element_name == "mmultiscripts"3.25k{
    1180
    2.52k
              if element_name != "mmultiscripts" {
    1181
                // mhchem emits some cases that boil down to a completely empty script -- see test mhchem_beta_decay
    1182
    2.33k
                let mut is_empty_script = CanonicalizeContext::is_empty_element(as_element(children[0])) &&
    1183
    181
                                  CanonicalizeContext::is_empty_element(as_element(children[1]));
    1184
    2.33k
                if element_name == "msubsup" && is_empty_script228 {
    1185
    51
                  is_empty_script = CanonicalizeContext::is_empty_element(as_element(children[2]));
    1186
    2.28k
                }
    1187
    2.33k
                if is_empty_script {
    1188
    48
                  if parent_requires_child {
    1189
                    // need a placeholder -- make it empty mtext
    1190
    0
                    return Some( as_element(children[0]) ); // pick one of the empty elements
    1191
                  } else {
    1192
    48
                    return None;
    1193
                  }
    1194
    2.29k
                }
    1195
    185
              }
    1196
    2.47k
              let mathml = if element_name == "mmultiscripts" {clean_mmultiscripts185(mathml185).unwrap185()} else {mathml2.29k};
    1197
    2.47k
              if !is_chemistry_off(mathml) {
    1198
    2.47k
                let likely_chemistry = likely_adorned_chem_formula(mathml);
    1199
                // debug!("likely_chemistry={}, {}", likely_chemistry, mml_to_string(mathml));
    1200
    2.47k
                if likely_chemistry >= 0 {
    1201
    553
                  mathml.set_attribute_value(MAYBE_CHEMISTRY, likely_chemistry.to_string().as_str());
    1202
    1.92k
                }
    1203
    0
              }
    1204
    1205
    2.47k
              if element_name == "msubsup" {
    1206
    180
                return Some( clean_msubsup(mathml) );
    1207
              } else {
    1208
    2.29k
                return Some(mathml);
    1209
              }
    1210
    3.06k
            }
    1211
    1212
    13.2k
            mathml.replace_children(children);
    1213
            // debug!("clean_mathml: after loop\n{}", mml_to_string(mathml));
    1214
    13.2k
            if element_name == "mrow" || ELEMENTS_WITH_ONE_CHILD9.88k.contains9.88k(element_name9.88k) {
    1215
    10.1k
              clean_chemistry_mrow(mathml);
    1216
    10.1k
            }3.06k
    1217
    13.2k
            self.assure_nary_tag_has_one_child(mathml);
    1218
    13.2k
            if crate::xpath_functions::IsNode::is_2D(mathml) {
    1219
    4.48k
              CanonicalizeContext::mark_empty_content(mathml);
    1220
    8.77k
            }
    1221
    1222
    13.2k
            return Some(mathml);
    1223
          }
    1224
        }
    1225
    1226
        /// Returns substitute text if hyphen sequence should be a short or long dash
    1227
    11.8k
        fn canonicalize_dash(text: &str)  -> Option<&str> {
    1228
    11.8k
          if text == "--"  {
    1229
    1
            return Some("—"); // U+2014 (em dash)
    1230
    11.8k
          } else if text == "---" || text == "----"11.8k {    // use a regexp to catch a longer sequence?
    1231
    2
            return Some("―"); // U+2015 (Horizontal bar)
    1232
          } else {
    1233
    11.8k
            return None;
    1234
          }
    1235
    11.8k
        }
    1236
    1237
    11
        fn  set_annotation_attrs(new_presentation: Element, semantics: Element) {
    1238
    24
          for child in semantics11.children11() {
    1239
    24
            let child = as_element(child);
    1240
    24
            let child_name = name(child);
    1241
    24
            if child == new_presentation {
    1242
    1
              continue;
    1243
    23
            }
    1244
    23
            let attr_name = match child.attribute_value("encoding") {
    1245
    23
              Some(encoding_name) => format!("data-{}-{}", child_name, encoding_name.replace('/', "_slash_")),
    1246
    0
              None => format!("data-{child_name}"),    // probably shouldn't happen
    1247
            };
    1248
    23
            let attr_name = attr_name.as_str();
    1249
    23
            if child_name == "annotation" {
    1250
    12
              new_presentation.set_attribute_value(attr_name, as_text(child));
    1251
    12
            } else {
    1252
    11
              new_presentation.set_attribute_value(attr_name, &mml_to_string(child));
    1253
    11
            }
    1254
          }
    1255
    1256
    11
        }
    1257
    1258
        /// Hack to try and guess if a colon should be a ratio -- this affects parsing because of different precedences
    1259
        /// It also guesses on the spacing after the colon and adds a space attr if it looks like set building or function mapping notation.
    1260
        /// These conditions are really not well thought out and are just a first cut -- they do cause the braille tests to pass
    1261
        /// If 'intent' is given, it must be intent='ratio'
    1262
        /// 2. It must be infix and there is a proportion (∷) mo as a sibling, or
    1263
        /// 3. It is the only mo and has numbers on each side
    1264
        ///
    1265
        /// Need to rule out field extensions "[K:F]" and trilinear coordinates "a:b:c" (Nemeth doesn't consider these to be ratios)
    1266
    94
        fn is_ratio(mathml: Element) -> bool {
    1267
    94
          assert_eq!(name(mathml), "mo");
    1268
    94
          let parent = get_parent(mathml);  // must exist
    1269
    94
          if name(parent) != "mrow" && name(parent) != "math"81{
    1270
    0
            return false;
    1271
    94
          }
    1272
    1273
    94
          if let Some(intent_value1) = mathml.attribute_value(INTENT_ATTR)
    1274
    1
            && (intent_value != "ratio" || !intent_value.starts_with('_')0) {
    1275
    1
              return false;
    1276
    93
            }
    1277
    1278
    93
          if let Some(value0) = mathml.attribute_value("data-mjx-texclass")
    1279
    0
            && value ==  "PUNCT" {
    1280
    0
              mathml.remove_attribute("data-mjx-texclass");
    1281
    0
              mathml.set_attribute_value(SPACE_AFTER, "true");  // signal to at least Nemeth rules that this is punctuation
    1282
    93
            }
    1283
    1284
    93
          let preceding = mathml.preceding_siblings();
    1285
    93
          let following = mathml.following_siblings();
    1286
    93
          if preceding.is_empty() || following92.is_empty92() {
    1287
    2
            return false;
    1288
    91
          }
    1289
    91
          let preceding_child = as_element( preceding[preceding.len()-1] );
    1290
    91
          let following_child = as_element(following[0]);
    1291
    91
          if preceding.len() == 1 && name(preceding_child) == "mn"34 &&
    1292
    8
             following.len() == 1 && name(following_child) == "mn"2 {
    1293
    2
            return true;
    1294
    89
          }
    1295
          // only want one "∷"
    1296
    89
          let is_before = is_proportional_before_colon(preceding.iter().rev());
    1297
    89
          if let Some(is_before3) = is_before
    1298
    3
            && !is_before {
    1299
    0
              return false;
    1300
    89
            }
    1301
    89
          let is_before = is_before.is_some();   // move this to true/false (found/not found)
    1302
    89
          let is_after = is_proportional_before_colon(following.iter());
    1303
    89
          if let Some(is_after3) = is_after
    1304
    3
            && !is_after {
    1305
    0
              return false;
    1306
    89
            }
    1307
    89
          let is_after = is_after.is_some();   // move this to true/false (found/not found)
    1308
    89
          return is_before ^ is_after;
    1309
    1310
    178
          fn is_proportional_before_colon<'a>(siblings: impl Iterator<Item = &'a ChildOfElement<'a>>) -> Option<bool> {
    1311
            // unparsed, so we look at relative priorities to make sure the proportional operator is really the next operator
    1312
    3
            static PROPORTIONAL_PRIORITY: LazyLock<usize> = LazyLock::new(|| OPERATORS.get("∷").unwrap().priority);
    1313
    461
            for sibling in siblings178 {
    1314
    461
              let child = as_element(*sibling);
    1315
    461
              if name(child) == "mo" {
    1316
    203
                let text = as_text(child);
    1317
    203
                match text {
    1318
    203
                  "∷" | "::"198 => return Some(true)6,   // "::" might not be canonicalized yet
    1319
    197
                  "∶" => return Some(false)0,
    1320
                  _ => {
    1321
    197
                    if let Some(op191) = OPERATORS.get(text)
    1322
    191
                      && op.priority < *PROPORTIONAL_PRIORITY {
    1323
    109
                        return None;   // no "∷"
    1324
    88
                      }
    1325
                  },
    1326
                }
    1327
    258
              }
    1328
            }
    1329
    63
            return None;
    1330
    178
          }
    1331
    94
        }
    1332
    1333
    1334
        /// Returns true if it detects that this is likely coming from mhchem:
    1335
        /// v3: msub/msup/msubsup with mpadded width=0/mphantom/mi=A)
    1336
        /// v4: msub/msup/msubsup with mrow/mrow/mpadded width=0/mphantom/mi=A)
    1337
        /// This should be called with 'mrow' being the outer mrow
    1338
    3.15k
        fn is_from_mhchem_hack(mathml: Element) -> bool {
    1339
    3.15k
          assert!(name(mathml) == "mrow" || name(mathml) == "mpadded"588);
    1340
    3.15k
          assert_eq!(mathml.children().len(), 1);
    1341
    3.15k
          let parent = get_parent(mathml);
    1342
    3.15k
          let parent_name = name(parent);
    1343
    3.15k
          if !(parent_name == "msub" || parent_name == "msup"2.99k || parent_name == "msubsup"2.80k) {
    1344
    2.56k
            return false;
    1345
    594
          }
    1346
    1347
    594
          let mpadded315 = if name(mathml) == "mrow" {
    1348
    545
            let mrow = as_element(mathml.children()[0]);
    1349
    545
            if !(name(mrow) == "mrow" && mrow.children().len() == 1347) {
    1350
    255
              return false;
    1351
    290
            }
    1352
    290
            let child = as_element(mrow.children()[0]);
    1353
    290
            if name(child) != "mpadded" {
    1354
    24
              return false;
    1355
    266
            }
    1356
    266
            child
    1357
          } else {
    1358
    49
            mathml
    1359
          };
    1360
    315
          if let Some(width169) = mpadded.attribute_value("width") {
    1361
    169
            if width != "0" {
    1362
    0
              return false;
    1363
    169
            }
    1364
          } else {
    1365
    146
            return false;
    1366
          }
    1367
    1368
    169
          let mphantom = as_element(mpadded.children()[0]);
    1369
    169
          if !(name(mphantom) == "mphantom" && mphantom.children().len() == 1) {
    1370
    0
            return false;
    1371
    169
          }
    1372
    1373
    169
          let child = as_element(mphantom.children()[0]);
    1374
    169
          return name(child) == "mi" && as_text(child) == "A";
    1375
    3.15k
        }
    1376
    1377
        /// 'text' is potentially one of the many Unicode whitespace chars. Estimate the width in ems
    1378
    149
        fn white_space_em_width(text: &str) -> f64 {
    1379
    149
          assert!(IS_WHITESPACE.is_match(text));
    1380
    149
          let mut width = 0.0;
    1381
    163
          for ch in text149.chars149() {
    1382
    163
            width += match ch {
    1383
    137
              ' ' | '\u{00A0}' | '\u{1680}' | ' ' => 0.7, // space, non-breaking space, Ogham space mark, figure space
    1384
    0
              ' ' | ' ' => 0.5,           // en quad, en space
    1385
    0
              ' ' | ' ' => 1.0,           // em quad, em space
    1386
    0
              ' ' => 1.0/3.0,             // three per em space
    1387
    0
              ' ' | ' ' => 0.25,           // four per em space, punctuation space (wild guess)
    1388
    22
              ' ' | ' ' => 3.0/18.0,         // six per em space, thin space
    1389
    0
              ' ' => 1.0/18.0,           // hair space
    1390
    0
              ' ' => 0.3,               // narrow no-break space (half a regular space?)
    1391
    4
              ' ' => 4.0/18.0,           // medium math space
    1392
    0
              '　' => 1.5,             // Ideographic Space
    1393
    0
              _ => 0.7,               // shouldn't happen
    1394
            }
    1395
          }
    1396
    149
          return width;
    1397
    149
        }
    1398
    1399
        /// Splits the leaf element into chemical elements if needed
    1400
    12.5k
        fn clean_chemistry_leaf(mathml: Element) -> Element {
    1401
    12.5k
          if !(is_chemistry_off(mathml) || mathml.attribute(MAYBE_CHEMISTRY).is_some()) {
    1402
    12.3k
            assert!(name(mathml)=="mi" || name(mathml)=="mtext"942);
    1403
            // this is a hack -- VII is more likely to be roman numeral than the molecule V I I so prevent that from happening
    1404
            // FIX: come up with a less hacky way to prevent chem element misinterpretation
    1405
    12.3k
            let text = as_text(mathml);
    1406
    12.3k
            if text.len() > 2 && is_roman_number_match3.09k(text3.09k) {
    1407
    0
              return mathml;
    1408
    12.3k
            }
    1409
    12.3k
            if let Some(elements135) = convert_leaves_to_chem_elements(mathml) {
    1410
              // children are already marked as chemical elements
    1411
    135
              let answer = replace_children(mathml, elements);
    1412
    135
              if name(answer) == "mrow" {
    1413
    29
                answer.remove_attribute(MAYBE_CHEMISTRY);   // was lifted, and not set -- remove and it will be computed later
    1414
    106
              }
    1415
    135
              return answer;
    1416
            } else {
    1417
    12.1k
              let likely_chemistry = likely_chem_element(mathml);
    1418
    12.1k
              if likely_chemistry >= 0 {
    1419
    2.59k
                mathml.set_attribute_value(MAYBE_CHEMISTRY, likely_chemistry.to_string().as_str());
    1420
    9.57k
              }
    1421
            };
    1422
    259
          }
    1423
    12.4k
          return mathml;
    1424
    12.5k
        }
    1425
    1426
    1427
        /// looks for pairs of (letter, pseudo-script) such as x' or p'q' all inside of a single token element
    1428
    11.4k
        fn split_apart_pseudo_scripts<'a>(mi: Element<'a>) -> Option<Element<'a>> {
    1429
    2
          static IS_DEGREES_C_OR_F: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[°º][CF]").unwrap());
    1430
    1431
    11.4k
          let text = as_text(mi);
    1432
          // debug!("split_apart_pseudo_scripts: start text=\"{text}\"");
    1433
    11.4k
          if !text.chars().any(is_pseudo_script_char) || IS_DEGREES_C_OR_F14.is_match(text) {
    1434
    11.4k
            return None;
    1435
    1
          }
    1436
    1437
    1
          let document = mi.document();
    1438
          // create pairs of text
    1439
    1
          let chars = text.chars();
    1440
    1
            let next_chars = text.chars().skip(1);
    1441
    1
          let result = chars.zip(next_chars).map(|(a, b)|
    1442
    1
                if a.is_alphabetic() && is_pseudo_script_char(b) {
    1443
                  // create msup
    1444
    1
                  let base = create_mathml_element(&document, "mi");
    1445
    1
                  base.set_text(&a.to_string());
    1446
    1
                  let script = create_mathml_element(&document, "mo");
    1447
    1
                  script.set_text(&b.to_string());
    1448
    1
                  let msup = create_mathml_element(&document, "msup");
    1449
    1
                  msup.append_child(base);
    1450
    1
                  msup.append_child(script);
    1451
    1
                  msup
    1452
                } else {
    1453
                  // create an mi "ab"
    1454
    0
                  let new_mi = create_mathml_element(&document, "mi");
    1455
    0
                  let mut new_mi_text = String::with_capacity(6);    // likely will fit almost all cases
    1456
    0
                  new_mi_text.push(a);
    1457
    0
                  new_mi_text.push(b);
    1458
    0
                  new_mi.set_text(&new_mi_text);
    1459
    0
                  new_mi
    1460
    1
                } )
    1461
    1
            .collect::<Vec<Element>>();
    1462
    1
          if result.len() == 1 {
    1463
    1
            return Some( result[0] );
    1464
          } else {
    1465
    0
            return Some( replace_children(mi, result) );
    1466
          }
    1467
    11.4k
        }
    1468
    1469
    1470
        /// If 'mathml' is a scripted element and has an mrow for a base,
    1471
        ///   attach any prescripts to the first element in mrow
    1472
        ///   attach any postscript to the last element in mrow
    1473
        /// Return the modified element (which might now be an mrow)
    1474
    31.0k
        fn attach_scripts_to_split_element(mathml: Element) -> Element {
    1475
    31.0k
          if !IsNode::is_scripted(mathml) {
    1476
    28.5k
            return mathml;
    1477
    2.48k
          }
    1478
    2.48k
          let base = as_element(mathml.children()[0]);
    1479
    2.48k
          if name(base) != "mrow" {
    1480
    2.30k
            return mathml;
    1481
    185
          }
    1482
    185
          let base_children = base.children();
    1483
    185
          let i_last_base = base_children.len()-1;
    1484
    185
          let last_child = as_element(base_children[i_last_base]);
    1485
    185
          if last_child.attribute(SPLIT_TOKEN).is_none() {
    1486
    156
            return mathml;
    1487
    29
          }
    1488
          // debug!("attach_scripts_to_split_element -- start: \n{}", mml_to_string(mathml));
    1489
    29
          let mut mathml_replacement = Vec::with_capacity(base_children.len());
    1490
    29
          if name(mathml) == "mmultiscripts" {
    1491
            // pull any prescript (should be at most one prefix pair) into the first child
    1492
    1
            let multiscripts_children = mathml.children();
    1493
    1
            let n_multiscripts_children = multiscripts_children.len();
    1494
    1
            let potential_mprescripts_element = as_element(multiscripts_children[n_multiscripts_children-3]);
    1495
    1
            if name(potential_mprescripts_element) == "mprescripts" {    // we have potential chem prescripts
    1496
              // create a new mmultiscripts elements with first child as its base mathml's prescripts as the new element's prescripts
    1497
    1
              let mut new_mmultiscripts_children = Vec::with_capacity(4);
    1498
    1
              new_mmultiscripts_children.push(base_children[0]);
    1499
    1
              base.remove_child(as_element(base_children[0]));
    1500
    1
              new_mmultiscripts_children.push(multiscripts_children[n_multiscripts_children-3]);
    1501
    1
              new_mmultiscripts_children.push(multiscripts_children[n_multiscripts_children-2]);
    1502
    1
              new_mmultiscripts_children.push(multiscripts_children[n_multiscripts_children-1]);
    1503
    1504
    1
              let new_mmultiscripts = create_mathml_element(&base.document(), "mmultiscripts");
    1505
    1
              new_mmultiscripts.append_children(new_mmultiscripts_children);
    1506
    1
              let likely = likely_adorned_chem_formula(new_mmultiscripts);
    1507
    1
              new_mmultiscripts.set_attribute_value(MAYBE_CHEMISTRY, &likely.to_string());
    1508
              // debug!("attach_scripts_to_split_element -- new_mmultiscripts: \n{}", mml_to_string(new_mmultiscripts));
    1509
    1
              if n_multiscripts_children == 4 {
    1510
                // we stripped all the children so only the (modified) base exists
    1511
                // create mrow(new_mmultiscripts, mathml[0])
    1512
    0
                let children = vec![new_mmultiscripts, base];
    1513
    0
                return replace_children(mathml, children);
    1514
    1
              }
    1515
    1
              mathml_replacement.push(new_mmultiscripts);
    1516
    0
            }
    1517
    28
          }
    1518
    1519
          // Add all the middle children of the base to the mrow
    1520
    34
          base.children().iter()29.take29(base.children().len()-129).for_each29(|&child|  mathml_replacement.push(as_element(child)));
    1521
    1522
          // create a new script element with last child as its base
    1523
    29
          let mut new_mathml_children = mathml.children();
    1524
    29
          new_mathml_children[0] = ChildOfElement::Element(base);
    1525
    29
          mathml.replace_children(new_mathml_children);
    1526
    29
          mathml_replacement.push(mathml);
    1527
          // debug!("attach_scripts_to_split_element -- after adjusting ({} replacement children): \n{}", mathml_replacement.len(), mml_to_string(mathml));
    1528
    29
          return replace_children(mathml, mathml_replacement);
    1529
    31.0k
        }
    1530
    1531
        /// makes sure the structure is correct and also eliminates <none/> pairs
    1532
        /// MathML core changed <none/> to <mrow/>. For now (since MathCAT has lots of "none" tests), <mrow/> => <mtext> => <none/>
    1533
        /// (used https://chem.libretexts.org/Courses/Saint_Francis_University/CHEM_113%3A_Human_Chemistry_I_(Muino)/13%3A_Nuclear_Chemistry12/13.04%3A_Nuclear_Decay)
    1534
        ///
    1535
        /// This does some dubious repairs when the structure is bad, but not sure what else to do
    1536
    185
        fn clean_mmultiscripts(mathml: Element) -> Option<Element> {
    1537
    185
          let mut mathml = mathml;
    1538
    185
          let children = mathml.children();
    1539
    185
          let n = children.len();
    1540
    185
          let i_mprescripts =
    1541
    185
            if let Some((i108,_)) = children.iter().enumerate()
    1542
    659
              .find185(|&(_,&el)| name(as_element(el)) == "mprescripts") { i108 } else { n77 };
    1543
    185
          let has_misplaced_mprescripts = i_mprescripts & 1 == 0;  // should be first, third, ... child
    1544
    185
          let mut has_proper_number_of_children = if i_mprescripts == n { n & 1 == 077} else { n & 1 != 0108 }; // should be odd else even #
    1545
    185
          if has_misplaced_mprescripts || !has_proper_number_of_children || has_none_none_script_pair0(&children0) {
    1546
            // need to reset the children
    1547
    185
            let mut new_children = Vec::with_capacity(n+2); // adjusting position of mprescripts might add two children
    1548
    185
            new_children.push(children[0]);
    1549
            // drop none, none script pairs
    1550
    185
            let mut i = 1;
    1551
    604
            while i < n {
    1552
    419
              let child = as_element(children[i]);
    1553
    419
              let child_name = name(child);
    1554
    419
              if child_name == "mprescripts" {
    1555
    108
                if has_misplaced_mprescripts {
    1556
    0
                  let mtext = CanonicalizeContext::create_empty_element(&mathml.document());
    1557
    0
                  new_children.push(ChildOfElement::Element(mtext));
    1558
    0
                  has_proper_number_of_children = !has_proper_number_of_children;
    1559
    108
                }
    1560
    108
                new_children.push(children[i]);
    1561
    108
                i += 1;
    1562
    311
              } else if i+1 < n && child_name == "none" && name85(as_element85(children[i+1])) == "none" {
    1563
    2
                i += 2;   // found none, none pair
    1564
    309
              } else {
    1565
    309
                // copy pair
    1566
    309
                new_children.push(children[i]);
    1567
    309
                new_children.push(children[i+1]);
    1568
    309
                i += 2;
    1569
    309
              }
    1570
            }
    1571
    185
            if new_children.len() <= 2 {  // base only, or base and </mprescripts>
    1572
    1
              mathml = as_element(new_children[0]);
    1573
    184
            } else {
    1574
    184
              mathml.replace_children(new_children);
    1575
    184
            }
    1576
    0
          }
    1577
    1578
    185
          return Some(mathml);
    1579
    1580
    0
          fn has_none_none_script_pair(children: &[ChildOfElement]) -> bool {
    1581
    0
            let mut i = 1;
    1582
    0
            let n = children.len();
    1583
    0
            while i < n {
    1584
    0
              let child = as_element(children[i]);
    1585
    0
              let child_name = name(child);
    1586
    0
              if child_name == "mprescripts" {
    1587
    0
                i += 1;
    1588
    0
              } else if i+1 < n && child_name == "none" && name(as_element(children[i+1])) == "none" {
    1589
    0
                return true;   // found none, none pair
    1590
    0
              } else {
    1591
    0
                i += 2;
    1592
    0
              }
    1593
            }
    1594
    0
            return false;
    1595
    0
          }
    1596
    185
        }
    1597
    1598
        /// converts element if there is an empty subscript or superscript
    1599
    180
        fn clean_msubsup(mathml: Element) -> Element {
    1600
    180
          let children = mathml.children();
    1601
    180
          let subscript = as_element(children[1]);
    1602
    180
          let has_subscript = !(name(subscript) == "mtext" && as_text(subscript).trim()3.is_empty3());
    1603
    180
          let superscript = as_element(children[2]);
    1604
    180
          let has_superscript = !(name(superscript) == "mtext" && as_text(superscript).trim()6.is_empty6());
    1605
    180
          if has_subscript && has_superscript177 {
    1606
    171
            return mathml;
    1607
    9
          } else if has_subscript {
    1608
    6
            set_mathml_name(mathml, "msub");
    1609
    6
            let children = vec!(children[0], children[1]);
    1610
    6
            mathml.replace_children(children);
    1611
    6
            return mathml;
    1612
    3
          } else if has_superscript {
    1613
    3
            set_mathml_name(mathml, "msup");
    1614
    3
            let children = vec!(children[0], children[2]);
    1615
    3
            mathml.replace_children(children);
    1616
    3
            return mathml;
    1617
          } else {
    1618
    0
            return as_element(children[0]);  // no scripts
    1619
          }
    1620
    180
        }
    1621
    1622
        /// Split off the currency symbol from the rest of the text and return an mrow with the result
    1623
        /// Assumes it has already checked and that we have a leaf
    1624
    12
        fn split_currency_symbol(leaf: Element) -> Option<Element> {
    1625
    12
          assert!(is_leaf(leaf));
    1626
    12
          let text = as_text(leaf);
    1627
    12
          assert!(contains_currency(text));
    1628
    12
          let mut iter = text.chars();
    1629
    12
          match (iter.next(), iter.next()) {
    1630
    0
            (None, _) => return None,
    1631
            (Some(_), None) => {  // 1 char
    1632
    9
              leaf.set_name("mi");
    1633
    9
              return Some(leaf);       }
    1634
            (Some(_), Some(_)) => { // 2 or more chars
    1635
              // WARNING: don't use 'leaf' in the mrow -- that detaches it from its parent and could shrink the number of children causing problems
    1636
    4
              if text.chars()3.any3(|c| c.is_ascii_digit()) {   // might be a number with a currency symbol
    1637
    3
                leaf.set_name("mn");  // make sure we create an mn (might be one already)
    1638
    3
              }0
    1639
    3
              let first_ch = text.char_indices().next().map(|(i, ch)| &text[i..i + ch.len_utf8()]).unwrap();
    1640
    3
              if is_currency_symbol(first_ch.chars().next().unwrap()) {
    1641
    1
                let mrow = create_mathml_element(&leaf.document(), "mrow");
    1642
    1
                mrow.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    1643
    1
                let currency_symbol = create_mathml_element(&leaf.document(), "mi");
    1644
    1
                currency_symbol.set_text(first_ch);
    1645
    1
                mrow.append_child(currency_symbol);
    1646
    1
                let implied_times = create_mo(leaf.document(), "\u{2062}", ADDED_ATTR_VALUE);
    1647
    1
                mrow.append_child(implied_times);
    1648
    1
                let currency_amount = create_mathml_element(&leaf.document(), name(leaf));
    1649
    1
                currency_amount.set_text(&text[first_ch.len()..]);
    1650
    1
                mrow.append_child(currency_amount);
    1651
    1
                return Some(mrow);
    1652
    2
              }
    1653
    2
              let last_ch = text.char_indices().last().map(|(i, _)| &text[i..]).unwrap();
    1654
    2
              if is_currency_symbol(last_ch.chars().next().unwrap()) {
    1655
    1
                let mrow = create_mathml_element(&leaf.document(), "mrow");
    1656
    1
                mrow.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    1657
    1
                let implied_times = create_mo(leaf.document(), "\u{2062}", ADDED_ATTR_VALUE);
    1658
    1
                mrow.append_child(implied_times);
    1659
    1
                let currency_amount = create_mathml_element(&leaf.document(), name(leaf));
    1660
    1
                currency_amount.set_text(&text[..text.len()-last_ch.len()]);
    1661
    1
                mrow.append_child(currency_amount);
    1662
    1
                let currency_symbol = create_mathml_element(&leaf.document(), "mi");
    1663
    1
                currency_symbol.set_text(last_ch);
    1664
    1
                mrow.append_child(currency_symbol);
    1665
    1
                return Some(mrow);
    1666
    1
              }
    1667
              // try to find it in the middle
    1668
    2
              for (byte_idx, ch) in text1.char_indices1() {
    1669
    2
                if contains_currency(&text[byte_idx .. byte_idx + ch.len_utf8()]) {
    1670
                  // get all the substrings
    1671
    1
                  let first_part = &text[..byte_idx];
    1672
    1
                  let currency_symbol = &text[byte_idx .. byte_idx + ch.len_utf8()];
    1673
    1
                  let second_part = &text[byte_idx + ch.len_utf8() ..];
    1674
    1
                  let mrow = create_mathml_element(&leaf.document(), "mrow");
    1675
    1
                  mrow.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    1676
    1
                  let first_part_element = create_mathml_element(&leaf.document(), name(leaf));
    1677
    1
                  first_part_element.set_text(first_part);
    1678
    1
                  mrow.append_child(first_part_element);
    1679
    1
                  let implied_times = create_mo(leaf.document(), "\u{2062}", ADDED_ATTR_VALUE);
    1680
    1
                  mrow.append_child(implied_times);
    1681
    1
                  let currency_symbol_element = create_mathml_element(&leaf.document(), "mi");
    1682
    1
                  currency_symbol_element.set_text(currency_symbol);
    1683
    1
                  mrow.append_child(currency_symbol_element);
    1684
    1
                  let implied_times = create_mo(leaf.document(), "\u{2062}", ADDED_ATTR_VALUE);
    1685
    1
                  mrow.append_child(implied_times);
    1686
    1
                  let second_part_element = create_mathml_element(&leaf.document(), name(leaf));
    1687
    1
                  second_part_element.set_text(second_part);
    1688
    1
                  mrow.append_child(second_part_element);
    1689
    1
                  return Some(mrow);
    1690
    1
                }
    1691
              }
    1692
    0
              return None
    1693
            }
    1694
          }
    1695
    12
        }
    1696
    1697
        /// If arg is "arc" (with optional space), merge the following element in if a trig function (sibling is deleted)
    1698
    11.8k
        fn merge_arc_trig(leaf: Element) -> Option<Element> {
    1699
    11.8k
          assert!(is_leaf(leaf));
    1700
    11.8k
          let leaf_text = as_text(leaf);
    1701
    11.8k
          if !(leaf_text == "arc" || leaf_text == "arc "11.8k || leaf_text == "arc "11.8k /* non-breaking space */ ) {
    1702
    11.8k
            return None;
    1703
    2
          }
    1704
    1705
    2
          let following_siblings = leaf.following_siblings();
    1706
    2
          if following_siblings.is_empty() {
    1707
    0
            return None;
    1708
    2
          }
    1709
    1710
    2
          let following_sibling = as_element(following_siblings[0]);
    1711
    2
          let following_sibling_name = name(following_sibling);
    1712
    2
          if !(following_sibling_name == "mi" || following_sibling_name == "mo"0 || following_sibling_name == "mtext"0) {
    1713
    0
            return None;
    1714
    2
          }
    1715
    1716
    2
          return crate::definitions::SPEECH_DEFINITIONS.with(|definitions| {
    1717
            // change "arc" "cos" to "arccos" -- we look forward because calling loop stores previous node
    1718
    2
            let following_text = as_text(following_sibling);
    1719
    2
            if definitions.borrow().get_hashset("TrigFunctionNames").unwrap().contains(following_text) {
    1720
    2
              let new_text = "arc".to_string() + following_text;
    1721
    2
              set_mathml_name(leaf, "mi");
    1722
    2
              leaf.set_text(&new_text);
    1723
    2
              following_sibling.remove_from_parent();
    1724
    2
              return Some(leaf);
    1725
    0
            }
    1726
    0
            return None;
    1727
    2
          })
    1728
    11.8k
        }
    1729
    1730
        /// Convert "||" to "‖", if in single element or in repeated 'mo's (but not "|x||y|" or "{x ||x|>0}")
    1731
    305
        fn merge_vertical_bars(leaf: Element) -> Option<Element> {
    1732
    305
          assert!(is_leaf(leaf));
    1733
    305
          let leaf_text = as_text(leaf);
    1734
    305
          if leaf_text == "||" {
    1735
    4
            leaf.set_text("‖");    // U+2016
    1736
    4
            return Some(leaf);
    1737
    301
          } else if leaf_text != "|" {
    1738
    0
            return None;
    1739
    301
          }
    1740
    301
          let following_siblings = leaf.following_siblings();
    1741
    301
          if following_siblings.is_empty() {
    1742
    96
            return None;
    1743
    205
          }
    1744
    1745
    205
          let following_sibling = as_element(following_siblings[0]);
    1746
    205
          if name(following_sibling) != "mo" || as_text(following_sibling) != "|"18 {
    1747
    201
            return None
    1748
    4
          }
    1749
    1750
          // have "||" -- if there a single "|" on left, rule out merge
    1751
    4
          let preceding_siblings = leaf.preceding_siblings();
    1752
    5
          if preceding_siblings.iter()4.any4(|&child| {
    1753
    5
            let child = as_element(child);
    1754
    5
            return name(child) == "mo" && as_text(child) == "|"3;
    1755
    5
          }) {
    1756
    1
            return None;   // found "|" on left
    1757
    3
          }
    1758
    1759
    3
          if following_siblings.len() > 1 {
    1760
    2
            let following_siblings = &following_siblings[1..];
    1761
            // if there are an odd number of "|"s to the right, rule out the merge
    1762
    8
            if !(following_siblings2.iter2().filter2(|&&child| {
    1763
    8
              let child = as_element(child);
    1764
    8
              return name(child) == "mo" && as_text(child) == "|"5;
    1765
    8
            }).count2()).is_multiple_of2(2) {
    1766
    1
              return None;
    1767
    1
            }
    1768
    1
          }
    1769
    1770
          // didn't find any
    1771
    2
          leaf.set_text("‖");    // U+2016
    1772
    2
          following_sibling.remove_from_parent();
    1773
    2
          return Some(leaf);
    1774
    305
        }
    1775
    1776
        /// merge a following mstyle that has the same attrs
    1777
    714
        fn merge_adjacent_similar_mstyles(mathml: Element) {
    1778
    714
          if ELEMENTS_WITH_FIXED_NUMBER_OF_CHILDREN.contains(name(get_parent(mathml))) {
    1779
            // FIX: look to see if all of the children (might be more than just the adjacent one) have the same attr and then pull them up to the parent
    1780
    65
            return;   // can't remove subsequent child
    1781
    649
          }
    1782
    649
          let following_siblings = mathml.following_siblings();
    1783
    649
          if following_siblings.is_empty() {
    1784
    579
            return;
    1785
    70
          }
    1786
    70
          let following_element = as_element(following_siblings[0]);
    1787
    70
          if name(following_element) != "mstyle" {
    1788
    66
            return;
    1789
    4
          }
    1790
    4
          let are_same = mathml.attributes().iter()
    1791
    4
                  .zip( following_element.attributes() )
    1792
    5
                  .all4(|(first, second)| first.name()==second.name() && first.value()==second.value());
    1793
    4
          if are_same {
    1794
    4
            mathml.append_children(following_element.children());
    1795
    4
            following_element.remove_from_parent();
    1796
    4
          }0
    1797
    714
        }
    1798
    1799
    40
        fn convert_mfenced_to_mrow(mfenced: Element) -> Element {
    1800
          // The '<'/'>' replacements are because WIRIS uses them out instead of the correct chars in its template
    1801
    40
          let open = mfenced.attribute_value("open").unwrap_or("(").replace('<', "⟨");
    1802
    40
          let close = mfenced.attribute_value("close").unwrap_or(")").replace('>', "⟩");
    1803
          // debug!("open={}, close={}", open, close);
    1804
    40
          let mut separators= mfenced.attribute_value("separators").unwrap_or(",").chars();
    1805
    40
          set_mathml_name(mfenced, "mrow");
    1806
    40
          mfenced.remove_attribute("open");
    1807
    40
          mfenced.remove_attribute("close");
    1808
    40
          mfenced.remove_attribute("separators");
    1809
    40
          let children = mfenced.children();
    1810
    40
          let mut new_children = Vec::with_capacity(2*children.len() + 1);
    1811
    40
          if !open.is_empty() {
    1812
    40
            new_children.push(ChildOfElement::Element( create_mo(mfenced.document(), &open, MFENCED_ATTR_VALUE)) );
    1813
    40
          }0
    1814
    40
          if !children.is_empty() {
    1815
    40
            new_children.push(children[0]);
    1816
    40
            for child3 in &children[1..] {
    1817
    3
              let sep = separators.next().unwrap_or(',').to_string();
    1818
    3
              new_children.push( ChildOfElement::Element( create_mo(mfenced.document(), &sep, MFENCED_ATTR_VALUE)) );
    1819
    3
              new_children.push(*child);
    1820
    3
            }
    1821
    0
          }
    1822
    40
          if !close.is_empty() {
    1823
    38
            new_children.push(ChildOfElement::Element( create_mo(mfenced.document(), &close, MFENCED_ATTR_VALUE)) );
    1824
    38
          }2
    1825
    40
          mfenced.replace_children(new_children);
    1826
    40
          return mfenced;
    1827
    40
        }
    1828
    1829
    30.4k
        fn is_roman_number_match(text: &str) -> bool {
    1830
    30.4k
          return UPPER_ROMAN_NUMERAL.is_match(text) || LOWER_ROMAN_NUMERAL29.6k.is_match(text);
    1831
    30.4k
        }
    1832
    1833
        /// Return true if 'element' (which is syntactically a roman numeral) is only inside mrows and
    1834
        ///  if its length is < 3 chars, then there is another roman numeral near it (separated by an operator).
    1835
        /// We want to rule out something like 'm' or 'cm' being a roman numeral.
    1836
        /// Note: this function assumes 'mathml' is a Roman Numeral, and optimizes operations based on that.
    1837
        /// Note: Nemeth has some rules about roman numerals (capitalization and punctuation after)
    1838
    3.35k
        fn is_roman_numeral_number_context(mathml: Element) -> bool {
    1839
    3.35k
          assert!(name(mathml)=="mtext" || name(mathml)=="mi"3.32k);
    1840
    3.35k
          let mut parent = mathml;
    1841
          loop {
    1842
    5.41k
            parent = get_parent(parent);
    1843
    5.41k
            let current_name = name(parent);
    1844
    5.41k
            if current_name == "math" {
    1845
    1.57k
              break;
    1846
    3.84k
            } else if current_name == "msup" || current_name == "mmultiscripts"3.42k {
    1847
              // could be a oxidation state in a Chemical formula
    1848
    559
              let children = parent.children();
    1849
              // make sure that there is only one script and that 'mathml' is a superscript
    1850
    559
              if current_name == "mmultiscripts" && (children.len() > 3139  || !mathml.following_siblings().is_empty()27) {
    1851
    122
                return false;
    1852
    437
              }
    1853
    437
              let base = as_element(children[0]);
    1854
    437
              if is_chemical_element(base) {
    1855
    21
                break;
    1856
              } else {
    1857
    416
                return false;
    1858
              }
    1859
    3.28k
            } else if current_name != "mrow" {
    1860
    1.22k
              return false;
    1861
    2.06k
            }
    1862
          }
    1863
    1864
    1.59k
          let text = as_text(mathml).as_bytes(); // note: we know it is all ASCII chars
    1865
          // if roman numeral is in superscript and we get here, then it had a chemical element base, so we accept it
    1866
          // note: you never has a state = I; if two letters, it must be 'II'.
    1867
    1.59k
          if text.len() > 2  ||
    1868
    1.57k
             ((name(parent) =="msup" || name(parent) == "mmultiscripts"1.57k) && text.len()==212 && text==[b'I',b'I']8) {
    1869
    28
            return true;
    1870
          } else {
    1871
    1.56k
            let is_upper_case = text[0].is_ascii_uppercase(); // safe since we know it is a roman numeral
    1872
    1.56k
            let preceding = mathml.preceding_siblings();
    1873
    1.56k
            let following = mathml.following_siblings();
    1874
    1.56k
            if preceding.is_empty() && following356.is_empty356() {
    1875
    81
              return false;   // no context and too short to confirm it is a roman numeral
    1876
    1.48k
            }
    1877
    1.48k
            if preceding.is_empty() {
    1878
    275
              return is_roman_numeral_adjacent(following.iter(), is_upper_case);
    1879
    1.21k
            }
    1880
    1.21k
            if following.is_empty() {
    1881
    399
              return is_roman_numeral_adjacent(preceding.iter().rev(), is_upper_case);
    1882
    813
            }
    1883
    813
            return is_roman_numeral_adjacent(preceding.iter().rev(), is_upper_case) &&
    1884
    3
                 is_roman_numeral_adjacent(following.iter(), is_upper_case);
    1885
          }
    1886
    1887
          /// make sure all the non-mo leaf siblings are roman numerals
    1888
          /// 'mo' should only be '+', '-', '=', ',', '.'  -- unlikely someone is doing anything sophisticated
    1889
    1.49k
          fn is_roman_numeral_adjacent<'a, I>(siblings: I, must_be_upper_case: bool) -> bool
    1890
    1.49k
              where I: Iterator<Item = &'a ChildOfElement<'a>> {
    1891
            static ROMAN_NUMERAL_OPERATORS: phf::Set<&str> = phf_set! {
    1892
              "+", "-'", "=", "<", "≤", ">", "≥",
    1893
              // ",", ".",   // [c,d] triggers this if "," is present, so omitting it
    1894
            };
    1895
    1.49k
            let mut found_match = false;       // guard against no siblings
    1896
    1.49k
            let mut last_was_roman_numeral = true; // started at roman numeral
    1897
            // debug!("start is_roman_numeral_adjacent");
    1898
    1.74k
            for child in siblings1.49k {
    1899
    1.74k
              let maybe_roman_numeral = as_element(*child);
    1900
              // debug!("maybe_roman_numeral: {}", mml_to_string(maybe_roman_numeral));
    1901
    1.74k
              match name(maybe_roman_numeral) {
    1902
    1.74k
                "mo" => {
    1903
    858
                  if !last_was_roman_numeral {
    1904
    18
                    return false;
    1905
    840
                  }
    1906
    840
                  let text = as_text(maybe_roman_numeral);
    1907
    840
                  if !ROMAN_NUMERAL_OPERATORS.contains(text) {
    1908
    660
                    return false;
    1909
    180
                  }
    1910
    180
                  last_was_roman_numeral = false;
    1911
                },
    1912
    889
                "mi" | "mn"585 => {
    1913
    562
                  if last_was_roman_numeral {
    1914
    429
                    return false;   // no implicit multiplication (or whatever)
    1915
    133
                  }
    1916
    133
                  let text = as_text(maybe_roman_numeral);
    1917
    133
                  if !(( must_be_upper_case && UPPER_ROMAN_NUMERAL18.is_match(text)) ||
    1918
    117
                     (!must_be_upper_case && LOWER_ROMAN_NUMERAL115.is_match(text)) ) {
    1919
    109
                    return false;
    1920
    24
                  };
    1921
    24
                  found_match = true;
    1922
    24
                  last_was_roman_numeral = true;
    1923
                },
    1924
    327
                "mtext" | "mspace"252 | "mphantom"252 => {}75,
    1925
                _ => {
    1926
    252
                  return false;
    1927
                }
    1928
              }
    1929
            }
    1930
    22
            return found_match;
    1931
    1.49k
          }
    1932
    3.35k
        }
    1933
    1934
        /// Merge adjacent mtext by increasing the width of the first mtext
    1935
        /// The resulting merged whitespace is put on the previous child, or if there is one, on the following child
    1936
        ///
    1937
        /// Note: this should be called *after* the mo/mtext cleanup (i.e., after the MathML child cleanup loop).
    1938
    10.1k
        fn merge_whitespace(children: &mut Vec<ChildOfElement>) {
    1939
    10.1k
          if children.is_empty() {
    1940
    3
            return;
    1941
    10.1k
          }
    1942
    1943
    10.1k
          let mut i = 0;
    1944
    10.1k
          let mut previous_mtext_with_width: Option<Element<'_>> = None;  // prefer to spacing on previous mtext
    1945
    10.1k
          let mut whitespace: Option<f64> = None;
    1946
    42.0k
          while i < children.len() {
    1947
    31.8k
            let child = as_element(children[i]);
    1948
    31.8k
            let is_child_whitespace = name(child) == "mtext" && as_text(child) == "\u{00A0}"555;
    1949
            // debug!("merge_whitespace: i={}, whitespace={:?}, mtext set={} {}",
    1950
            //    i, whitespace, previous_mtext_with_width.is_some(), mml_to_string(child));
    1951
    31.8k
            if is_child_whitespace {
    1952
              // update the running total of whitespace
    1953
    340
              let child_width = child.attribute_value("data-width").unwrap_or("0")
    1954
    340
                                              .parse::<f64>().unwrap_or(0.0) ;
    1955
    340
              whitespace = match whitespace {
    1956
    327
                None => Some(child_width),
    1957
    13
                Some(w) => Some(w + child_width),
    1958
              };
    1959
    340
              if children.len() == 1 {
    1960
    15
                i += 1;             // don't remove only child
    1961
    325
              } else {
    1962
    325
                children.remove(i);   // remove the current child (don't inc 'i')
    1963
    325
              }
    1964
    31.5k
            } else if let Some(ws305) = whitespace {
    1965
              // done with sequence of whitespaces
    1966
    305
              if let Some(prev_mtext13) = previous_mtext_with_width {
    1967
    13
                // prefer to set on previous mtext
    1968
    13
                prev_mtext.set_attribute_value("data-following-space-width", (ws).to_string().as_str());
    1969
    13
                previous_mtext_with_width = None;
    1970
    13
              } else {
    1971
                // if the space is significant, set it on the current child
    1972
    292
                child.set_attribute_value("data-previous-space-width", ws.to_string().as_str());
    1973
    292
                if name(child) == "mtext" {
    1974
    18
                  previous_mtext_with_width = Some(child);
    1975
    274
                }
    1976
              }
    1977
    305
              whitespace = None;
    1978
    305
              i += 1;
    1979
    31.2k
            } else {
    1980
    31.2k
              i += 1;
    1981
    31.2k
              previous_mtext_with_width = None;
    1982
    31.2k
            }
    1983
          }
    1984
          // debug!("  after loop: whitespace={:?}, {}", whitespace, mml_to_string(as_element(children[children.len()-1])));
    1985
    10.1k
          if let Some(mut ws22) = whitespace {
    1986
            // last child in mrow is white space -- mark with space *after*
    1987
    22
            if children.len() == 1 {
    1988
              // only child -- check to see if we need to set the space-width
    1989
    21
              let child = as_element(children[0]);
    1990
    21
              let child_width = child.attribute_value("data-width").unwrap_or("0").parse::<f64>().unwrap_or(0.0);
    1991
    21
              if (child_width - ws).abs() > 0.001 {
    1992
    9
                ws += child_width;
    1993
    9
                child.set_attribute_value("data-following-space-width", ws.to_string().as_str());
    1994
    12
              }
    1995
    1
            } else {
    1996
    1
              let non_space_child = as_element(children[children.len()-1]);
    1997
    1
              non_space_child.set_attribute_value("data-following-space-width", ws.to_string().as_str());
    1998
    1
            }
    1999
    10.1k
          }
    2000
    10.1k
        }
    2001
    2002
        /// look for potential numbers by looking for sequences with commas, spaces, and decimal points
    2003
    10.1k
        fn merge_number_blocks(context: &CanonicalizeContext, parent_mrow: Element, children: &mut Vec<ChildOfElement>) {
    2004
          // debug!("parent:\n{}", mml_to_string(parent_mrow));
    2005
          // If we find a comma that is not part of a number, don't form a number
    2006
          //   (see https://github.com/NSoiffer/MathCAT/issues/271)
    2007
          // Unfortunately, we can't do this in the loop below because we might discover the "not part of a number" after a number has been formed
    2008
    10.1k
          let do_not_merge_comma = is_comma_not_part_of_a_number(children);
    2009
    10.1k
          let mut i = 0;
    2010
    38.2k
          while i < children.len() {    // length might change after a merge
    2011
            // {
    2012
            //  debug!("merge_number_blocks: top of loop");
    2013
            //  for (i_child, &child) in children[i..].iter().enumerate() {
    2014
            //    let child = as_element(child);
    2015
            //    debug!("child #{}: {}", i+i_child, mml_to_string(child));
    2016
            //  }
    2017
            // }
    2018
    28.0k
            let child = as_element(children[i]);
    2019
    28.0k
            let child_name = name(child);
    2020
    2021
            // numbers start with an mn or a decimal separator
    2022
    28.0k
            if child_name == "mn" || child_name=="mtext"22.4k{
    2023
    6.09k
              let leaf_child_text = as_text(child);
    2024
              // if Roman numeral, don't merge (move on)
    2025
              // or if the 'mn' has ',', '.', or space, consider it correctly parsed and move on
    2026
    6.09k
              if is_roman_number_match(leaf_child_text) ||
    2027
    5.75k
                context.patterns.block_separator.is_match(leaf_child_text) ||
    2028
    5.64k
                (leaf_child_text.len() > 1 && context.patterns.decimal_separator710.is_match710(leaf_child_text710)) {
    2029
    559
                i += 1;
    2030
    559
                continue;
    2031
    5.53k
              }
    2032
    21.9k
            } else if child_name != "mo" ||
    2033
    9.20k
                  (do_not_merge_comma && as_text(child) == ","3.08k) ||
    2034
    6.44k
                  !context.patterns.decimal_separator.is_match(as_text(child)) {
    2035
    21.9k
              i += 1;
    2036
    21.9k
              continue;
    2037
    31
            }
    2038
    2039
            // potential start of a number
    2040
    5.56k
            let mut end = i + 1;
    2041
    5.56k
            let mut has_decimal_separator = false;
    2042
    5.56k
            let mut not_a_number = false;
    2043
    5.56k
            if i < children.len() {
    2044
              // look at the right siblings and pull in the longest sequence of number/separators -- then check it for validity
    2045
    5.56k
              for sibling4.00k in children[i+1..].iter() {
    2046
    4.00k
                let sibling = as_element(*sibling);
    2047
    4.00k
                let sibling_name = name(sibling);
    2048
    4.00k
                if sibling_name == "mn" {
    2049
    245
                  let leaf_text = as_text(sibling);
    2050
    245
                  let is_block_separator = context.patterns.block_separator.is_match(leaf_text);
    2051
    245
                  let is_decimal_separator = context.patterns.decimal_separator.is_match(leaf_text);
    2052
    245
                  if is_roman_number_match(leaf_text) || is_block_separator || is_decimal_separator {
    2053
                    // consider this mn correctly parsed
    2054
    1
                    break;
    2055
    244
                  }
    2056
    3.75k
                } else if sibling_name=="mo" || sibling_name=="mtext"2.59k {
    2057
    1.33k
                  let leaf_text = as_text(sibling);
    2058
    1.33k
                  let is_block_separator = context.patterns.block_separator.is_match(leaf_text);
    2059
    1.33k
                  let is_decimal_separator = context.patterns.decimal_separator.is_match(leaf_text);
    2060
    1.33k
                  if (leaf_text == "," && do_not_merge_comma315) ||
    2061
    1.14k
                     !(is_block_separator || is_decimal_separator954) ||
    2062
    261
                     (is_decimal_separator && has_decimal_separator75) {
    2063
                    // not a separator or (it is decimal separator and we've already seen a decimal separator)
    2064
    1.09k
                    not_a_number = is_decimal_separator && has_decimal_separator127; // e.g., 1.2.3 or 1,2,3
    2065
    1.09k
                    break;
    2066
    244
                  }
    2067
    244
                  has_decimal_separator |= is_decimal_separator;
    2068
                } else {
    2069
                  // not mn, mo, or mtext -- end of a number
    2070
    2.41k
                  break;
    2071
                }
    2072
    488
                end += 1;     // increment at end so we can tell the difference between a 'break' and end of loop
    2073
              }
    2074
    0
            }
    2075
    5.56k
            if not_a_number {
    2076
    17
              i = end + 1;
    2077
    17
              continue; // continue looking in the rest of the mrow
    2078
    5.55k
            }
    2079
    5.55k
            if ignore_final_punctuation(context, parent_mrow, &children[i..end]) {
    2080
    18
              end -= 1;
    2081
    5.53k
            };
    2082
            // debug!("start={}, end={}", i, end);
    2083
            // no need to merge if only one child (also avoids "." being considered a number)
    2084
    5.55k
            if end > i + 1 && is_likely_a_number275(context275, parent_mrow275, &275children275[i..end]) {
    2085
    107
              (i, end) = trim_whitespace(children, i, end);
    2086
    107
              merge_block(children, i, end);
    2087
    107
              // note: start..end has been collapsed, so restart after the collapsed part
    2088
    5.44k
            } else {
    2089
    5.44k
              i = end;  // start looking at the end of the block we just rejected
    2090
    5.44k
            }
    2091
    5.55k
            i += 1;
    2092
          }
    2093
    10.1k
        }
    2094
    2095
        /// Return true if we find a comma that doesn't have an <mn> on both sides
    2096
    10.1k
        fn is_comma_not_part_of_a_number(children: &[ChildOfElement])-> bool {
    2097
    10.1k
          let n_children = children.len();
    2098
    10.1k
          if n_children == 0 {
    2099
    3
            return false;
    2100
    10.1k
          }
    2101
    10.1k
          let mut previous_child = as_element(children[0]);
    2102
    14.5k
          for i in 1..n_children10.1k {
    2103
    14.5k
            let child = as_element(children[i]);
    2104
    14.5k
            if name(child) == "mo" && as_text(child) == ","6.27k && i+1 < n_children980 &&
    2105
    972
               (name(previous_child) != "mn" || name208(as_element(children[i+1])) != "mn") {
    2106
    809
              return true;
    2107
    13.7k
            }
    2108
    13.7k
            previous_child = child;
    2109
          }
    2110
    9.37k
          return false;
    2111
    10.1k
        }
    2112
    2113
        /// If we have something like 'shape' ABC, we split the ABC and add IMPLIED_SEPARATOR_HIGH_PRIORITY between them
    2114
        /// under some specific conditions (trying to be a little cautious).
    2115
        /// The returned (mrow) element reuses the arg so tree siblings links remain correct.
    2116
    11.8k
        fn split_points(leaf: Element) -> Option<Element> {
    2117
    3
          static IS_UPPERCASE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[A-Z]+$").unwrap());
    2118
    2119
    11.8k
          if !IS_UPPERCASE.is_match(as_text(leaf)) {
    2120
    9.88k
            return None;
    2121
    1.96k
          }
    2122
    2123
          // check to see if there is a bar, arrow, etc over the letters (line-segment, arc, ...)
    2124
    1.96k
          let parent = get_parent(leaf);
    2125
    1.96k
          if name(parent) == "mover" {
    2126
            // look for likely overscripts (basically just rule out some definite 'no's)
    2127
    29
            let over = as_element(parent.children()[1]);
    2128
    29
            if is_leaf(over) {
    2129
    29
              let mut over_chars = as_text(over).chars();
    2130
    29
              let first_char = over_chars.next();
    2131
    29
              if first_char.is_some() && over_chars.next().is_none() && !first_char.unwrap().is_alphanumeric(){
    2132
                // only one char and it isn't alphanumeric
    2133
    29
                return Some( split_element(leaf) );
    2134
    0
              }
    2135
    0
            }
    2136
    1.93k
          }
    2137
    2138
          // check to see if it is preceded by a geometric shape (e.g, ∠ABC)
    2139
    1.93k
          let preceding_siblings = leaf.preceding_siblings();
    2140
    1.93k
          if !preceding_siblings.is_empty() {
    2141
    1.11k
            let preceding_sibling = as_element(preceding_siblings[preceding_siblings.len()-1]);
    2142
    1.11k
            let preceding_sibling_name = name(preceding_sibling);
    2143
    1.11k
            if preceding_sibling_name == "mi" || preceding_sibling_name == "mo"886 || preceding_sibling_name == "mtext"439 {
    2144
    711
              let preceding_text = as_text(preceding_sibling);
    2145
    711
              return crate::definitions::SPEECH_DEFINITIONS.with(|definitions| {
    2146
    711
                let defs = definitions.borrow();
    2147
    711
                let prefix_ops = defs.get_hashset("GeometryPrefixOperators").unwrap();
    2148
    711
                let shapes = defs.get_hashset("GeometryShapes").unwrap();
    2149
    711
                if prefix_ops.contains(preceding_text) || shapes708.contains(preceding_text) {
    2150
                  // split leaf
    2151
    9
                  return Some( split_element(leaf) ); // always treated as function names
    2152
                } else {
    2153
    702
                  return None;
    2154
                }
    2155
    711
              })
    2156
    407
            }
    2157
    817
          }
    2158
    1.22k
          return None;
    2159
    2160
    38
          fn split_element(leaf: Element) -> Element {
    2161
    38
            let mut children = Vec::with_capacity(leaf.children().len());
    2162
    51
            for ch in as_text(leaf)38.chars38() {
    2163
    51
              let new_leaf = create_mathml_element(&leaf.document(), "mi");
    2164
    51
              new_leaf.set_text(&ch.to_string());
    2165
    51
              children.push(new_leaf);
    2166
    51
            }
    2167
    38
            set_mathml_name(leaf, "mrow");
    2168
    38
            leaf.replace_children(children);
    2169
    38
            return leaf;
    2170
    38
          }
    2171
    11.8k
        }
    2172
    2173
        /// If we have something like 'V e l o c i t y', merge that into a single <mi>
    2174
        /// We only do this for sequences of at least three chars, and also exclude things like consecutive letter (e.g., 'x y z')
    2175
        /// The returned (mi) element reuses 'mi'
    2176
    11.4k
        fn merge_mi_sequence(mi: Element) -> Option<Element> {
    2177
          // The best solution would be to use a dictionary of words, or maybe restricted to words in a formula,
    2178
          //   but that would likely miss the words used in slope=run/rise.
    2179
          // It would also be really expensive since we would need a dictionary for each language.
    2180
          // We shouldn't need to worry about trig names like "cos", but people sometimes forget to use "\cos"
    2181
          // Hence, we check against the "FunctionNames" that get read on startup.
    2182
    70
          fn is_vowel(ch: char) -> bool {
    2183
    70
            matches!58(ch,
    2184
              'a' | 'e' | 'i' | 'o' | 'u' | 'y' |
    2185
              'à' | 'á' | 'â' | 'ã' | 'ä' | 'è' | 'é' | 'ê' | 'ë' | 'ì' | 'í' | 'î' | 'ï' |
    2186
              'ò' | 'ó' | 'ô' | 'õ' | 'ö' | 'ú' | 'Ù' | 'û' | 'ü' | 'ý' | 'ÿ' |
    2187
              'ả' | 'ạ' | 'ă' | 'ằ' | 'ẳ' | 'ẵ' | 'ắ' | 'ặ' | 'ầ' | 'ẩ' | 'ẫ' | 'ấ' | 'ậ' | 'ẻ' | 'ẽ' | 'ẹ' | 'ề' | 'ể' | 'ễ' | 'ế' | 'ệ' |
    2188
              'ỉ' | 'ĩ' | 'ị' | 'ỏ' | 'ọ' | 'ồ' | 'ổ' | 'ỗ' | 'ố' | 'ộ' | 'ơ' | 'ờ' | 'ở' | 'ỡ' | 'ớ' | 'ợ' |
    2189
              'ủ' | 'ũ' | 'ụ' | 'ư' | 'ừ' | 'ử' | 'ữ' | 'ứ' | 'ự' | 'ỳ' | 'ỷ' | 'ỹ' | 'ỵ'
    2190
            )
    2191
    70
          }
    2192
    11.4k
          let parent = get_parent(mi);  // not canonicalized into mrows, so parent could be "math"
    2193
    11.4k
          let parent_name = name(parent);
    2194
          // don't merge if more than one char, or if not in an mrow (or implied on since we haven't normalized yet)
    2195
    11.4k
          if as_text(mi).chars().nth(1).is_some() || !(parent_name == "mrow"8.87k || parent_name == "math"5.62k) {
    2196
    5.16k
            return None;
    2197
    6.25k
          }
    2198
    6.25k
          let mut text =  as_text(mi).to_string();
    2199
    6.25k
          let text_script = Script::from(text.chars().next().unwrap_or('a'));
    2200
    6.25k
          let following_siblings = mi.following_siblings();
    2201
    6.25k
          let mut last_char_is_scripted = None;
    2202
    6.25k
          let mut following_mi_siblings: Vec<Element> = following_siblings.iter()
    2203
    6.25k
                .map_while(|&child| {4.15k
    2204
    4.15k
                  let mut child = as_element(child);
    2205
    4.15k
                  let mut is_ok = false;
    2206
    4.15k
                  if name(child) == "msub" || name(child) == "msup"4.02k {
    2207
                    // check if the *last* char in the sequence is scripted
    2208
                    // if so, we need to stop here anyway and deal with it specially
    2209
    163
                    last_char_is_scripted = Some(child);   // need to remember the value -- cleared later if not ok
    2210
    163
                    child = as_element(child.children()[0]);
    2211
    233
                    while name(child) == "mrow" && child.children().len() == 171 {
    2212
    70
                      // the base may be wrapped with mrows
    2213
    70
                      child = as_element(child.children()[0]);
    2214
    70
                    }
    2215
    3.99k
                  }
    2216
    4.15k
                  if name(child) == "mi" {
    2217
    402
                    let mut child_text = as_text(child).chars();
    2218
    402
                    let first_char = child_text.next().unwrap_or('a');
    2219
    402
                    if child_text.next().is_none() && Script::from(first_char) == text_script376 {
    2220
    365
                      text.push(first_char);
    2221
    365
                      is_ok = true;
    2222
    365
                    }37
    2223
    3.75k
                  }
    2224
    4.15k
                  if last_char_is_scripted.is_some() {
    2225
    163
                    if is_ok {
    2226
    114
                      is_ok = false;    // don't want to continue
    2227
    114
                    } else {
    2228
    49
                      last_char_is_scripted = None; // reset to None
    2229
    49
                    }
    2230
    3.99k
                  }
    2231
    4.15k
                  if is_ok {Some(child)251} else {None3.90k}
    2232
    4.15k
                })
    2233
    6.25k
                .collect();
    2234
    6.25k
          if following_mi_siblings.is_empty() {
    2235
    6.03k
            return None;
    2236
    224
          }
    2237
    2238
    224
          if let Some(last14) = last_char_is_scripted {
    2239
    14
            // add the last char to the run
    2240
    14
            following_mi_siblings.push(last);
    2241
    210
          }
    2242
          // debug!("merge_mi_sequence: text={}", &text);
    2243
    224
          if let Some(answer11) = crate::definitions::SPEECH_DEFINITIONS.with(|definitions| {
    2244
    224
            let definitions = definitions.borrow();
    2245
    224
            let function_names = definitions.get_hashset("FunctionNames").unwrap();
    2246
            // UEB seems to think "Sin" (etc) is used for "sin", so we move to lower case
    2247
            // function name might be (wrongly) set to italic math alphanumeric chars, including bold italic
    2248
    224
            if let Some(ascii_text221) = CanonicalizeContext::math_alphanumeric_to_ascii(&text)
    2249
    221
              && function_names.contains(&ascii_text.to_lowercase()) {
    2250
    10
                return Some(merge_from_text(mi, &ascii_text, &following_mi_siblings));
    2251
    214
              }
    2252
    214
            if function_names.contains(&text) {
    2253
    0
              return Some(merge_from_text(mi, &text, &following_mi_siblings));
    2254
    214
            }
    2255
            // unlike "FunctionNames", "KnownWords" might not exist
    2256
    214
            if let Some(word_map131) = definitions.get_hashset("KnownWords")
    2257
    131
              && word_map.contains(&text) {
    2258
    1
                return Some(merge_from_text(mi, &text, &following_mi_siblings));
    2259
    213
              }
    2260
    213
            return None;
    2261
    224
          }) {
    2262
    11
            return answer;
    2263
    213
          }
    2264
    2265
          // don't be too aggressive combining mi's when they are short
    2266
    213
          if text.chars().count() < 3 {
    2267
    186
            return None;
    2268
    27
          }
    2269
          // If it is a word, it needs a vowel and it must be a letter
    2270
          // FIX: this check needs to be internationalized to include accented vowels, other alphabets
    2271
    70
          if !text.chars()27.any27(|ch| is_vowel(ch) || !ch.is_ascii_alphabetic()58) {
    2272
    15
            return None;
    2273
    12
          }
    2274
    2275
          // now for some heuristics to rule out a sequence of variables
    2276
          // rule out sequences like 'abc' and also 'axy' that are in alphabetical order
    2277
    12
          let mut chars = text.chars();
    2278
    12
          let mut left = chars.next().unwrap();   // at least 3 chars
    2279
    12
          let mut is_in_alphabetical_order = true;
    2280
    23
          for ch in chars12 {
    2281
    23
            if (left as u32) >= (ch as u32) {
    2282
    3
              is_in_alphabetical_order = false;
    2283
    3
              break;                 // can't be 'abc', 'axy', etc
    2284
    20
            }
    2285
    20
            left = ch;
    2286
          }
    2287
    12
          if is_in_alphabetical_order || text.len() < 43 {
    2288
            // If it is in alphabetical order, it's not likely a word
    2289
    12
            return None;
    2290
    0
          }
    2291
    2292
          // FIX: should add more heuristics to rule out words
    2293
    0
          return merge_from_text(mi, &text, &following_mi_siblings);
    2294
    2295
    11
          fn merge_from_text<'a>(mi: Element<'a>, text: &str, following_siblings: &[Element<'a>]) -> Option<Element<'a>> {
    2296
            // remove trailing mi's
    2297
    11
            let i_last_child = following_siblings.len()-1;
    2298
    11
            let last_child = following_siblings[i_last_child];
    2299
    11
            if name(last_child) == "mi" {
    2300
    10
              following_siblings5.iter5().for_each5(|sibling| sibling.remove_from_parent());
    2301
    5
              mi.set_text(text);
    2302
    5
              return Some(mi);
    2303
            } else {
    2304
              // replace the base of the scripted element (the last child) with the run (e.g. 's i n^2' -> {sin}^2)
    2305
    6
              mi.remove_from_parent();
    2306
    6
              following_siblings[..i_last_child].iter().for_each(|sibling| sibling.remove_from_parent());
    2307
    6
              let mut base = as_element(last_child.children()[0]);
    2308
    9
              while name(base) == "mrow" && base.children().len() == 13 {
    2309
    3
                // the base may be wrapped with mrows
    2310
    3
                base = as_element(base.children()[0]);
    2311
    3
                base.remove_attribute(SPLIT_TOKEN);
    2312
    3
              }
    2313
    6
              base.set_text(text);
    2314
    6
              return Some(last_child);
    2315
            }
    2316
    11
          }
    2317
    11.4k
        }
    2318
    2319
        // Check if start..end is a number
    2320
    275
        fn is_likely_a_number(context: &CanonicalizeContext, mrow: Element, children: &[ChildOfElement]) -> bool {
    2321
          // Note: the children of math_or_mrow aren't valid ('children' represents the current state)
    2322
    275
          let end = children.len();
    2323
          // {
    2324
          //  let n_preceding_siblings = as_element(children[0]).preceding_siblings().len();
    2325
          //  debug!("is_likely_a_number: start/end={}/{}", n_preceding_siblings, n_preceding_siblings+end);
    2326
          //  for (i, &child) in children.iter().enumerate() {
    2327
          //    let child = as_element(child);
    2328
          //    debug!("child# {}: {}", n_preceding_siblings+i, mml_to_string(child));
    2329
          //  }
    2330
          //  debug!("\n");
    2331
          // }
    2332
    2333
          // gather up the text of the children (all mn, mo, or mtext)
    2334
    275
          let mut previous_name_was_mn = false;
    2335
    275
          let mut text = "".to_string();
    2336
    727
          for &child in children275 {
    2337
    727
            let child = as_element(child);
    2338
    727
            let child_name = name(child);
    2339
    727
            if previous_name_was_mn && child_name == "mn"303 {
    2340
    94
              text.push('\u{FFFF}');      // FIX: this should come from the separator string
    2341
    633
            }
    2342
    727
            text.push_str(as_text(child));
    2343
    727
            previous_name_was_mn = child_name == "mn";
    2344
          }
    2345
    2346
    275
          let text = text.trim(); // could be space got merged into an mn (e.g., braille::UEB::iceb::expr_3_1_6)
    2347
          // debug!("  text='{}', decimal num={}, 3 digit match={}, 3-5 match={}, 1 digit={}", &text,
    2348
          //    context.patterns.digit_only_decimal_number.is_match(text),
    2349
          //    context.patterns.block_3digit_pattern.is_match(text),
    2350
          //    context.patterns.block_3_5digit_pattern.is_match(text),
    2351
          //    context.patterns.block_1digit_pattern.is_match(text));
    2352
    275
          if !(context.patterns.digit_only_decimal_number.is_match(text) ||
    2353
    190
             context.patterns.block_3digit_pattern.is_match(text) ||
    2354
    167
             context.patterns.block_3_5digit_pattern.is_match(text) ||
    2355
    166
             context.patterns.block_4digit_hex_pattern.is_match(text) ||
    2356
    162
             ( (text.chars().count() > 5 || context.patterns.decimal_separator139.is_match139(text139)) &&
    2357
    25
               context.patterns.block_1digit_pattern.is_match(text) )
    2358
            ) {
    2359
    161
              return false;
    2360
    114
          }
    2361
    2362
          // ??? might want to rule out "sequences" like '100, 200, 300' and '100, 103, 106' (if constant difference, then a sequence)
    2363
    2364
          // If surrounded by fences, and commas are used, leave as is (e.g, "{1,234}")
    2365
    114
          if !text.contains(',') {
    2366
    84
            return true;   // not comma separated
    2367
    30
          }
    2368
    2369
          // We have already checked for whitespace as separators, so it must be a comma. Just check the fences.
    2370
          // This is not yet in canonical form, so the fences may be siblings or siblings of the parent
    2371
    30
          let preceding_siblings = as_element(children[0]).preceding_siblings();
    2372
    30
          let following_siblings = as_element(children[end-1]).following_siblings();
    2373
          let first_child;
    2374
          let last_child;
    2375
    30
          if preceding_siblings.is_empty() && following_siblings19.is_empty19() {
    2376
            // number spans all children, look to parent for fences
    2377
    14
            let preceding_children = mrow.preceding_siblings();
    2378
    14
            let following_children = mrow.following_siblings();
    2379
    14
            if preceding_children.is_empty() || following_children9.is_empty9() {
    2380
    9
              return true; // doesn't have left or right fence
    2381
    5
            }
    2382
    5
            first_child = preceding_children[preceding_children.len()-1];
    2383
    5
            last_child = following_children[0];
    2384
    16
          } else if preceding_siblings.is_empty() || following_siblings11.is_empty11() {
    2385
    13
            return true; // can't be fences around it
    2386
    3
          } else {
    2387
    3
            first_child = preceding_siblings[preceding_siblings.len()-1];
    2388
    3
            last_child = following_siblings[0];
    2389
    3
          }
    2390
    8
          let first_child = as_element(first_child);
    2391
    8
          let last_child = as_element(last_child);
    2392
    8
          return !(name(first_child) == "mo" && is_fence(first_child) &&
    2393
    7
                 name(last_child) == "mo" && is_fence(last_child) );
    2394
    275
        }
    2395
    2396
        // fn count_decimal_pts(context: &CanonicalizeContext, children: &[ChildOfElement], start: usize, end: usize) -> usize {
    2397
        //  let mut n_decimal_pt = 0;
    2398
        //  for &child_as_element in children.iter().take(end).skip(start) {
    2399
        //    let child = as_element(child_as_element);
    2400
        //    if context.patterns.decimal_separator.is_match(as_text(child))  {
    2401
        //      n_decimal_pt += 1;
    2402
        //    }
    2403
        //  }
    2404
        //  return n_decimal_pt;
    2405
        // }
    2406
    2407
        /// This is a special case heuristic so try and determine if a terminating punctuation should be a decimal separator
    2408
        /// Often math expressions end with punctuations for typographic reasons, so we try to figure that out here.
    2409
        /// 'children' is a subset of 'mrow'
    2410
    5.55k
        fn ignore_final_punctuation(context: &CanonicalizeContext, mrow: Element, children: &[ChildOfElement]) -> bool {
    2411
    5.55k
          let last_child = children[children.len()-1];
    2412
    5.55k
          if mrow.children()[mrow.children().len()-1] != last_child {
    2413
    3.49k
            return false;   // not at end
    2414
    2.05k
          }
    2415
    2.05k
          let parent = mrow.parent().unwrap().element();
    2416
    2.05k
          if let Some(math1.71k) = parent
    2417
    1.71k
            && name(math) != "math" {
    2418
    1.58k
              return false;     // mrow inside something else -- not at end
    2419
    471
            }
    2420
    2421
    471
          let last_child = as_element(last_child);
    2422
          // debug!("ignore_final_punctuation: last child={}", mml_to_string(last_child));
    2423
    471
          if name(last_child) != "mo" {
    2424
    451
            return false; // last was not "mo", so can't be a period
    2425
    20
          }
    2426
    2427
    20
          if !context.patterns.decimal_separator.is_match(as_text(last_child)) {
    2428
    0
            return false;
    2429
    20
          }
    2430
    2431
          // debug!("ignore_final_punctuation: #preceding={}", as_element(children[0]).preceding_siblings().len());
    2432
          // look to preceding siblings and see if an of the mn's have a decimal separator
    2433
    20
          return !as_element(children[0]).preceding_siblings().iter()
    2434
    101
              .any20(|&child| {
    2435
    101
                let child = as_element(child);
    2436
    101
                name(child) == "mn" && context.patterns.decimal_separator14.is_match14(as_text(child)14)
    2437
    101
              });
    2438
    5.55k
        }
    2439
    2440
        /// Trim off any children that are whitespace on either side
    2441
    107
        fn trim_whitespace(children: &mut [ChildOfElement], start: usize, end: usize) -> (usize, usize) {
    2442
    107
          let mut real_start = start;
    2443
          #[allow(clippy::needless_range_loop)]  // I don't like enumerate/take/skip here
    2444
    107
          for i in start..end {
    2445
    107
            let child = as_element(children[i]);
    2446
    107
            if !as_text(child).trim().is_empty() {
    2447
    107
              real_start = i;
    2448
    107
              break;
    2449
    0
            }
    2450
          }
    2451
    2452
    107
          let mut real_end = end;
    2453
    157
          for i in (start..end107).rev107() {
    2454
    157
            let child = as_element(children[i]);
    2455
    157
            if !as_text(child).trim().is_empty() {
    2456
    107
              real_end = i+1;
    2457
    107
              break;
    2458
    50
            }
    2459
          }
    2460
    107
          return (real_start, real_end);
    2461
    107
        }
    2462
    2463
        /// Merge the number block from start..end
    2464
    107
        fn merge_block(children: &mut Vec<ChildOfElement>, start: usize, end: usize) {
    2465
    2466
          // debug!("merge_block: merging {}..{}", start, end);
    2467
    107
          let mut mn_text = String::with_capacity(4*(end-start)-1);    // true size less than #3 digit blocks + separator
    2468
    237
          for &child_as_element in children.iter()107.take107(end107).skip107(start107) {
    2469
    237
            let child = as_element(child_as_element);
    2470
    237
            mn_text.push_str(as_text(child));
    2471
    237
          }
    2472
    107
          let child = as_element(children[start]);
    2473
    107
          set_mathml_name(child, "mn");
    2474
    107
          child.set_text(&mn_text);
    2475
    2476
    107
          children.drain(start+1..end);
    2477
    107
        }
    2478
    2479
    2480
        /// merge  ° C or  ° F into a single <mi> with the text '℃' or '℉' -- prevents '°' from becoming a superscript
    2481
        #[allow(non_snake_case)]
    2482
    5.90k
        fn merge_degrees_C_F<'a>(mrow: Element<'a>) -> Element<'a> {
    2483
    5.90k
          let mut degree_child = None;
    2484
    28.1k
          for child in mrow5.90k.children5.90k() {
    2485
    28.1k
            let child = as_element(child);
    2486
    28.1k
            if is_leaf(child) {
    2487
    23.9k
              match as_text(child) {
    2488
    23.9k
                "°" => {
    2489
    34
                  degree_child = Some(child);
    2490
    34
                },
    2491
    23.9k
                "°C" => {
    2492
    12
                  child.set_text("℃");
    2493
    12
                  degree_child = None;
    2494
    12
                },
    2495
    23.8k
                "°F" => {
    2496
    0
                  child.set_text("℉");
    2497
    0
                  degree_child = None;
    2498
    0
                },
    2499
    23.8k
                text  => {
    2500
    23.8k
                  if let Some(degree_child23) = degree_child
    2501
    23
                    && (text == "C" || text == "F"22) {
    2502
                      // merge the degree child with the current child
    2503
    3
                      degree_child.set_text(if text == "C" { "℃"1 } else { "℉"2 });
    2504
    3
                      child.remove_from_parent();
    2505
    23.8k
                    }
    2506
                    // merge the degree child with the current child
    2507
    23.8k
                  degree_child = None;
    2508
                },
    2509
              }
    2510
    4.25k
            }
    2511
          }
    2512
    5.90k
          return mrow;
    2513
    5.90k
        }
    2514
    2515
    2516
        /// merge consecutive leaves containing any of the 'chars' into the first leaf -- probably used for omission with('_')
    2517
    5.90k
        fn merge_chars<'a>(mrow: Element<'a>, pattern: &Regex) -> Element<'a> {
    2518
    5.90k
          let mut first_child = None;
    2519
    5.90k
          let mut new_text = "".to_string();
    2520
    28.1k
          for child in mrow5.90k.children5.90k() {
    2521
    28.1k
            let child = as_element(child);
    2522
    28.1k
            if is_leaf(child) {
    2523
    23.9k
              let text = as_text(child);
    2524
    23.9k
              if pattern.is_match(text) {
    2525
    134
                if new_text.is_empty() {
    2526
    118
                  // potential start of a string
    2527
    118
                  first_child = Some(child);
    2528
    118
                  new_text = as_text(child).to_string();
    2529
    118
                } else {
    2530
    16
                  // merge chars
    2531
    16
                  new_text.push_str(text);
    2532
    16
                  child.remove_from_parent();
    2533
    16
                }
    2534
    23.8k
              } else if new_text.len() > 1 {
    2535
    99
                // end of a run
    2536
    99
                first_child.unwrap().set_text(&new_text);
    2537
    99
                new_text.clear();
    2538
    23.7k
              } else {
    2539
    23.7k
                new_text.clear(); // just one entry -- no need to set the text
    2540
    23.7k
              }
    2541
    4.25k
            } else if new_text.len() > 1{
    2542
    7
              // end of a run
    2543
    7
              first_child.unwrap().set_text(&new_text);
    2544
    7
              new_text.clear();
    2545
    4.24k
            } else {
    2546
    4.24k
              new_text.clear();     // just one entry -- no need to set the text
    2547
    4.24k
            }
    2548
          }
    2549
    5.90k
          if new_text.len() > 1{
    2550
    9
            // end of a run
    2551
    9
            first_child.unwrap().set_text(&new_text);
    2552
    5.89k
          }
    2553
    5.90k
          return mrow;
    2554
    5.90k
        }
    2555
    2556
        /// curl and divergence are handled as two character operators
    2557
        /// if found, merge them into their own (new) mrow that has an intent on it
    2558
        /// we can have '∇' or '𝛁', or those as vectors (inside an mover)
    2559
    10.1k
        fn merge_cross_or_dot_product_elements(children: &mut Vec<ChildOfElement>) {
    2560
    10.1k
          if children.is_empty() {
    2561
    3
            return;
    2562
    10.1k
          }
    2563
    10.1k
          let mut i = 0;
    2564
    10.1k
          let mut is_previous_nabla = false;
    2565
    31.5k
          while i < children.len() - 1 {
    2566
    21.3k
            let child = as_element(children[i]);
    2567
    21.3k
            if is_previous_nabla {
    2568
    14
              if is_leaf(child) {
    2569
    14
                let text = as_text(child);
    2570
    14
                if text == "⋅" || text == "·"13|| text == "×"9 {
    2571
    12
                  let nabla_child = as_element(children[i-1]);
    2572
    12
                  let nabla_text = as_text( get_possible_embellished_node(nabla_child) );
    2573
    12
                  let new_mrow = create_mathml_element(&child.document(), "mrow");
    2574
    12
                  new_mrow.set_attribute_value(ACT_AS_OPERATOR, nabla_text);
    2575
    12
                  new_mrow.append_child(nabla_child);
    2576
    12
                  new_mrow.append_child(child);
    2577
    12
                  children[i-1] = ChildOfElement::Element(new_mrow);
    2578
    12
                  children.remove(i);
    2579
    12
                }2
    2580
    0
              }
    2581
    14
              is_previous_nabla = false;
    2582
            } else {
    2583
    21.3k
              let potential_nabla = if name(child) == "mover" {as_element136(child.children()[0]136)} else {child21.1k};
    2584
    21.3k
              if is_leaf(potential_nabla) {
    2585
    19.0k
                let text = as_text(potential_nabla);
    2586
    19.0k
                if text == "∇" || text == "𝛁"19.0k {
    2587
    22
                  is_previous_nabla = true;
    2588
    19.0k
                }
    2589
    2.27k
              }
    2590
            }
    2591
    21.3k
            i += 1;
    2592
          }
    2593
    10.1k
        }
    2594
    2595
    5.90k
        fn merge_dots(mrow: Element) -> Element {
    2596
          // merge consecutive <mo>s containing '.' into ellipsis
    2597
    5.90k
          let children = mrow.children();
    2598
    5.90k
          let mut i = 0;
    2599
    5.90k
          let mut n_dots = 0;   // number of consecutive mo's containing dots
    2600
    34.1k
          while i < children.len() {
    2601
    28.2k
            let child = as_element(children[i]);
    2602
    28.2k
            if name(child) == "mo" {
    2603
    10.4k
              let text = as_text(child);
    2604
    10.4k
              if text == "." {
    2605
    71
                n_dots += 1;
    2606
    71
                if n_dots == 3 {
    2607
    3
                  let first_child = as_element(children[i-2]);
    2608
    3
                  first_child.set_text("…");
    2609
    3
                  as_element(children[i-1]).remove_from_parent();
    2610
    3
                  child.remove_from_parent();
    2611
    3
                  n_dots = 0;
    2612
    68
                }
    2613
    10.3k
              } else {
    2614
    10.3k
                n_dots = 0;
    2615
    10.3k
              }
    2616
    17.7k
            } else {
    2617
    17.7k
              n_dots = 0;
    2618
    17.7k
            }
    2619
    28.2k
            i += 1;
    2620
          }
    2621
    5.90k
          return mrow;
    2622
    5.90k
        }
    2623
    2624
    5.90k
        fn merge_primes(mrow: Element) -> Element {
    2625
          // merge consecutive <mo>s containing primes (in various forms)
    2626
    5.90k
          let mut children = mrow.children();
    2627
    5.90k
          let mut i = 0;
    2628
    5.90k
          let mut n_primes = 0;   // number of consecutive mo's containing primes
    2629
    34.1k
          while i < children.len() {
    2630
    28.1k
            let child = as_element(children[i]);
    2631
    28.1k
            if name(child) == "mo" {
    2632
    10.4k
              let text = as_text(child);
    2633
              // FIX: should we be more restrictive and change (apostrophe) only in a superscript?
    2634
    10.4k
              if IS_PRIME.is_match(text) {
    2635
    21
                n_primes += 1;
    2636
    10.4k
              } else if n_primes > 0 {
    2637
    3
                merge_prime_elements(&mut children, i - n_primes, i);
    2638
    3
                n_primes = 0;
    2639
    10.4k
              }
    2640
    17.7k
            } else if n_primes > 0 {
    2641
    2
              merge_prime_elements(&mut children, i - n_primes, i);
    2642
    2
              n_primes = 0;
    2643
    17.7k
            }
    2644
    28.1k
            i += 1;
    2645
          }
    2646
    5.90k
          if n_primes > 0 {
    2647
    12
            merge_prime_elements(&mut children, i - n_primes, i);
    2648
    5.89k
          }
    2649
    5.90k
          return mrow;
    2650
    5.90k
        }
    2651
    2652
    17
        fn merge_prime_elements(children: &mut [ChildOfElement], start: usize, end: usize) {
    2653
          // not very efficient since this is probably causing an array shift each time (array is probably not big though)
    2654
    17
          let first_child = as_element(children[start]);
    2655
    17
          let mut new_text = String::with_capacity(end+3-start);  // one per element plus a little extra
    2656
    17
          new_text.push_str(as_text(first_child));
    2657
    17
          for &child_as_element4 in children.iter().take(end).skip(start+1) {
    2658
    4
            let child = as_element(child_as_element);
    2659
    4
            let text = as_text(child);    // only in this function because it is an <mo>
    2660
    4
            new_text.push_str(text);
    2661
    4
            child.remove_from_parent();
    2662
    4
          }
    2663
    17
          first_child.set_text(&merge_prime_text(&new_text));
    2664
    17
        }
    2665
    2666
    83
        fn merge_prime_text(text: &str) -> String {
    2667
          // merge together single primes into double primes, etc.
    2668
    83
          let mut n_primes = 0;
    2669
    101
          for ch in text83.chars83() {
    2670
    101
            match ch {
    2671
    90
              '\'' | '′' => n_primes += 1,
    2672
    9
              '″' => n_primes += 2,
    2673
    0
              '‴' => n_primes += 3,
    2674
    2
              '⁗' => n_primes += 4,
    2675
              _ => {
    2676
    0
                eprintln!("merge_prime_text: unexpected char '{ch}' found in prime text '{text}'");
    2677
    0
                return text.to_string();
    2678
              }
    2679
            }
    2680
          }
    2681
          // it would be very rare to have more than a quadruple prime, so the inefficiency in the won't likely happen
    2682
    83
          let mut result = String::with_capacity(n_primes);  // likely 4x too big, but string is short-lived and small
    2683
    83
          for _ in 0..n_primes/4 {
    2684
    3
            result.push('⁗');
    2685
    3
          }
    2686
    83
          match n_primes % 4 {
    2687
    61
            1 => result.push('′'),
    2688
    20
            2 => result.push('″'),
    2689
    1
            3 => result.push('‴'),
    2690
    1
            _ => ()  // can't happen
    2691
          }
    2692
    83
          return result;
    2693
    83
        }
    2694
    2695
        // from https://www.w3.org/TR/MathML3/chapter7.html#chars.pseudo-scripts
    2696
    35.1k
        fn is_pseudo_script_char(ch: char) -> bool {
    2697
    35.1k
          matches!35.0k(ch,
    2698
            '\"' | '\'' | '*' | '`' | 'ª' | '°' | '²' | '³' | '´' | '¹' | 'º' |
    2699
            '\u{2018}' | '\u{2019}' | '\u{201C}' | '\u{201D}' | '\u{201E}' | '\u{201F}' |
    2700
            '\u{2032}' | '\u{2033}' | '\u{2034}' | '\u{2035}' | '\u{2036}' | '\u{2037}' | '\u{2057}'
    2701
          )
    2702
    35.1k
        }
    2703
    5.90k
        fn handle_pseudo_scripts(mrow: Element) -> Element {
    2704
    2705
    5.90k
          assert!(name(mrow) == "mrow" || ELEMENTS_WITH_ONE_CHILD2.42k.contains2.42k(name(mrow)2.42k), "non-mrow passed to handle_pseudo_scripts: {}", mml_to_string0(mrow0));
    2706
    5.90k
          let mut children = mrow.children();
    2707
          // check to see if mrow of all pseudo scripts
    2708
    5.91k
          if children.iter()5.90k.all5.90k(|&child| {
    2709
    5.91k
            is_pseudo_script(as_element(child))
    2710
    5.91k
          }) {
    2711
    2
            let parent = get_parent(mrow);  // must exist
    2712
    2
            let is_first_child = mrow.preceding_siblings().is_empty();
    2713
    2
            if  is_first_child {
    2714
    0
              return mrow; // FIX: what should happen
    2715
    2
            }
    2716
    2
            if crate::xpath_functions::IsNode::is_scripted(parent) {
    2717
    2
              return mrow;   // already in a script position
    2718
    0
            }
    2719
    0
            if name(parent) == "mrow" {
    2720
    0
              mrow.set_attribute_value("data-pseudo-script", "true");
    2721
    0
              return handle_pseudo_scripts(parent);
    2722
            } else {
    2723
    0
              return mrow; // FIX: what should happen?
    2724
            }
    2725
    5.90k
          }
    2726
    2727
    5.90k
          let mut i = 1;
    2728
    5.90k
          let mut found = false;
    2729
    28.1k
          while i < children.len() {
    2730
    22.2k
            let child = as_element(children[i]);
    2731
    22.2k
            if is_pseudo_script(child) ||
    2732
    22.2k
               child.attribute("data-pseudo-script").is_some() {
    2733
    35
              let msup = create_mathml_element(&child.document(), "msup");
    2734
    35
              msup.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    2735
    35
              msup.append_child(children[i-1]);
    2736
    35
              msup.append_child(child);
    2737
    35
              children[i-1] = ChildOfElement::Element(msup);
    2738
    35
              children.remove(i);
    2739
    35
              found = true;
    2740
    22.2k
            } else {
    2741
    22.2k
              i += 1;
    2742
    22.2k
            }
    2743
          }
    2744
    5.90k
          if found {
    2745
    25
            mrow.replace_children(children)
    2746
    5.88k
          }
    2747
    5.90k
          return mrow;
    2748
    2749
    28.1k
          fn is_pseudo_script(child: Element) -> bool {
    2750
    28.1k
            if name(child) == "mo" {
    2751
    10.4k
              let text = as_text(child);
    2752
    10.4k
              if let Some(ch10.3k) = single_char(text)
    2753
    10.3k
                && is_pseudo_script_char(ch) {
    2754
                  // don't script a pseudo-script
    2755
    55
                  let preceding_siblings = child.preceding_siblings();
    2756
    55
                  if !preceding_siblings.is_empty() {
    2757
    42
                    let last_child = as_element(preceding_siblings[preceding_siblings.len()-1]);
    2758
    42
                    if name(last_child) == "mo" &&
    2759
    10
                       let Some(ch) = single_char(as_text(last_child))
    2760
    10
                        && is_pseudo_script_char(ch) {
    2761
    6
                          return false;
    2762
    36
                        }
    2763
    13
                  }
    2764
    49
                  if text == "*" {
    2765
                    // could be infix "*" -- this is a weak check to see if what follows is potentially an operand
    2766
    5
                    let following_siblings = child.following_siblings();
    2767
    5
                    if  following_siblings.is_empty() {
    2768
    1
                      return true;
    2769
    4
                    }
    2770
    4
                    let first_child = as_element(following_siblings[0]);
    2771
    4
                    return name(first_child) != "mo" || ["(", "[", "{"].contains(&text);
    2772
                  } else {
    2773
    44
                    return true;
    2774
                  }
    2775
    10.3k
                }
    2776
    17.7k
            }
    2777
    28.1k
            return false;
    2778
    2779
            /// An efficient method to get the char from a string if it is just one char or fail
    2780
    10.4k
            fn single_char(text: &str) -> Option<char> {
    2781
    10.4k
              let mut chars = text.chars();
    2782
    10.4k
              let ch = chars.next();
    2783
    10.4k
              if ch.is_none() || chars.next().is_some() {
    2784
    39
                return None;   // not one character
    2785
              } else {
    2786
    10.3k
                return ch;
    2787
              }
    2788
    10.4k
            }
    2789
    28.1k
          }
    2790
    2791
    5.90k
        }
    2792
    2793
    10.1k
        fn handle_convert_to_mmultiscripts(children: &mut Vec<ChildOfElement>) {
    2794
    10.1k
          if children.len() == 1 {
    2795
    4.45k
            return;   // can't convert to mmultiscripts if there is nothing to attach an empty base to
    2796
    5.72k
          }
    2797
    5.72k
            let mut i = 0;
    2798
          // convert_to_mmultiscripts changes 'children', so can't cache length
    2799
    32.7k
          while i < children.len() {
    2800
    26.9k
            let child = as_element(children[i]);
    2801
    26.9k
            let child_name = name(child);
    2802
    26.9k
            if (child_name == "msub" || child_name == "msup"26.3k || child_name == "msubsup"25.8k) && CanonicalizeContext::is_empty_element1.24k(as_element1.24k(child.children()[0]1.24k)) {
    2803
    115
              i = convert_to_mmultiscripts(children, i);
    2804
    26.8k
            } else {
    2805
    26.8k
              i += 1;
    2806
    26.8k
            }
    2807
          }
    2808
    10.1k
        }
    2809
    2810
    2811
        /// Converts the script element with an empty base to mmultiscripts by sucking the base from the following or preceding element.
    2812
        /// The following element is preferred so that these become prescripts (common usage is from TeX), but if the preceding element
    2813
        ///   has a closer mi/mtext, it is used.
    2814
        /// mhchem has some ugly output (at least in MathJax) and that's where using the following element makes sense (usually)
    2815
        ///   because an empty base (mpadded width=0) is used for the scripts. A hacky attribute indicates this case.
    2816
    115
        fn convert_to_mmultiscripts(mrow_children: &mut Vec<ChildOfElement>, i: usize) -> usize {
    2817
          // this is a bit messy/confusing because we might scan forwards or backwards and this affects whether
    2818
          // we are scanning for prescripts or postscripts
    2819
          // the generic name "primary_scripts" means prescripts if going forward or postscripts if going backwards
    2820
          // if we are going forward and hit a sub/superscript with a base, then those scripts become postscripts ("other_scripts")
    2821
          // if we are going backwards, we never add prescripts
    2822
    2823
          // let parent = get_parent(as_element(mrow_children[i]));
    2824
          // debug!("convert_to_mmultiscripts (i={}) -- PARENT:\n{}", i, mml_to_string(parent));
    2825
    2826
    115
          let i_base = choose_base_of_mmultiscripts(mrow_children, i);
    2827
    115
          let mut base = as_element(mrow_children[i_base]);
    2828
          // debug!("convert_to_mmultiscripts -- base\n{}", mml_to_string(base));
    2829
    115
          let base_name = name(base);
    2830
    115
          let mut prescripts = vec![];
    2831
    115
          let mut postscripts = vec![];
    2832
    115
          let mut i_postscript = i_base + 1;
    2833
    2834
    115
          if (base_name == "msub" || base_name == "msup"110 || base_name == "msubsup"110) &&
    2835
    5
             !CanonicalizeContext::is_empty_element(as_element(base.children()[0])) {
    2836
    5
            // if the base is a script element, then we want the base of that to be the base of the mmultiscripts
    2837
    5
            let mut base_children = base.children();
    2838
    5
            let script_base = as_element(base.children()[0]);
    2839
    5
            base_children[0] = ChildOfElement::Element(CanonicalizeContext::create_empty_element(&base.document()));
    2840
    5
            base.replace_children(base_children);
    2841
    5
            add_to_scripts(base, &mut postscripts);
    2842
    5
            base = script_base;
    2843
    110
          }
    2844
    2845
    115
          let mut has_chemistry_prescript = false; // chemical elements don't have both prescripts (nuclear chem) and postscripts
    2846
    115
          if i_base > i {
    2847
            // we have prescripts -- gather them up
    2848
    61
            let mut i_prescript = i;
    2849
    122
            while i_prescript < i_base {
    2850
    61
              let script = as_element(mrow_children[i_prescript]);
    2851
              // kind of ugly -- this duplicates the first part of add_to_scripts
    2852
    61
              let script_name = name(script);
    2853
    61
              if script_name == "msub" || script_name == "msup"56 || script_name == "msubsup"48 {
    2854
    61
                let base = as_element(script.children()[0]);
    2855
    61
                has_chemistry_prescript |= base.attribute(MHCHEM_MMULTISCRIPTS_HACK).is_some();
    2856
    61
              }0
    2857
    61
              if !add_to_scripts(script, &mut prescripts) {
    2858
    0
                break;
    2859
    61
              }
    2860
    61
              i_prescript += 1;
    2861
            }
    2862
    54
          }
    2863
    2864
    115
          if !has_chemistry_prescript {
    2865
            // gather up the postscripts (if any)
    2866
    137
            while i_postscript < mrow_children.len() {
    2867
    104
              let script = as_element(mrow_children[i_postscript]);
    2868
              // debug!("script: {}", mml_to_string(script));
    2869
              // if name(script) == "msub" && i_postscript+1 < mrow_children.len() {
    2870
              //  let superscript = as_element(mrow_children[i_postscript+1]);
    2871
              //  if name(superscript) == "msup" && CanonicalizeContext::is_empty_element(as_element(superscript.children()[0])) {
    2872
              //    set_mathml_name(script, "msubsup");
    2873
              //    script.append_child(superscript.children()[1]);
    2874
              //    i_postscript += 1;
    2875
              //  }
    2876
              // }
    2877
              // debug!("adding postscript\n{}", mml_to_string(script));
    2878
    104
              if !add_to_scripts(script, &mut postscripts) {
    2879
    32
                break;
    2880
    72
              }
    2881
    72
              i_postscript += 1;
    2882
            }
    2883
    50
          }
    2884
    2885
    115
          let i_multiscript = if i_base < i {i_base54} else {i61};
    2886
    115
          let script = create_mathml_element(&base.document(), "mmultiscripts");
    2887
    115
          let mut num_children = 1 + postscripts.len();
    2888
    115
          if !prescripts.is_empty() {
    2889
    61
            num_children += 1 + prescripts.len();
    2890
    61
          }54
    2891
    115
          let mut new_children = Vec::with_capacity(num_children);
    2892
    115
          new_children.push(ChildOfElement::Element(base));
    2893
    115
          new_children.append(&mut postscripts);
    2894
    115
          if !prescripts.is_empty() {
    2895
    61
            new_children.push( ChildOfElement::Element( create_mathml_element(&script.document(), "mprescripts") ) );
    2896
    61
            new_children.append(&mut prescripts);
    2897
    61
          }54
    2898
    2899
    115
          script.replace_children(new_children);
    2900
    115
          let lifted_base = as_element(mrow_children[i_multiscript]);
    2901
    115
          add_attrs(script, &lifted_base.attributes());
    2902
    115
          script.remove_attribute("data-split");   // doesn't make sense on mmultiscripts
    2903
    115
          script.remove_attribute("mathvariant");    // doesn't make sense on mmultiscripts
    2904
    115
          mrow_children[i_multiscript] = ChildOfElement::Element(script);
    2905
    115
          mrow_children.drain(i_multiscript+1..i_postscript);  // remove children after the first
    2906
    2907
    115
          let likely_chemistry = likely_adorned_chem_formula(script);
    2908
    115
          if likely_chemistry >= 0 {
    2909
    106
            script.set_attribute_value(MAYBE_CHEMISTRY, likely_chemistry.to_string().as_str());
    2910
    106
          }9
    2911
    2912
          // debug!("convert_to_mmultiscripts -- converted script:\n{}", mml_to_string(script));
    2913
          // debug!("convert_to_mmultiscripts (at end) -- #children={}", mrow_children.len());
    2914
    115
          return i_multiscript + 1;   // child to start on next
    2915
    115
        }
    2916
    2917
    170
        fn add_to_scripts<'a>(el: Element<'a>, scripts: &mut Vec<ChildOfElement<'a>>) -> bool {
    2918
    170
          let script_name = name(el);
    2919
    170
          if !(script_name == "msub" || script_name == "msup"111 || script_name == "msubsup"80) {
    2920
    32
            return false;
    2921
    138
          }
    2922
    138
          let base = as_element(el.children()[0]);
    2923
    138
          if !CanonicalizeContext::is_empty_element(base) { // prescript that really should be a postscript
    2924
            // debug!("add_to_scripts: not empty base:\n{}", mml_to_string(base));
    2925
    0
            return false;
    2926
    138
          }
    2927
    138
          if script_name == "msub" {
    2928
    59
            add_pair(scripts, Some(el.children()[1]), None);
    2929
    79
          } else if script_name == "msup" {
    2930
    31
            add_pair(scripts, None, Some(el.children()[1]));
    2931
    48
          } else { // msubsup
    2932
    48
            add_pair(scripts, Some(el.children()[1]), Some(el.children()[2]));
    2933
    48
          };
    2934
    138
          return true;
    2935
    170
        }
    2936
    2937
    138
        fn add_pair<'v, 'a:'v>(script_vec: &'v mut Vec<ChildOfElement<'a>>, subscript: Option<ChildOfElement<'a>>, superscript: Option<ChildOfElement<'a>>) {
    2938
    138
          let child_of_element = if let Some(subscript107) = subscript {subscript107} else {superscript31.unwrap31()};
    2939
    138
          let doc = as_element(child_of_element).document();
    2940
    138
          let subscript = if let Some(subscript107)= subscript {
    2941
    107
            if CanonicalizeContext::is_empty_element(as_element(subscript)) {
    2942
    0
              ChildOfElement::Element(create_mathml_element(&doc, "none"))
    2943
            } else {
    2944
    107
              subscript
    2945
            }
    2946
          } else {
    2947
    31
            ChildOfElement::Element(create_mathml_element(&doc, "none"))
    2948
          };
    2949
    138
          let superscript = if let Some(superscript79) = superscript {
    2950
    79
            if CanonicalizeContext::is_empty_element(as_element(superscript)) {
    2951
    0
              ChildOfElement::Element(create_mathml_element(&doc, "none"))
    2952
            } else {
    2953
    79
              superscript
    2954
            }
    2955
          } else {
    2956
    59
            ChildOfElement::Element(create_mathml_element(&doc, "none"))
    2957
          };
    2958
    138
          script_vec.push(subscript);
    2959
    138
          script_vec.push(superscript);
    2960
    138
        }
    2961
    2962
        /// Find the closest likely base to the 'i'th child, preferring the next one over the preceding one, but want the closest.
    2963
        ///
    2964
        /// Note: because the base might be (...), 'mrow_children might be changed so that they are grouped into an mrow.
    2965
    115
        fn choose_base_of_mmultiscripts(mrow_children: &mut Vec<ChildOfElement>, i: usize) -> usize {
    2966
          // We already know there are no empty scripts to the left (because we find first empty base from left to right).
    2967
          // However, there may be some empty bases before we get to real base on the right.
    2968
    115
          let script_element_base = as_element(as_element(mrow_children[i]).children()[0]);
    2969
    115
          let mut likely_postscript = script_element_base.attribute(MHCHEM_MMULTISCRIPTS_HACK).is_some() && i > 0103;
    2970
    115
          if likely_postscript {
    2971
    86
            let base_of_postscript = as_element(mrow_children[i-1]);
    2972
    86
            if name(base_of_postscript) != "mi" || likely_chem_element(base_of_postscript) < 050 {
    2973
    36
              likely_postscript = false;  // base for potential postscript doesn't look reasonable -- consider it a prescript
    2974
    50
            }
    2975
    29
          }
    2976
    115
          if i+1 < mrow_children.len() && !likely_postscript107 && is_child_simple_base61(mrow_children[i+1]61) {
    2977
    61
            return i+1;
    2978
    54
          }
    2979
    54
          if i > 0 {
    2980
    54
            if let Some(i_start2) = is_grouped_base(&mrow_children[..i]) {
    2981
    2
              assert!(i_start < i-1);  // should be at least two children (open and close)
    2982
              // create a new mrow, add the grouped children to it, then drain all but the first of them from the original mrow vec.
    2983
              // stick the mrow into the first of them -- this is the base
    2984
    2
              let new_mrow = create_mathml_element(&as_element(mrow_children[0]).document(), "mrow");
    2985
    2
              new_mrow.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    2986
    8
              for &child in &2mrow_children2[i_start..i] {
    2987
    8
                new_mrow.append_child(child);
    2988
    8
              }
    2989
    2
              mrow_children.drain(i_start+1..i);
    2990
    2
              mrow_children[i_start] = ChildOfElement::Element(new_mrow);
    2991
    2
              return i_start;
    2992
    52
            }
    2993
    52
            if is_child_simple_base(mrow_children[i-1]) {
    2994
    52
              return i-1;
    2995
    0
            }
    2996
    0
          }
    2997
    2998
          // base very likely after multiple scripts to the right
    2999
    0
          for (i_base, &child) in mrow_children.iter().enumerate().skip(i+1) {
    3000
    0
            if is_child_simple_base(child) {
    3001
    0
                return i_base;
    3002
            } else {
    3003
    0
              let child = as_element(child);
    3004
    0
              let child_name = name(child);
    3005
    0
              if !(child_name == "msub" || child_name == "msup" || child_name == "msubsup") {
    3006
    0
                break;
    3007
    0
              }
    3008
            }
    3009
          }
    3010
          // didn't find any good candidates for a base -- pick something valid
    3011
    0
          assert!(mrow_children.len() > i);
    3012
    0
          return i;
    3013
    3014
    3015
    113
          fn is_child_simple_base(child: ChildOfElement) -> bool {
    3016
    113
            let mut child = as_element(child);
    3017
    113
            let child_name = name(child);
    3018
    113
            if child_name == "msub" || child_name == "msup"108 || child_name == "msubsup"108 {
    3019
    5
              child = as_element(child.children()[0]);
    3020
    108
            }
    3021
    3022
    113
            return is_leaf(child) && !CanonicalizeContext::is_empty_element(child);  // a little overly general (but hopefully doesn't matter)
    3023
    113
          }
    3024
    3025
          /// Return the index of the matched open paren/bracket if the last element is a closed paren/bracket
    3026
    54
          fn is_grouped_base(mrow_children: &[ChildOfElement]) -> Option<usize> {
    3027
            // FIX: this really belongs in canonicalization pass, not the clean pass
    3028
    54
            let i_last = mrow_children.len()-1;
    3029
    54
            let last_child = get_possible_embellished_node(as_element(mrow_children[i_last]));
    3030
    54
            if name(last_child) == "mo" &&
    3031
    3
               CanonicalizeContext::find_operator(None, last_child, None, None, None).is_right_fence() {
    3032
    6
              for i_child in (0..i_last2).rev2() {
    3033
    6
                let child = get_possible_embellished_node(as_element(mrow_children[i_child]));
    3034
    6
                if name(child) == "mo" &&
    3035
    2
                   CanonicalizeContext::find_operator(None, child, None, None, None).is_left_fence() {
    3036
                  // FIX: should make sure left and right match. Should also count for nested parens
    3037
    2
                  return Some(i_child);
    3038
    4
                }
    3039
              }
    3040
    52
            }
    3041
    52
            return None;
    3042
    54
          }
    3043
    115
        }
    3044
    52.3k
      }
    3045
    3046
    64.1k
      fn canonicalize_mrows<'a>(&self, mathml: Element<'a>) -> Result<Element<'a>> {
    3047
    64.1k
        let tag_name = name(mathml);
    3048
    64.1k
        set_mathml_name(mathml, tag_name);  // add namespace
    3049
    64.1k
        match tag_name {
    3050
    64.1k
          "mi" | "ms"48.7k | "mtext"48.7k | "mspace"48.3k  => {
    3051
    15.8k
            self.canonicalize_plane1(mathml);
    3052
    15.8k
            return Ok( mathml ); },
    3053
    48.3k
          "mo" => {
    3054
    14.6k
            self.canonicalize_plane1(mathml);
    3055
    14.6k
            self.canonicalize_mo_text(mathml);
    3056
    14.6k
            return Ok( mathml );
    3057
          },
    3058
    33.7k
          "mn" => {
    3059
    11.6k
            self.canonicalize_plane1(mathml);
    3060
    11.6k
            return Ok( mathml );
    3061
          },
    3062
    22.0k
          "mrow" => {
    3063
    7.48k
            return self.canonicalize_mrows_in_mrow(mathml);
    3064
          },
    3065
          _ => {
    3066
            // recursively try to make mrows in other structures (eg, num/denom in fraction)
    3067
    14.6k
            let mut new_children = Vec::with_capacity(mathml.children().len());
    3068
    21.5k
            for child in mathml14.6k.children14.6k() {
    3069
    21.5k
              match child {
    3070
    21.5k
                ChildOfElement::Element(e) => {
    3071
    21.5k
                  new_children.push( ChildOfElement::Element(self.canonicalize_mrows(e)?0 ));
    3072
                },
    3073
    0
                ChildOfElement::Text(t) => {
    3074
    0
                  if mathml.children().len() != 1 {
    3075
    0
                    bail!("Text '{}' found with more than one child in element '{}'", t.text(), tag_name);
    3076
    0
                  }
    3077
    0
                  return Ok( mathml );
    3078
                },
    3079
    0
                _ => bail!("Should have been an element or text in '{}'", tag_name),
    3080
              }
    3081
            }
    3082
    14.6k
            mathml.replace_children(new_children);
    3083
    14.6k
            return Ok( mathml );
    3084
          },
    3085
        }
    3086
    64.1k
      }
    3087
    3088
    1.91k
      fn potentially_lift_script<'a>(&self, mrow: Element<'a>) -> Element<'a> {
    3089
    1.91k
        if name(mrow) != "mrow" {
    3090
    0
          return mrow;
    3091
    1.91k
        }
    3092
    1.91k
        let mut mrow_children = mrow.children();
    3093
    1.91k
        let first_child = as_element(mrow_children[0]);
    3094
    1.91k
        let last_child = as_element(mrow_children[mrow_children.len()-1]);
    3095
    1.91k
        let last_child_name = name(last_child);
    3096
    3097
    1.91k
        if name(first_child) == "mo" && is_fence1.91k(first_child1.91k) &&
    3098
    1.91k
           (last_child_name == "msub" || last_child_name == "msup" || last_child_name == "msubsup"1.89k) {
    3099
    19
          let base = as_element(last_child.children()[0]);
    3100
    19
          if !(name(base) == "mo" && is_fence(base)) {
    3101
    0
            return mrow; // not a case we are interested in
    3102
    19
          }
    3103
          // else drop through
    3104
        } else {
    3105
    1.89k
          return mrow; // not a case we are interested in
    3106
        }
    3107
    3108
    19
        let script = last_child; // better name now that we know what it is
    3109
    19
        let mut script_children = script.children();
    3110
    19
        let close_fence = script_children[0];
    3111
    19
        let mrow_children_len = mrow_children.len();     // rust complains about a borrow after move if we don't store this first
    3112
    19
        mrow_children[mrow_children_len-1] = close_fence;     // make the mrow hold the fences
    3113
    19
        mrow.replace_children(mrow_children);
    3114
        // make the mrow the child of the script
    3115
    19
        script_children[0] = ChildOfElement::Element(mrow);
    3116
    19
        script.replace_children(script_children);
    3117
    19
        return script;
    3118
    1.91k
      }
    3119
    3120
      /// Map names to start of Unicode alphanumeric blocks (Roman, digits, Greek)
    3121
      /// Don't do this for function names -- for function names, map them back to ASCII
    3122
    42.1k
      fn canonicalize_plane1<'a>(&self, mi: Element<'a>) -> Element<'a> {
    3123
        // if the character shouldn't be mapped, use 0 -- don't use 'A' as ASCII and Greek aren't contiguous
    3124
        static MATH_VARIANTS: phf::Map<&str, [u32; 3]> = phf_map! {
    3125
          // "normal" -- nothing to do
    3126
          "italic" => [0, 0, 0x1D6E2],
    3127
          "bold" => [0x1D400, 0x1D7CE, 0x1D6A8],
    3128
          "bold-italic" => [0x1D468, 0x1D7CE, 0x1D71C],
    3129
          "double-struck" => [0x1D538, 0x1D7D8, 0],
    3130
          "bold-fraktur" => [0x1D56C, 0, 0x1D6A8],
    3131
          "script" => [0x1D49C, 0, 0],
    3132
          "bold-script" => [0x1D4D0, 0, 0x1D6A8],
    3133
          "fraktur" => [0x1D504, 0, 0],
    3134
          "sans-serif" => [0x1D5A0, 0x1D7E2, 0],
    3135
          "bold-sans-serif" => [0x1D5D4, 0x1D7EC, 0x1D756],
    3136
          "sans-serif-italic" => [0x1D608, 0x1D7E2, 0],
    3137
          "sans-serif-bold-italic" => [0x1D63C, 0x1D7EC, 0x1D790],
    3138
          "monospace" => [0x1D670, 0x1D7F6, 0],
    3139
        };
    3140
    3141
    42.1k
        return crate::definitions::SPEECH_DEFINITIONS.with(|defs| {
    3142
          // names that are always function names (e.g, "sin" and "log")
    3143
    42.1k
          let defs = defs.borrow();
    3144
    42.1k
          let names42.1k = match defs.get_hashset("FunctionNames") {
    3145
    42.1k
            Some(hs) => hs,
    3146
    3
            None => return mi,  // happens in some canonicalize tests but not in real use
    3147
          };
    3148
    3149
    3150
    42.1k
          let mi_text = as_text(mi);
    3151
    42.1k
          let variant = mi.attribute_value("mathvariant");
    3152
    3153
    42.1k
          if names.contains(mi_text) {
    3154
    791
            return mi;   // avoid mapping mathvariant for function names
    3155
    41.3k
          }
    3156
          // function name might be (wrongly) set to italic math alphanumeric chars, including bold italic
    3157
    41.3k
          if let Some(ascii_text12.6k) = CanonicalizeContext::math_alphanumeric_to_ascii(mi_text)
    3158
    12.6k
            && names.contains(&ascii_text) {
    3159
    3
              mi.set_text(&ascii_text);
    3160
    3
              return mi
    3161
    41.3k
            }
    3162
    3163
    41.3k
          if variant.is_none() {
    3164
    40.3k
            return mi;
    3165
    952
          }
    3166
    3167
    952
          let new_text = match MATH_VARIANTS.get(variant.unwrap()) {
    3168
    755
            None => mi_text.to_string(),
    3169
    197
            Some(start) => shift_text(mi_text, start),
    3170
          };
    3171
          // mi.remove_attribute("mathvariant");  // leave attr -- for Nemeth, there are italic digits etc that don't have Unicode points
    3172
    952
          mi.set_text(&new_text);
    3173
    952
          return mi;
    3174
    42.1k
        });
    3175
    3176
    197
        fn shift_text(old_text: &str, char_mapping: &[u32; 3]) -> String {
    3177
          // if there is no block for something, use 'a', 'A', 0 as that will be a no-op
    3178
          struct Offsets {
    3179
            ch: u32,
    3180
            table: usize,
    3181
          }
    3182
          static SHIFT_AMOUNTS: phf::Map<char, Offsets> = phf_map! {
    3183
            'A' => Offsets{ ch: 0, table: 0},
    3184
            'B' => Offsets{ ch: 1, table: 0},
    3185
            'C' => Offsets{ ch: 2, table: 0},
    3186
            'D' => Offsets{ ch: 3, table: 0},
    3187
            'E' => Offsets{ ch: 4, table: 0},
    3188
            'F' => Offsets{ ch: 5, table: 0},
    3189
            'G' => Offsets{ ch: 6, table: 0},
    3190
            'H' => Offsets{ ch: 7, table: 0},
    3191
            'I' => Offsets{ ch: 8, table: 0},
    3192
            'J' => Offsets{ ch: 9, table: 0},
    3193
            'K' => Offsets{ ch: 10, table: 0},
    3194
            'L' => Offsets{ ch: 11, table: 0},
    3195
            'M' => Offsets{ ch: 12, table: 0},
    3196
            'N' => Offsets{ ch: 13, table: 0},
    3197
            'O' => Offsets{ ch: 14, table: 0},
    3198
            'P' => Offsets{ ch: 15, table: 0},
    3199
            'Q' => Offsets{ ch: 16, table: 0},
    3200
            'R' => Offsets{ ch: 17, table: 0},
    3201
            'S' => Offsets{ ch: 18, table: 0},
    3202
            'T' => Offsets{ ch: 19, table: 0},
    3203
            'U' => Offsets{ ch: 20, table: 0},
    3204
            'V' => Offsets{ ch: 21, table: 0},
    3205
            'W' => Offsets{ ch: 22, table: 0},
    3206
            'X' => Offsets{ ch: 23, table: 0},
    3207
            'Y' => Offsets{ ch: 24, table: 0},
    3208
            'Z' => Offsets{ ch: 25, table: 0},
    3209
            'a' => Offsets{ ch: 26, table: 0},
    3210
            'b' => Offsets{ ch: 27, table: 0},
    3211
            'c' => Offsets{ ch: 28, table: 0},
    3212
            'd' => Offsets{ ch: 29, table: 0},
    3213
            'e' => Offsets{ ch: 30, table: 0},
    3214
            'f' => Offsets{ ch: 31, table: 0},
    3215
            'g' => Offsets{ ch: 32, table: 0},
    3216
            'h' => Offsets{ ch: 33, table: 0},
    3217
            'i' => Offsets{ ch: 34, table: 0},
    3218
            'j' => Offsets{ ch: 35, table: 0},
    3219
            'k' => Offsets{ ch: 36, table: 0},
    3220
            'l' => Offsets{ ch: 37, table: 0},
    3221
            'm' => Offsets{ ch: 38, table: 0},
    3222
            'n' => Offsets{ ch: 39, table: 0},
    3223
            'o' => Offsets{ ch: 40, table: 0},
    3224
            'p' => Offsets{ ch: 41, table: 0},
    3225
            'q' => Offsets{ ch: 42, table: 0},
    3226
            'r' => Offsets{ ch: 43, table: 0},
    3227
            's' => Offsets{ ch: 44, table: 0},
    3228
            't' => Offsets{ ch: 45, table: 0},
    3229
            'u' => Offsets{ ch: 46, table: 0},
    3230
            'v' => Offsets{ ch: 47, table: 0},
    3231
            'w' => Offsets{ ch: 48, table: 0},
    3232
            'x' => Offsets{ ch: 49, table: 0},
    3233
            'y' => Offsets{ ch: 50, table: 0},
    3234
            'z' => Offsets{ ch: 51, table: 0},
    3235
            '0' => Offsets{ ch: 0, table: 1},
    3236
            '1' => Offsets{ ch: 1, table: 1},
    3237
            '2' => Offsets{ ch: 2, table: 1},
    3238
            '3' => Offsets{ ch: 3, table: 1},
    3239
            '4' => Offsets{ ch: 4, table: 1},
    3240
            '5' => Offsets{ ch: 5, table: 1},
    3241
            '6' => Offsets{ ch: 6, table: 1},
    3242
            '7' => Offsets{ ch: 7, table: 1},
    3243
            '8' => Offsets{ ch: 8, table: 1},
    3244
            '9' => Offsets{ ch: 9, table: 1},
    3245
            'Α' => Offsets{ ch: 0, table: 2},
    3246
            'Β' => Offsets{ ch: 1, table: 2},
    3247
            'Γ' => Offsets{ ch: 2, table: 2},
    3248
            'Δ' => Offsets{ ch: 3, table: 2},
    3249
            'Ε' => Offsets{ ch: 4, table: 2},
    3250
            'Ζ' => Offsets{ ch: 5, table: 2},
    3251
            'Η' => Offsets{ ch: 6, table: 2},
    3252
            'Θ' => Offsets{ ch: 7, table: 2},
    3253
            'Ι' => Offsets{ ch: 8, table: 2},
    3254
            'Κ' => Offsets{ ch: 9, table: 2},
    3255
            'Λ' => Offsets{ ch: 10, table: 2},
    3256
            'Μ' => Offsets{ ch: 11, table: 2},
    3257
            'Ν' => Offsets{ ch: 12, table: 2},
    3258
            'Ξ' => Offsets{ ch: 13, table: 2},
    3259
            'Ο' => Offsets{ ch: 14, table: 2},
    3260
            'Π' => Offsets{ ch: 15, table: 2},
    3261
            'Ρ' => Offsets{ ch: 16, table: 2},
    3262
            'ϴ' => Offsets{ ch: 17, table: 2},
    3263
            'Σ' => Offsets{ ch: 18, table: 2},
    3264
            'Τ' => Offsets{ ch: 19, table: 2},
    3265
            'Υ' => Offsets{ ch: 20, table: 2},
    3266
            'Φ' => Offsets{ ch: 21, table: 2},
    3267
            'Χ' => Offsets{ ch: 22, table: 2},
    3268
            'Ψ' => Offsets{ ch: 23, table: 2},
    3269
            'Ω' => Offsets{ ch: 24, table: 2},
    3270
            '∇' => Offsets{ ch: 25, table: 2},
    3271
            'α' => Offsets{ ch: 26, table: 2},
    3272
            'β' => Offsets{ ch: 27, table: 2},
    3273
            'γ' => Offsets{ ch: 28, table: 2},
    3274
            'δ' => Offsets{ ch: 29, table: 2},
    3275
            'ε' => Offsets{ ch: 30, table: 2},
    3276
            'ζ' => Offsets{ ch: 31, table: 2},
    3277
            'η' => Offsets{ ch: 32, table: 2},
    3278
            'θ' => Offsets{ ch: 33, table: 2},
    3279
            'ι' => Offsets{ ch: 34, table: 2},
    3280
            'κ' => Offsets{ ch: 35, table: 2},
    3281
            'λ' => Offsets{ ch: 36, table: 2},
    3282
            'μ' => Offsets{ ch: 37, table: 2},
    3283
            'ν' => Offsets{ ch: 38, table: 2},
    3284
            'ξ' => Offsets{ ch: 39, table: 2},
    3285
            'ο' => Offsets{ ch: 40, table: 2},
    3286
            'π' => Offsets{ ch: 41, table: 2},
    3287
            'ρ' => Offsets{ ch: 42, table: 2},
    3288
            'ς' => Offsets{ ch: 43, table: 2},
    3289
            'σ' => Offsets{ ch: 44, table: 2},
    3290
            'τ' => Offsets{ ch: 45, table: 2},
    3291
            'υ' => Offsets{ ch: 46, table: 2},
    3292
            'φ' => Offsets{ ch: 47, table: 2},
    3293
            'χ' => Offsets{ ch: 48, table: 2},
    3294
            'ψ' => Offsets{ ch: 49, table: 2},
    3295
            'ω' => Offsets{ ch: 50, table: 2},
    3296
            '∂' => Offsets{ ch: 51, table: 2},
    3297
            'ϵ' => Offsets{ ch: 52, table: 2},
    3298
            'ϑ' => Offsets{ ch: 53, table: 2},
    3299
            'ϰ' => Offsets{ ch: 54, table: 2},
    3300
            'ϕ' => Offsets{ ch: 55, table: 2},
    3301
            'ϱ' => Offsets{ ch: 56, table: 2},
    3302
            'ϖ' => Offsets{ ch: 57, table: 2},
    3303
          };
    3304
    197
          let mut new_text = String::new();
    3305
    321
          for ch in old_text197.chars197() {
    3306
    321
            new_text.push(
    3307
    321
              match SHIFT_AMOUNTS.get(&ch) {
    3308
                None => {
    3309
                  // there are two digamma chars only in the bold mapping. Handled here
    3310
    71
                  if char_mapping[2] == 0x1D6A8 {
    3311
    43
                    match ch {
    3312
    1
                      'Ϝ' => '𝟊',
    3313
    1
                      'ϝ' => '𝟋',
    3314
    41
                      _   => ch,
    3315
                    }
    3316
                  } else {
    3317
    28
                    ch
    3318
                  }
    3319
                },
    3320
    250
                Some(offsets) => {
    3321
    250
                  let start_of_mapping = char_mapping[offsets.table];
    3322
    250
                  if start_of_mapping == 0 {ch37} else {shift_char213(start_of_mapping + offsets.ch213)}
    3323
                }
    3324
              }
    3325
            )
    3326
          }
    3327
    197
          return new_text;
    3328
    3329
    213
          fn shift_char(ch: u32) -> char {
    3330
            // there are "holes" in the math alphanumerics due to legacy issues
    3331
            // this table maps the holes to their legacy location
    3332
            static EXCEPTIONS: phf::Map<u32, u32> = phf_map! {
    3333
              0x1D455u32 => 0x210Eu32,
    3334
              0x1D49Du32 => 0x212Cu32,
    3335
              0x1D4A0u32 => 0x2130u32,
    3336
              0x1D4A1u32 => 0x2131u32,
    3337
              0x1D4A3u32 => 0x210Bu32,
    3338
              0x1D4A4u32 => 0x2110u32,
    3339
              0x1D4A7u32 => 0x2112u32,
    3340
              0x1D4A8u32 => 0x2133u32,
    3341
              0x1D4ADu32 => 0x211Bu32,
    3342
              0x1D4BAu32 => 0x212Fu32,
    3343
              0x1D4BCu32 => 0x210Au32,
    3344
              0x1D4C4u32 => 0x2134u32,
    3345
              0x1D506u32 => 0x212Du32,
    3346
              0x1D50Bu32 => 0x210Cu32,
    3347
              0x1D50Cu32 => 0x2111u32,
    3348
              0x1D515u32 => 0x211Cu32,
    3349
              0x1D51Du32 => 0x2128u32,
    3350
              0x1D53Au32 => 0x2102u32,
    3351
              0x1D53Fu32 => 0x210Du32,
    3352
              0x1D545u32 => 0x2115u32,
    3353
              0x1D547u32 => 0x2119u32,
    3354
              0x1D548u32 => 0x211Au32,
    3355
              0x1D549u32 => 0x211Du32,
    3356
              0x1D551u32 => 0x2124u32,
    3357
            };
    3358
    3359
    213
            return unsafe { char::from_u32_unchecked(   // safe because the values are a char or from the table above
    3360
    213
              match EXCEPTIONS.get(&ch) {
    3361
    161
                None => ch,
    3362
    52
                Some(exception_value) => *exception_value,
    3363
              }
    3364
            ) }
    3365
    213
          }
    3366
    197
        }
    3367
    42.1k
      }
    3368
    3369
    41.5k
      fn math_alphanumeric_to_ascii(input: &str) -> Option<String> {
    3370
    41.5k
        let mut result = String::with_capacity(input.len());
    3371
    3372
    46.6k
        for c in input41.5k.chars41.5k() {
    3373
    46.6k
          let converted18.0k = match c {
    3374
            // Standard ASCII
    3375
    18.4k
            'a'..='z' | 'A'..='Z'11.2k => c17.8k,
    3376
    3377
            // Mathematical Bold (A-Z: U+1D400, a-z: U+1D41A)
    3378
    482
            '\u{1D400}'..='\u{1D419}' => ((c as u32 - 0x1D400) as u8 + b'A') as char22,
    3379
    460
            '\u{1D41A}'..='\u{1D433}' => ((c as u32 - 0x1D41A) as u8 + b'a') as char36,
    3380
    3381
            // Mathematical Italic (A-Z: U+1D434, a-z: U+1D44E)
    3382
            // Note: 'h' is missing from this range (U+210E)
    3383
    424
            '\u{1D434}'..='\u{1D44D}' => ((c as u32 - 0x1D434) as u8 + b'A') as char10,
    3384
    414
            '\u{1D44E}'..='\u{1D467}' => ((c as u32 - 0x1D44E) as u8 + b'a') as char14,
    3385
    3386
            // Mathematical Bold Italic (A-Z: U+1D468, a-z: U+1D482)
    3387
    400
            '\u{1D468}'..='\u{1D481}' => ((c as u32 - 0x1D468) as u8 + b'A') as char0,
    3388
    400
            '\u{1D482}'..='\u{1D49B}' => ((c as u32 - 0x1D482) as u8 + b'a') as char14,
    3389
    3390
            // Mathematical Sans-Serif (A-Z: U+1D5A0, a-z: U+1D5BA)
    3391
    274
            '\u{1D5A0}'..='\u{1D5B9}' => ((c as u32 - 0x1D5A0) as u8 + b'A') as char10,
    3392
    264
            '\u{1D5BA}'..='\u{1D5D3}' => ((c as u32 - 0x1D5BA) as u8 + b'a') as char11,
    3393
    3394
            // If a character isn't a letter (or supported math letter), return None
    3395
    28.6k
            _ => return None,
    3396
          };
    3397
    18.0k
          result.push(converted);
    3398
        }
    3399
    3400
    12.8k
        Some(result)
    3401
    41.5k
      }
    3402
    3403
    14.6k
      fn canonicalize_mo_text(&self, mo: Element) {
    3404
        // lazy_static! {    (NOTE: std::sync::LazyLock is now used instead)
    3405
        //  static ref IS_LIKELY_SCALAR_VARIABLE: Regex = Regex::new("[a-eh-z]").unwrap();
    3406
        // }
    3407
    3408
    14.6k
        let mut mo_text = as_text(mo);
    3409
    14.6k
        let parent = get_parent(mo);
    3410
    14.6k
        let parent_name = name(parent);
    3411
    14.6k
        let is_base = mo.preceding_siblings().is_empty();
    3412
    14.6k
        if !is_base && (parent_name == "mover"1.38k || parent_name == "munder"1.09k || parent_name == "munderover"1.07k) {
    3413
          // canonicalize various diacritics for munder, mover, munderover
    3414
    309
          mo_text = match mo_text {
    3415
    309
            "_" | "\u{02C9}"303| "\u{0304}"303| "\u{0305}"303| "\u{332}"303 | "\u{2212}"302 |
    3416
    302
            "\u{2010}" | "\u{2011}" | "\u{2012}" | "\u{2013}" | "\u{2014}" | "\u{2015}" | "\u{203e}"293 => "\u{00AF}"17,
    3417
    292
            "\u{02BC}" => "`"0,
    3418
    292
            "\u{02DC}" | "\u{223C}" => "~"0,   // use ASCII for diacriticals
    3419
    292
            "\u{02C6}"| "\u{0302}" => "^"0,
    3420
    292
            "\u{0307}" => "\u{02D9}"0, // Nemeth distinguishes this from "." -- \u{02D9} is generated for over dots by most generators
    3421
    292
            "\u{0308}" => "¨"0,
    3422
    292
            _ => mo_text,
    3423
          }
    3424
          // FIX: MathType generates the wrong version of union and intersection ops (binary instead of unary)
    3425
    14.3k
        } else if !is_base && (parent_name == "msup"1.07k || parent_name == "msubsup"858) {
    3426
    227
          mo_text = match mo_text {
    3427
    227
            "\u{00BA}"| "\u{2092}"| "\u{20D8}"| "\u{2218}" | "\u{25E6}"223 => "\u{00B0}"4,   // circle-like objects -> degree
    3428
    223
            _ => mo_text,
    3429
          };
    3430
        } else {
    3431
    14.0k
          mo_text = match mo_text {
    3432
    14.0k
            "\u{02C9}"| "\u{0304}"| "\u{0305}" => "\u{00AF}"0,
    3433
    14.0k
            "\u{02DC}" | "~"  => "\u{223C}"5,   // for base, use version with prefix and infix
    3434
    14.0k
            "\u{01C1}" => "\u{2016}"0, // U+2016 is "‖"
    3435
    3436
    14.0k
            _ => mo_text,
    3437
          };
    3438
        };
    3439
    14.6k
        if mo_text == "\u{2212}" {
    3440
    314
          mo_text = "-";
    3441
    14.2k
        }
    3442
    14.6k
        mo.set_text(mo_text);
    3443
    14.6k
      }
    3444
    3445
    3446
      // Find the operator associated with the 'mo_node'
    3447
      // This is complicated by potentially needing to distinguish between the
    3448
      //   prefix, infix, or postfix version of the operator.
    3449
      // To figure out prefix, we need to look at the node on the left; for postfix, we need to look to the left
    3450
      // If the node of the left has been parsed, then this works.
    3451
      // For example, suppose we want to determine if the "+" in 'x < n!+1' is prefix or infix.
    3452
      //   If we simply looked left without parsing, we'd see an operator and choose prefix unless we could figure out that
    3453
      //   that "!" was postfix.  But if it had been parsed, we'd see an mrow (operand) and tree "+" as infix (as it should).
    3454
      // The same problem applies on the right for postfix operators, but a problem is rare for those
    3455
      //   e.g., n!!n -- ((n!)!)*n or (n!)*(!n)  -- the latter doesn't make semantic sense though
    3456
      // FIX:  the above ignores mspace and other nodes that need to be skipped to determine the right node to determine airity
    3457
      // FIX:  the postfix problem above should be addressed
    3458
    19.4k
      fn find_operator<'a>(context: Option<&CanonicalizeContext>, mo_node: Element<'a>, previous_operator: Option<&'static OperatorInfo>,
    3459
    19.4k
                previous_node: Option<Element<'a>>, next_node: Option<Element<'a>>) -> &'static OperatorInfo {
    3460
        // get the unicode value and return the OpKeyword associated with it
    3461
    19.4k
        assert!( name(mo_node) == "mo");
    3462
    3463
        // if a form has been given, that takes precedence
    3464
    19.4k
        let form = mo_node.attribute_value("form");
    3465
    19.4k
        let op_type =  match form {
    3466
    19.4k
          None => match context {
    3467
    5.50k
            None => OperatorTypes::POSTFIX,   // what compute_type_from_position returns when the other args to this are all None
    3468
    13.9k
            Some(context) => compute_type_from_position(context, previous_operator, previous_node, next_node),
    3469
          },
    3470
    10
          Some(form) => match form.to_lowercase().as_str() {
    3471
    10
            "prefix" => OperatorTypes::PREFIX4,
    3472
    6
            "postfix" => OperatorTypes::POSTFIX2,
    3473
    4
            _ => OperatorTypes::INFIX,
    3474
          }
    3475
        };
    3476
    3477
    19.4k
        let found_op_info = if mo_node.attribute_value(CHEMICAL_BOND).is_some() {
    3478
    112
          Some(&IMPLIED_CHEMICAL_BOND)
    3479
        } else {
    3480
    19.3k
          OPERATORS.get(as_text(mo_node))
    3481
        };
    3482
    19.4k
        if found_op_info.is_none() {
    3483
          // no known operator -- return the unknown operator with the correct "fix" type
    3484
    49
          return op_not_in_operator_dictionary(op_type);
    3485
    19.4k
        }
    3486
    3487
    19.4k
        let found_op_info = found_op_info.unwrap();
    3488
    19.4k
        let matching_op_info = find_operator_info(found_op_info, op_type, form.is_some());
    3489
    19.4k
        if ptr_eq(matching_op_info, &ILLEGAL_OPERATOR_INFO) {
    3490
    0
          return op_not_in_operator_dictionary(op_type);
    3491
        } else {
    3492
    19.4k
          return matching_op_info;
    3493
        }
    3494
    3495
    3496
    13.9k
        fn compute_type_from_position<'a>(context: &CanonicalizeContext, previous_operator: Option<&'static OperatorInfo>, previous_node: Option<Element<'a>>, next_node: Option<Element<'a>>) -> OperatorTypes {
    3497
          // based on choices, pick one that fits the context
    3498
          // if there isn't an obvious one, we have parsed the left, but not the right, so discount that
    3499
    3500
          // Trig functions have some special syntax
    3501
          // We need to treat '-' as prefix for things like "sin -2x"
    3502
          // Need to be careful because (sin - cos)(x) needs an infix '-'
    3503
          // Return either the prefix or infix version of the operator
    3504
    13.9k
          if next_node.is_some() &&
    3505
    11.9k
             context.is_function_name(get_possible_embellished_node(next_node.unwrap()), None) == FunctionNameCertainty::True {
    3506
    260
            return OperatorTypes::INFIX;
    3507
    13.6k
          }
    3508
    13.6k
          if previous_node.is_some() &&
    3509
    11.1k
             context.is_function_name(get_possible_embellished_node(previous_node.unwrap()), None) == FunctionNameCertainty::True {
    3510
    207
            return OperatorTypes::PREFIX;
    3511
    13.4k
          }
    3512
    3513
          // after that special case, start with the obvious cases...
    3514
    13.4k
          let operand_on_left = previous_operator.is_none() || previous_operator.unwrap()2.94k.is_postfix2.94k(); // operand or postfix operator
    3515
    13.4k
          let operand_on_right = next_node.is_some() && name11.5k(get_possible_embellished_node11.5k(next_node.unwrap())) !="mo";     // FIX:  could improve by checking if it is a prefix op
    3516
    3517
    13.4k
          if operand_on_left && operand_on_right10.5k {
    3518
    8.19k
            return OperatorTypes::INFIX; // infix
    3519
    5.29k
          } else if !operand_on_left && operand_on_right2.94k {
    3520
    2.75k
            return OperatorTypes::PREFIX; // prefix
    3521
    2.54k
          } else if operand_on_left && !operand_on_right2.34k {
    3522
    2.34k
            return OperatorTypes::POSTFIX; // postfix
    3523
          } else {
    3524
            // either two operators in a row or right hand side not parsed so we don't really know what is right (same is true above)
    3525
            // since there is nothing good to return, assume right is an operand after parsing (thus infix case)
    3526
    196
            return OperatorTypes::INFIX;
    3527
          }
    3528
    13.9k
        }
    3529
    3530
    19.4k
        fn find_operator_info(op_info: &OperatorInfo, op_type: OperatorTypes, from_form_attr: bool) -> &OperatorInfo {
    3531
    19.4k
          if op_info.is_operator_type(op_type) {
    3532
    12.9k
            return op_info;
    3533
    6.45k
          } else if let Some(next_op_info1.64k) = op_info.next {
    3534
    1.64k
            if next_op_info.is_operator_type(op_type) {
    3535
    730
              return next_op_info;
    3536
    915
            } else if let Some(last_op_info256) = next_op_info.next
    3537
    256
              && last_op_info.is_operator_type(op_type) {
    3538
    256
                return last_op_info;
    3539
    659
              }
    3540
    4.81k
          }
    3541
    3542
          // didn't find op_info that matches -- if type is not forced, then return first value (any is probably ok)
    3543
    5.47k
          return if from_form_attr {&ILLEGAL_OPERATOR_INFO0} else {op_info};
    3544
    19.4k
        }
    3545
    3546
    49
        fn op_not_in_operator_dictionary(op_type: OperatorTypes) -> &'static OperatorInfo {
    3547
    49
          return match op_type {
    3548
    16
            OperatorTypes::PREFIX => &DEFAULT_OPERATOR_INFO_PREFIX,
    3549
    9
            OperatorTypes::POSTFIX => &DEFAULT_OPERATOR_INFO_POSTFIX,
    3550
    24
            _ => &DEFAULT_OPERATOR_INFO_INFIX, // should only be infix
    3551
          };
    3552
    49
        }
    3553
    19.4k
      }
    3554
    3555
    13.9k
      fn n_vertical_bars_on_right(&self, remaining_children: &[ChildOfElement], vert_bar_ch: &str) -> usize {
    3556
        // return the number of children that match 'vert_bar_op' not counting the first element
    3557
    13.9k
        let mut n = 0;
    3558
    149k
        for child_of_element in remaining_children13.9k {
    3559
    149k
          let child = as_element(*child_of_element);
    3560
    149k
          if name(child) == "mo" {
    3561
    49.9k
            let operator_str = as_text(child);
    3562
    49.9k
            if operator_str == vert_bar_ch {
    3563
    42.7k
              n += 1;
    3564
    42.7k
            }7.25k
    3565
    99.9k
          }
    3566
        }
    3567
    13.9k
        return n;
    3568
    13.9k
      }
    3569
    3570
    3571
    13.9k
      fn determine_vertical_bar_op<'a>(&self, original_op: &'static OperatorInfo, mo_node: Element<'a>,
    3572
    13.9k
            next_child: Option<Element<'a>>,
    3573
    13.9k
            parse_stack: &'a mut Vec<StackInfo>,
    3574
    13.9k
            n_vertical_bars_on_right: usize) -> &'static OperatorInfo {
    3575
        // if in a prefix location, it is a left fence
    3576
        // note:  if there is an operator on the top of the stack, it wants an operand (otherwise it would have been reduced)
    3577
    13.9k
        let operator_str = as_text(mo_node);
    3578
    13.9k
        let found_op_info = OPERATORS.get(operator_str);
    3579
    13.9k
        if found_op_info.is_none() {
    3580
    48
          return original_op;
    3581
    13.8k
        }
    3582
    13.8k
        let op = found_op_info.unwrap();
    3583
    13.8k
        if !AMBIGUOUS_OPERATORS.contains(operator_str) {
    3584
          // debug!("   op is not ambiguous");
    3585
    13.4k
          return original_op;
    3586
    401
        };
    3587
    3588
    401
        let operator_versions = OperatorVersions::new(op);
    3589
    401
        if let Some(prefix360) = operator_versions.prefix &&
    3590
    360
           (top(parse_stack).last_child_in_mrow().is_none() || !top(parse_stack).is_operand260) {
    3591
          // debug!("   is prefix");
    3592
    115
          return prefix;
    3593
    286
        }
    3594
    3595
        // We have either a right fence or an infix operand at the top of the stack
    3596
        // If this is already parsed, we'd look to the right to see if there is an operand after this child.
    3597
        // But it isn't parsed and there might be a prefix operator which will eventually become an operand, so it is tricky.
    3598
        // It is even trickier because we might have an implicit times, so we can't really tell
    3599
        // For example:  |x|y|z| which can be '|x| y |z|' or '|x |y| z|', or even | (x|y)|z |'
    3600
        // We can't really know what is intended (without @intent).
    3601
        // It seems like the case where it could be paired with a matching vertical bar as what most people would choose, so we favor that.
    3602
    3603
        // If there is a matching open vertical bar, it is either at the top of the stack or the entry just below the top
    3604
    3605
    286
        let has_left_match = if let Some(op_prefix245) = operator_versions.prefix {
    3606
    245
          if ptr_eq(top(parse_stack).op_pair.op, op_prefix) {   // match at top of stack? (empty matching bars)
    3607
    109
            true
    3608
    136
          } else if parse_stack.len() > 2 {
    3609
            // matching op is below top (operand between matching bars) -- pop, peek, push
    3610
    36
            let old_top = parse_stack.pop().unwrap();
    3611
    36
            let top_op = top(parse_stack).op_pair.op;                                 // can only access top, so we need to pop off top and push back later
    3612
    36
            parse_stack.push(old_top);
    3613
    36
            ptr_eq(top_op, op_prefix)
    3614
          } else {
    3615
    100
            false
    3616
          }
    3617
        } else {
    3618
    41
          false
    3619
        };
    3620
    286
        if let Some(postfix245) =operator_versions.postfix && (next_child245.is_none245() || has_left_match130) {
    3621
          // last child in row (must be a close) or we have a left match
    3622
          // debug!("   is postfix");
    3623
    136
          return postfix;
    3624
    150
        } else if next_child.is_none() {
    3625
          // operand on left, so prefer infix version
    3626
    18
          return if let Some(infix) = operator_versions.infix {infix} else {op0};
    3627
    132
        }
    3628
    3629
    132
        let next_child = next_child.unwrap();
    3630
    132
        if let Some(prefix109) = operator_versions.prefix && (n_vertical_bars_on_right & 0x1 != 0)109 {
    3631
          //  ("   is prefix");
    3632
    3
          return prefix;   // odd number of vertical bars remain, so consider this the start of a pair
    3633
    129
        }
    3634
    3635
    129
        let next_child = get_possible_embellished_node(next_child);
    3636
    129
        let next_child_op = if name(next_child) != "mo" {
    3637
    128
            None
    3638
          } else {
    3639
    1
            let next_next_children = next_child.following_siblings();
    3640
    1
            let next_next_child = if next_next_children.is_empty() { None0 } else { Some( as_element(next_next_children[0]) )};
    3641
    1
            Some( CanonicalizeContext::find_operator(Some(self), next_child, operator_versions.infix,
    3642
    1
                      top(parse_stack).last_child_in_mrow(), next_next_child) )
    3643
          };
    3644
    3645
        // If the next child is a prefix op or a left fence, it will reduce to an operand, so don't consider it an operator
    3646
    129
        if next_child_op.is_some() && !next_child_op.unwrap().is_left_fence()1 && !next_child_op.unwrap().is_prefix()0 {
    3647
    0
          if let Some(postfix) =operator_versions.postfix {
    3648
            // debug!("   is postfix");
    3649
    0
            return postfix;
    3650
    0
          }
    3651
    129
        } else if let Some(infix) = operator_versions.infix {
    3652
          // debug!("   is infix");
    3653
    129
          return infix;
    3654
    0
        }
    3655
    3656
        // nothing good to match
    3657
    0
        return op;
    3658
    13.9k
      }
    3659
    3660
    3661
      // return FunctionNameCertainty::False or Maybe if 'node' is a chemical element and is followed by a state (solid, liquid, ...)
    3662
      //  in other words, we are certain this can't be a function since it looks like it is or might be chemistry
    3663
    1.71k
      fn is_likely_chemical_state<'a>(&self, node: Element<'a>, right_sibling: Element<'a>) -> FunctionNameCertainty {
    3664
    1.71k
        assert_eq!(name(get_parent(node)), "mrow"); // should be here because we are parsing an mrow
    3665
    3666
        // debug!("   in is_likely_chemical_state: '{}'?",element_summary(node));
    3667
    1.71k
        let node_chem_likelihood= node.attribute_value(MAYBE_CHEMISTRY);
    3668
    1.71k
        if node.attribute(MAYBE_CHEMISTRY).is_none() {
    3669
    1.16k
          return FunctionNameCertainty::True;
    3670
    549
        }
    3671
    3672
    549
        if name(right_sibling) == "mrow" {    // clean_chemistry_mrow made sure any state-like structure is an mrow
    3673
    75
          let state_likelihood = likely_chem_state(right_sibling);
    3674
    75
          if state_likelihood > 0 {
    3675
    49
            right_sibling.set_attribute_value(MAYBE_CHEMISTRY, state_likelihood.to_string().as_str());
    3676
            // at this point, we know both node and right_sibling are positive, so we have at least a maybe
    3677
    49
            if state_likelihood + node_chem_likelihood.unwrap().parse::<i32>().unwrap() > 2 {
    3678
    49
              return FunctionNameCertainty::False;
    3679
            } else {
    3680
    0
              return FunctionNameCertainty::Maybe
    3681
            }
    3682
    26
          }
    3683
    474
        }
    3684
    3685
    500
        return FunctionNameCertainty::True;
    3686
    1.71k
      }
    3687
    3688
      // Try to figure out whether an <mi> is a function name or not.
    3689
      // There are two important cases depending upon whether parens/brackets are used or not.
    3690
      // E.g, sin x and f(x)
    3691
      // 1. If parens follow the name, then we use a more inclusive set of heuristics as it is more likely a function
    3692
      // The heuristics used are:
    3693
      //   - it is on the list of known function names (e.g., sin" and "log")
    3694
      //   - it is on the list of likely function names (e.g, f, g, h)
    3695
      //   - multi-char names that begin with a capital letter (e.g, "Tr")
    3696
      //   - there is a single token inside the parens (why else would someone use parens), any name (e.g, a(x))
    3697
      //   - if there are multiple comma-separated args
    3698
      //
    3699
      // 2. If there are no parens, then only names on the known function list are used (e.g., "sin x")
    3700
      //
    3701
      // If the name if followed by parens but doesn't fit into the above categories, we return a "maybe"
    3702
    32.0k
      fn is_function_name<'a>(&self, node: Element<'a>, right_siblings: Option<&[ChildOfElement<'a>]>) -> FunctionNameCertainty {
    3703
    32.0k
        let base_of_name = get_possible_embellished_node(node);
    3704
    3705
        // actually only 'mi' should be legal here, but some systems used 'mtext' for multi-char variables
    3706
        // FIX: need to allow for composition of function names. E.g, (f+g)(x) and (f^2/g)'(x)
    3707
    32.0k
        let node_name = name(base_of_name);
    3708
    32.0k
        if node_name != "mi" && node_name != "mtext"15.7k {
    3709
    15.4k
          return FunctionNameCertainty::False;
    3710
    16.6k
        }
    3711
        // whitespace is sometimes added to the mi since braille needs it, so do a trim here to get function name
    3712
    16.6k
        let base_name = as_text(base_of_name).trim();
    3713
    16.6k
        if base_name.is_empty() {
    3714
    2
          return FunctionNameCertainty::False;
    3715
    16.6k
        }
    3716
        // debug!("    is_function_name({}), {} following nodes", base_name, if right_siblings.is_none() {"No".to_string()} else {right_siblings.unwrap().len().to_string()});
    3717
    16.6k
        return crate::definitions::SPEECH_DEFINITIONS.with(|defs| {
    3718
          // names that are always function names (e.g, "sin" and "log")
    3719
    16.6k
          let defs = defs.borrow();
    3720
    16.6k
          let names = defs.get_hashset("FunctionNames").unwrap();
    3721
          // UEB seems to think "Sin" (etc) is used for "sin", so we move to lower case
    3722
    16.6k
          if names.contains(&base_name.to_ascii_lowercase()) {
    3723
            // debug!("     ...is in FunctionNames");
    3724
    1.02k
            return FunctionNameCertainty::True; // always treated as function names
    3725
    15.5k
          }
    3726
    3727
          // We include shapes as function names so that △ABC makes sense since △ and
    3728
          //   the other shapes are not in the operator dictionary
    3729
    15.5k
          let shapes = defs.get_hashset("GeometryShapes").unwrap();
    3730
    15.5k
          if shapes.contains(base_name) {
    3731
    23
            return FunctionNameCertainty::True; // always treated as function names
    3732
    15.5k
          }
    3733
    3734
    15.5k
          if right_siblings.is_none() {
    3735
    13.8k
            return FunctionNameCertainty::False; // only accept known names, which is tested above
    3736
    1.71k
          }
    3737
    3738
          // make sure that what follows starts and ends with parens/brackets
    3739
    1.71k
          assert_eq!(name(get_parent(node)), "mrow");
    3740
    1.71k
          let right_siblings = right_siblings.unwrap();
    3741
    1.71k
          let non_whitespace = right_siblings.iter().enumerate()
    3742
    1.71k
                .find(|&(_, child)| {
    3743
    1.71k
                  let child = as_element(*child);
    3744
    1.71k
                  name(child) != "mtext" || !as_text(child).trim().is_empty()54
    3745
    1.71k
                });
    3746
    1.71k
          let right_siblings = if let Some( (i, _) ) = non_whitespace {&right_siblings[i..]} else {right_siblings0};
    3747
    1.71k
          if right_siblings.is_empty() {
    3748
            // debug!("     ...right siblings not None, but zero of them");
    3749
    0
            return FunctionNameCertainty::False;
    3750
    1.71k
          }
    3751
    3752
    1.71k
          let first_child = as_element(right_siblings[0]);
    3753
    3754
          // clean_chemistry wrapped up a state in an mrow and this is assumed by is_likely_chemical_state()
    3755
    1.71k
          let chem_state_certainty = self.is_likely_chemical_state(node, first_child);
    3756
    1.71k
          if chem_state_certainty != FunctionNameCertainty::True {
    3757
            // debug!("      ...is_likely_chemical_state says it is a function ={:?}", chem_state_certainty);
    3758
    49
            return chem_state_certainty;
    3759
    1.66k
          }
    3760
    3761
    1.66k
          if name(first_child) == "mrow" && is_left_paren238(as_element238(first_child.children()[0]238)) {
    3762
            // debug!("     ...trying again after expanding mrow");
    3763
    235
            return self.is_function_name(node, Some(&first_child.children()));
    3764
    1.43k
          }
    3765
    3766
    1.43k
          if right_siblings.len() < 2 {
    3767
            // debug!("     ...not enough right siblings");
    3768
    542
            return FunctionNameCertainty::False; // can't be (...)
    3769
    892
          }
    3770
    3771
          // at least two siblings are this point -- check that they are parens/brackets
    3772
          // we can only check the open paren/bracket because the right side is unparsed and we don't know the close location
    3773
    892
          let first_sibling = as_element(right_siblings[0]);
    3774
    892
          if name(first_sibling) != "mo"  || !is_left_paren(first_sibling)384  // '(' or '['
    3775
          {
    3776
            // debug!("     ...first sibling is not '(' or '['");
    3777
    522
            return FunctionNameCertainty::False;
    3778
    370
          }
    3779
    3780
    370
          let likely_names = defs.get_hashset("LikelyFunctionNames").unwrap();
    3781
    370
          if likely_names.contains(base_name) {
    3782
    206
            return FunctionNameCertainty::True; // don't bother checking contents of parens, consider these as function names
    3783
    164
          }
    3784
    3785
    164
          if is_single_arg(as_text(first_sibling), &right_siblings[1..]) {
    3786
            // debug!("      ...is single arg");
    3787
    64
            return FunctionNameCertainty::True; // if there is only a single arg, why else would you use parens?
    3788
    100
          };
    3789
    3790
    100
          if is_comma_arg(as_text(first_sibling), &right_siblings[1..]) {
    3791
            // debug!("      ...is comma arg");
    3792
    2
            return FunctionNameCertainty::True; // if there is only a single arg, why else would you use parens?
    3793
    98
          };
    3794
    3795
          // FIX: should really make sure all the args are marked as MAYBE_CHEMISTRY, but we don't know the matching close paren/bracket
    3796
    98
          if node.attribute(MAYBE_CHEMISTRY).is_some() &&
    3797
    34
             as_element(right_siblings[1]).attribute(MAYBE_CHEMISTRY).is_some() {
    3798
    1
            return FunctionNameCertainty::False;
    3799
    97
          }
    3800
    3801
          // Names like "Tr" are likely function names, single letter names like "M" or "J" are iffy
    3802
          // This needs to be after the chemical state check above to rule out Cl(g), etc
    3803
          // This would be better if it were part of 'likely_names' as "[A-Za-z]+", but reg exprs don't work in HashSets.
    3804
          // FIX: create our own struct and write appropriate traits for it and then it could work
    3805
    97
          let mut chars = base_name.chars();
    3806
    97
          let first_char = chars.next().unwrap();   // we know there is at least one byte in it, hence one char
    3807
    97
          if chars.next().is_some() && first_char4.is_uppercase4() {
    3808
            // debug!("      ...is uppercase name");
    3809
    4
            return FunctionNameCertainty::True;
    3810
    93
          }
    3811
    3812
          // debug!("      ...didn't match options to be a function");
    3813
          // debug!("Right siblings:\n{}  ", right_siblings.iter().map(|&child| mml_to_string(as_element(child))).collect::<Vec<String>>().join("\n  "));
    3814
    93
          return if is_name_inside_parens(base_name, right_siblings) {FunctionNameCertainty::False5} else {FunctionNameCertainty::Maybe88};
    3815
    16.6k
        });
    3816
    3817
    164
        fn is_single_arg(open: &str, following_nodes: &[ChildOfElement]) -> bool {
    3818
          // following_nodes are nodes after "("
    3819
    164
          if following_nodes.is_empty() {
    3820
    0
            return true;   // "a(" might or might not be a function call -- treat as "is" because we can't see more
    3821
    164
          }
    3822
    3823
    164
          let first_child = as_element(following_nodes[0]);
    3824
    164
          if is_matching_right_paren(open, first_child) {
    3825
    0
            return true;   // no-arg case "a()"
    3826
    164
          }
    3827
    3828
          // could be really picky and restrict to checking for only mi/mn
    3829
          // that might make more sense in stranger cases, but mfrac, msqrt, etc., probably shouldn't have parens if times
    3830
    164
          return following_nodes.len() > 1 &&
    3831
    164
              name(first_child) != "mrow" &&
    3832
    127
              is_matching_right_paren(open, as_element(following_nodes[1]));
    3833
    164
        }
    3834
    3835
    100
        fn is_comma_arg(open: &str, following_nodes: &[ChildOfElement]) -> bool {
    3836
          // following_nodes are nodes after "("
    3837
    100
          if following_nodes.len() == 1 {
    3838
    0
            return false;
    3839
    100
          }
    3840
    3841
    100
          let first_child = as_element(following_nodes[1]);
    3842
    100
          if name(first_child) == "mrow" {
    3843
    0
            return is_comma_arg(open, &first_child.children()[..]);
    3844
    100
          }
    3845
    3846
          // FIX: this loop is very simplistic and could be improved to count parens, etc., to make sure "," is at top-level
    3847
    318
          for child in following_nodes100 {
    3848
    318
            let child = as_element(*child);
    3849
    318
            if name(child) == "mo" {
    3850
    141
              if as_text(child) == "," {
    3851
    2
                return true;
    3852
    139
              }
    3853
    139
              if is_matching_right_paren(open, child) {
    3854
    96
                return false;
    3855
    43
              }
    3856
    177
            }
    3857
          }
    3858
    3859
    2
          return false;
    3860
    100
        }
    3861
    3862
    622
        fn is_left_paren(node: Element) -> bool {
    3863
    622
          if name(node) != "mo" {
    3864
    1
            return false;
    3865
    621
          }
    3866
    621
          let text = as_text(node);
    3867
    621
          return text == "(" || text == "["22;
    3868
    622
        }
    3869
    3870
    430
        fn is_matching_right_paren(open: &str, node: Element) -> bool {
    3871
    430
          if name(node) != "mo" {
    3872
    184
            return false;
    3873
    246
          }
    3874
    246
          let text = as_text(node);
    3875
          // debug!("         is_matching_right_paren: open={}, close={}", open, text);
    3876
    246
          return (open == "(" && text == ")"244) || (open == "["88 && text == "]"2);
    3877
    430
        }
    3878
    3879
        /// Returns true if the name of the potential function is inside the parens. In that case, it is very unlikely to be a function call
    3880
        /// For example, "n(n+1)"
    3881
    93
        fn is_name_inside_parens(function_name: &str, right_siblings: &[ChildOfElement]) -> bool {
    3882
          // the first child of right_siblings is either '(' or '['
    3883
          // right_siblings may extend well beyond the closing parens, so we first break this into finding the contents
    3884
          // then we search the contents for the name
    3885
    93
          match find_contents(right_siblings) {
    3886
    2
            None => return false,
    3887
    91
            Some(contents) => return is_name_inside_contents(function_name, contents),
    3888
          }
    3889
    3890
    3891
    93
          fn find_contents<'a>(right_siblings: &'a[ChildOfElement<'a>]) -> Option<&'a[ChildOfElement<'a>]> {
    3892
    93
            let open_text = as_text(as_element(right_siblings[0]));
    3893
    93
            let close_text = if open_text == "("  { ")"91 } else { "]"2 };
    3894
    93
            let mut nesting_level = 1;
    3895
    93
            let mut i = 1;
    3896
    296
            while i < right_siblings.len() {
    3897
    294
              let child = as_element(right_siblings[i]);
    3898
    294
              if name(child) == "mo" {
    3899
    133
                let op_text = as_text(child);
    3900
    133
                if op_text == open_text {
    3901
    0
                  nesting_level += 1;
    3902
    133
                } else if op_text == close_text {
    3903
    91
                  if nesting_level == 1 {
    3904
    91
                    return Some(&right_siblings[1..i]);
    3905
    0
                  }
    3906
    0
                  nesting_level -= 1;
    3907
    42
                }
    3908
    161
              }
    3909
    203
              i += 1;
    3910
            }
    3911
    2
            return None; // didn't find matching paren
    3912
    93
          }
    3913
    3914
    134
          fn is_name_inside_contents(function_name: &str, contents: &[ChildOfElement]) -> bool {
    3915
    304
            for &child in contents134 {
    3916
    304
              let child = as_element(child);
    3917
              // debug!("is_name_inside_contents: child={}", mml_to_string(child));
    3918
    304
              if is_leaf(child) {
    3919
    261
                let text = as_text(child);
    3920
    261
                if (name(child) == "mi" || name(child) == "mtext"108) && text == function_name163 {
    3921
    5
                  return true;
    3922
    256
                }
    3923
    43
              } else if is_name_inside_contents(function_name, &child.children()) {
    3924
    4
                return true;
    3925
    39
              }
    3926
            }
    3927
    125
            return false;
    3928
    134
          }
    3929
    93
        }
    3930
    32.0k
      }
    3931
    3932
    5.79k
      fn is_mixed_fraction<'a>(&self, integer_part: Element<'a>, fraction_children: &[ChildOfElement<'a>]) -> Result<bool> {
    3933
        // do some simple disqualifying checks on the fraction part
    3934
    5.79k
        if fraction_children.is_empty() {
    3935
    0
          return Ok( false );
    3936
    5.79k
        }
    3937
    5.79k
        let right_child = as_element(fraction_children[0]);
    3938
    5.79k
        let right_child_name = name(right_child);
    3939
    5.79k
        if ! (right_child_name == "mfrac" ||
    3940
    5.68k
           (right_child_name == "mrow" && right_child.children().len() == 3218) ||
    3941
    5.48k
             (right_child_name == "mn" && fraction_children.len() >= 3138) ) {
    3942
    5.46k
          return Ok( false );
    3943
    329
        };
    3944
    3945
    329
        if !is_integer_part_ok(integer_part) {
    3946
    219
          return Ok( false );
    3947
    110
        }
    3948
    3949
    110
        if right_child_name == "mfrac" {
    3950
    75
          return Ok( is_mfrac_ok(right_child) );
    3951
    35
        }
    3952
    3953
    35
        return is_linear_fraction(self, fraction_children);
    3954
    3955
    3956
    351
        fn is_int(integer_part: Element) -> bool {
    3957
    351
          return name(integer_part) == "mn"  && !as_text(integer_part).contains(DECIMAL_SEPARATOR)185;
    3958
    351
        }
    3959
    3960
    329
        fn is_integer_part_ok(integer_part: Element) -> bool {
    3961
          // integer part must be either 'n' or '-n' (in an mrow)
    3962
    329
          let integer_part_name = name(integer_part);
    3963
    329
          if integer_part_name == "mrow" {
    3964
    83
            let children = integer_part.children();
    3965
    83
            if children.len() == 2 &&
    3966
    16
               name(as_element(children[0])) == "mo" &&
    3967
    0
               as_text(as_element(children[0])) == "-" {
    3968
    0
              let integer_part = as_element(children[1]);
    3969
    0
              return is_int(integer_part);
    3970
    83
            }
    3971
    83
            return false;
    3972
    246
          };
    3973
    3974
    246
          return is_int(integer_part);
    3975
    329
        }
    3976
    3977
    75
        fn is_mfrac_ok(fraction_part: Element) -> bool {
    3978
          // fraction_part needs to have integer numerator and denominator (already tested it is a frac)
    3979
    75
          let fraction_children = fraction_part.children();
    3980
    75
          if fraction_children.len() != 2 {
    3981
    0
            return false;
    3982
    75
          }
    3983
    75
          let numerator = as_element(fraction_children[0]);
    3984
    75
          if name(numerator) != "mn" || as_text(numerator)67.contains67(DECIMAL_SEPARATOR) {
    3985
    8
            return false;
    3986
    67
          }
    3987
    67
          let denominator = as_element(fraction_children[1]);
    3988
    67
          return is_int(denominator);
    3989
    75
        }
    3990
    3991
    66
        fn is_linear_fraction(canonicalize: &CanonicalizeContext, fraction_children: &[ChildOfElement]) -> Result<bool> {
    3992
          // two possibilities
    3993
          // 1. '3 / 4' is in an mrow
    3994
          // 2. '3 / 4' are three separate elements
    3995
    66
          let first_child = as_element(fraction_children[0]);
    3996
    66
          if name(first_child) == "mrow" {
    3997
    31
            if first_child.children().len() != 3 {
    3998
    0
              return Ok( false );
    3999
    31
            }
    4000
    31
            return is_linear_fraction(canonicalize, &first_child.children())
    4001
    35
          }
    4002
    4003
    4004
          // the length has been checked
    4005
    35
          assert!(fraction_children.len() >= 3);
    4006
    4007
    35
          if !is_int(first_child) {
    4008
    30
            return Ok( false );
    4009
    5
          }
    4010
    5
          let slash_part = canonicalize.canonicalize_mrows(as_element(fraction_children[1]))?0;
    4011
    5
          if name(slash_part) == "mo" && as_text(slash_part) == "/" {
    4012
    3
            let denom = canonicalize.canonicalize_mrows(as_element(fraction_children[2]))?0;
    4013
    3
            return Ok( is_int(denom) );
    4014
    2
          }
    4015
    2
          return Ok( false );
    4016
    66
        }
    4017
    5.79k
      }
    4018
    4019
      /// implied comma when two numbers are adjacent and are in a script position
    4020
    5.72k
      fn is_implied_comma<'a>(&self, prev: Element<'a>, current: Element<'a>, mrow: Element<'a>) -> bool {
    4021
    5.72k
        if name(prev) != "mn" || name(current) != "mn"4.06k {
    4022
    5.63k
          return false;
    4023
    95
        }
    4024
    4025
    95
        assert_eq!(name(mrow), "mrow");
    4026
    95
        let container = get_parent(mrow);
    4027
    95
        let name = name(container);
    4028
    4029
        // test for script position is that it is not the base and hence has a preceding sibling
    4030
    95
        return (name == "msub" || name == "msubsup"14 || name == "msup"14) && !mrow.preceding_siblings().is_empty()81;
    4031
    5.72k
      }
    4032
    4033
      /// implied separator when two capital letters are adjacent or two chemical elements
    4034
    5.64k
      fn is_implied_chemical_bond<'a>(&self, prev: Element<'a>, current: Element<'a>) -> bool {
    4035
        // debug!("is_implied_chemical_bond: previous: {:?}", prev.preceding_siblings());
    4036
        // debug!("is_implied_chemical_bond: following: {:?}", prev.following_siblings());
    4037
    5.64k
        if prev.attribute(MAYBE_CHEMISTRY).is_none() || current514.attribute(MAYBE_CHEMISTRY).is_none514() {
    4038
    5.18k
          return false;
    4039
    462
        }
    4040
        // ABC example where B and C are chemical elements is why we need to scan further than just checking B and C
    4041
        // look for an mi/mtext with @MAYBE_CHEMISTRY until we get to something that can't have it
    4042
    626
        for child in prev462.preceding_siblings462() {
    4043
    626
          if !is_valid_chemistry(as_element(child)) {
    4044
    11
            return false;
    4045
    615
          }
    4046
        }
    4047
    851
        for child in current451.following_siblings451() {
    4048
    851
          if !is_valid_chemistry(as_element(child)) {
    4049
    32
            return false;
    4050
    819
          }
    4051
        }
    4052
    419
        return true;   // sequence of all MAYBE_CHEMISTRY
    4053
    4054
    1.47k
        fn is_valid_chemistry(child: Element) -> bool {
    4055
    1.47k
          let child = get_possible_embellished_node(child);
    4056
    1.47k
          return child.attribute(MAYBE_CHEMISTRY).is_some() || (name(child) != "mi"654 && name(child) != "mtext"614);
    4057
    1.47k
        }
    4058
    5.64k
      }
    4059
    4060
      /// implied separator when two capital letters are adjacent or two chemical elements
    4061
      /// also for adjacent omission chars
    4062
    5.22k
      fn is_implied_separator<'a>(&self, prev: Element<'a>, current: Element<'a>) -> bool {
    4063
    5.22k
        if name(prev) != "mi" || name(current) != "mi"516 {
    4064
    4.83k
          return false;
    4065
    390
        }
    4066
    4067
        // trim because whitespace might have gotten stuffed into the <mi>s
    4068
    390
        let prev_text = as_text(prev).trim();
    4069
    390
        let current_text = as_text(current).trim();
    4070
    390
        return prev_text.len() == 1 && current_text.len() == 1352 &&
    4071
    317
             ((is_cap(prev_text) && is_cap174(current_text174)) ||
    4072
    151
              (prev_text=="_" && current_text=="_"0));
    4073
    4074
    4075
    491
        fn is_cap(str: &str) -> bool {
    4076
    491
          assert_eq!(str.len(), 1);
    4077
    491
          return str.chars().next().unwrap().is_ascii_uppercase();
    4078
    491
        }
    4079
    5.22k
      }
    4080
    4081
    42
      fn is_invisible_char_element(mathml: Element) -> bool {
    4082
    42
        if !is_leaf(mathml) {
    4083
    8
          return false
    4084
    34
        }
    4085
    34
        let text = as_text(mathml);
    4086
    34
        if text.len() != 3 {   // speed hack: invisible chars are three UTF-8 chars
    4087
    28
          return false;
    4088
    6
        }
    4089
    6
        let ch = text.chars().next().unwrap();
    4090
    6
        return ('\u{2061}'..='\u{2064}').contains(&ch);
    4091
    42
      }
    4092
    4093
      // Add the current operator if it's not n-ary to the stack
    4094
      // 'current_child' and it the operator to the stack.
    4095
    17.7k
      fn shift_stack<'s, 'a:'s, 'op:'a>(
    4096
    17.7k
            &self, parse_stack: &'s mut Vec<StackInfo<'a, 'op>>,
    4097
    17.7k
            current_child: Element<'a>,
    4098
    17.7k
            current_op: OperatorPair<'op>) -> (Element<'a>, OperatorPair<'op>) {
    4099
    17.7k
        let mut new_current_child = current_child;
    4100
    17.7k
        let mut new_current_op = current_op.clone();
    4101
    17.7k
        let previous_op = top(parse_stack).op_pair.clone();
    4102
        // debug!(" shift_stack: mrow len={}", top(parse_stack).mrow.children().len().to_string());
    4103
        // debug!(" shift_stack: shift on '{}'; ops: prev '{}/{}', cur '{}/{}'",
    4104
        //    element_summary(current_child),show_invisible_op_char(previous_op.ch), previous_op.op.priority,
    4105
        //    show_invisible_op_char(current_op.ch), current_op.op.priority);
    4106
    17.7k
        if !current_op.op.is_nary(previous_op.op) {
    4107
          // grab operand on top of stack (if there is one) and make it part of the new mrow since current op has higher precedence
    4108
          // if operators are the same and are binary, then this push makes them act as left associative
    4109
    13.0k
          let mut top_of_stack = parse_stack.pop().unwrap();
    4110
    13.0k
          if top_of_stack.mrow.children().is_empty() || (!top_of_stack.is_operand12.9k && !current_op.op.is_right_fence()72) {
    4111
    138
            // "bad" syntax - no operand on left -- don't grab operand (there is none)
    4112
    138
            //   just start a new mrow beginning with operator
    4113
    138
            // FIX -- check this shouldn't happen:  parse_stack.push(top_of_stack);
    4114
    138
            parse_stack.push( top_of_stack );   // put top back on
    4115
    138
            parse_stack.push( StackInfo::new(current_child.document()) );
    4116
    12.8k
          } else if current_op.op.is_right_fence() {
    4117
            // likely, but not necessarily, there is a left fence to start the mrow
    4118
            // this is like the postfix case except we grab the entire mrow, push on the close, and make that the mrow
    4119
            // note:  the code does these operations on the stack for consistency, but it could be optimized without push/popping the stack
    4120
    1.96k
            let mrow = top_of_stack.mrow;
    4121
    1.96k
            top_of_stack.add_child_to_mrow(current_child, current_op);
    4122
            // debug!("shift_stack: after adding right fence to mrow:\n{}", mml_to_string(mrow));
    4123
    1.96k
            new_current_op = OperatorPair::new();             // treat matched brackets as operand
    4124
    1.96k
            new_current_child = mrow;
    4125
    1.96k
            let children = mrow.children();
    4126
    1.96k
            let base_of_first_child = get_possible_embellished_node(as_element(children[0]));
    4127
            // debug!("looking for left fence: len={}, {:#?}", children.len(), CanonicalizeContext::find_operator(Some(self), base_of_first_child, None, Some(as_element(children[0])), Some(mrow)));
    4128
    1.96k
            if children.len() == 2 &&
    4129
    64
               (name(base_of_first_child) != "mo" ||
    4130
    13
                !CanonicalizeContext::find_operator(Some(self), base_of_first_child, None,
    4131
    51
                                Some(as_element13(children[0])), Some(mrow)).is_left_fence()) {
    4132
    51
              // the mrow did *not* start with an open (hence no push)
    4133
    51
              // since parser really wants balanced parens to keep stack state right, we do a push here
    4134
    51
              parse_stack.push( StackInfo::new(mrow.document()) );
    4135
    51
            } else {
    4136
              // the mrow started with some open fence (which caused a push) -- add the close, pop, and push on the "operand"
    4137
    1.91k
              new_current_child = self.potentially_lift_script(mrow)
    4138
            }
    4139
    10.9k
          } else if current_op.op.is_postfix() {
    4140
    81
            // grab the left operand and start a new mrow with it and the operator -- put those back on the stack
    4141
    81
            // note:  the code does these operations on the stack for consistency, but it could be optimized without push/popping the stack
    4142
    81
            let previous_child = top_of_stack.remove_last_operand_from_mrow();         // remove operand from mrow
    4143
    81
            parse_stack.push(top_of_stack);
    4144
    81
            let mut new_top_of_stack = StackInfo::with_op(&current_child.document(), previous_child, current_op.clone()); // begin new mrow with operand
    4145
    81
            new_top_of_stack.add_child_to_mrow(current_child, current_op);  // add on operator
    4146
    81
            new_current_child = new_top_of_stack.mrow;                // grab for pushing on old mrow
    4147
    81
            new_current_op = OperatorPair::new();               // treat "reduced" postfix operator & operand as an operand
    4148
    81
            // debug!("shift_stack: after adding postfix to mrow has len: {}", new_current_child.children().len().to_string());
    4149
    10.8k
          } else {
    4150
    10.8k
            // normal infix op case -- grab the left operand and start a new mrow with it and the operator
    4151
    10.8k
            let previous_child = top_of_stack.remove_last_operand_from_mrow();
    4152
    10.8k
            parse_stack.push(top_of_stack);
    4153
    10.8k
            parse_stack.push( StackInfo::with_op(&current_child.document(),previous_child, current_op) );
    4154
    10.8k
          }
    4155
    4.73k
        }
    4156
    17.7k
        return (new_current_child, new_current_op);
    4157
    17.7k
      }
    4158
    4159
    4160
    25.2k
      fn reduce_stack<'s, 'a:'s, 'op:'a>(&self, parse_stack: &'s mut Vec<StackInfo<'a, 'op>>, current_priority: usize) {
    4161
    25.2k
        let mut prev_priority = top(parse_stack).priority();
    4162
        // debug!(" reduce_stack: stack len={}, priority: prev={}, cur={}", parse_stack.len(), prev_priority, current_priority);
    4163
    37.2k
        while current_priority < prev_priority {          // pop off operators until we are back to the right level
    4164
    12.0k
          if parse_stack.len() == 1 {
    4165
    0
            break;     // something went wrong -- break before popping too much
    4166
    12.0k
          }
    4167
    12.0k
          prev_priority = self.reduce_stack_one_time(parse_stack);
    4168
        };
    4169
    25.2k
      }
    4170
    4171
    12.0k
      fn reduce_stack_one_time<'s, 'a:'s, 'op:'a>(&self, parse_stack: &'s mut Vec<StackInfo<'a, 'op>>) -> usize {
    4172
    12.0k
        let mut top_of_stack = parse_stack.pop().unwrap();
    4173
        // debug!(" ..popped len={} op:'{}/{}', operand: {}",
    4174
        //    top_of_stack.mrow.children().len(),
    4175
        //    show_invisible_op_char(top_of_stack.op_pair.ch), top_of_stack.op_pair.op.priority,
    4176
        //    top_of_stack.is_operand);
    4177
    12.0k
        let mut mrow = top_of_stack.mrow;
    4178
    12.0k
        if mrow.children().len() == 1 && CanonicalizeContext::is_ok_to_merge_mrow_child63(mrow63) {
    4179
    63
          // should have added at least operator and operand, but input might not be well-formed
    4180
    63
          // in this case, unwrap the mrow and expose the single child for pushing onto stack
    4181
    63
          let single_child = top_of_stack.remove_last_operand_from_mrow();
    4182
    63
          mrow = single_child;
    4183
    11.9k
        }
    4184
    4185
    12.0k
        let mut top_of_stack = parse_stack.pop().unwrap();
    4186
    12.0k
        top_of_stack.add_child_to_mrow(mrow, OperatorPair::new());  // mrow on top is "parsed" -- now add it to previous
    4187
    12.0k
        let prev_priority = top_of_stack.priority();
    4188
    12.0k
        parse_stack.push(top_of_stack);
    4189
    12.0k
        return prev_priority;
    4190
    12.0k
      }
    4191
    4192
    5.06k
      fn is_trig_arg<'a, 'op:'a>(&self, previous_child: Element<'a>, current_child: Element<'a>, parse_stack: &mut Vec<StackInfo<'a, 'op>>) -> bool {
    4193
        // We have operand-operand and know we want multiplication at this point.
    4194
        // Check for special case where we want multiplication to bind more tightly than function app (e.g, sin 2x, sin -2xy)
    4195
        // We only want to do this for simple args
    4196
        // debug!("  is_trig_arg: prev {}, current {}, Stack:", element_summary(previous_child), element_summary(current_child));
    4197
        // parse_stack.iter().for_each(|stack_info| debug!("    {}", stack_info));
    4198
    5.06k
        if !IsNode::is_simple(current_child) {
    4199
    2.98k
          return false;
    4200
    2.07k
        }
    4201
        // This only matters if we are not inside of parens
    4202
    2.07k
        if IsBracketed::is_bracketed(previous_child, "(", ")", false, false) ||
    4203
    2.01k
           IsBracketed::is_bracketed(previous_child, "[", "]", false, false) {
    4204
    63
          return false;
    4205
    2.01k
        }
    4206
    4207
        // Use lower priority multiplication if current_child is a function (e.g. "cos" in "sin x cos 3y")
    4208
        // if !is_trig(current_child) {
    4209
    2.01k
        if self.is_function_name(current_child, None) == FunctionNameCertainty::True {
    4210
    1
          return false;
    4211
    2.01k
        }
    4212
        // Three cases:
    4213
        // 1. First operand-operand (e.g, sin 2x, where 'current_child' is 'x') -- top of stack is mrow('sin' f_apply '2')
    4214
        // 2. Another First operand-operand (e.g, sin -2x, where 'current_child' is 'x') -- top of stack is mrow('-' '2'), next is mrow('sin', f_apply)
    4215
        // 3. Subsequent operand-operand (e.g, sin 2xy, where 'current_child' is 'y') -- top of stack is mrow('2' 'times' 'x')
    4216
        //    Note: IMPLIED_TIMES_HIGH_PRIORITY is only present if we have a trig function
    4217
    2.01k
        let op_on_top = &top(parse_stack).op_pair;
    4218
    2.01k
        if ptr_eq(op_on_top.op, *INVISIBLE_FUNCTION_APPLICATION) {
    4219
    8
          let function_element = as_element(top(parse_stack).mrow.children()[0]);
    4220
    8
          return is_trig(function_element);
    4221
    2.00k
        }
    4222
    2.00k
        if ptr_eq(op_on_top.op, *PREFIX_MINUS) {
    4223
    74
          if parse_stack.len() < 2 {
    4224
    0
            return false;
    4225
    74
          }
    4226
    74
          let next_stack_info = &parse_stack[parse_stack.len()-2];
    4227
    74
          if !ptr_eq(next_stack_info.op_pair.op, *INVISIBLE_FUNCTION_APPLICATION) {
    4228
    72
            return false;
    4229
    2
          }
    4230
    2
          let function_element = as_element(next_stack_info.mrow.children()[0]);
    4231
    2
          if is_trig(function_element) {
    4232
            // want '- 2' to be an mrow; don't want '- 2 x ...' to be the mrow (IMPLIED_TIMES_HIGH_PRIORITY is an internal hack)
    4233
    1
            self.reduce_stack_one_time(parse_stack);
    4234
    1
            return true;
    4235
    1
          }
    4236
    1
          return false;
    4237
    1.92k
        }
    4238
    1.92k
        return ptr_eq(op_on_top.op, &IMPLIED_TIMES_HIGH_PRIORITY);
    4239
    4240
    10
        fn is_trig(node: Element) -> bool {
    4241
    10
          let base_of_name = get_possible_embellished_node(node);
    4242
    4243
          // actually only 'mi' should be legal here, but some systems used 'mtext' for multi-char variables
    4244
    10
          let node_name = name(base_of_name);
    4245
    10
          if node_name != "mi" && node_name != "mtext"0 {
    4246
    0
            return false;
    4247
    10
          }
    4248
          // whitespace is sometimes added to the mi since braille needs it, so do a trim here to get function name
    4249
    10
          let base_name = as_text(base_of_name).trim();
    4250
    10
          if base_name.is_empty() {
    4251
    0
            return false;
    4252
    10
          }
    4253
    10
          return crate::definitions::SPEECH_DEFINITIONS.with(|defs| {
    4254
            // names that are always function names (e.g, "sin" and "log")
    4255
    10
            let defs = defs.borrow();
    4256
    10
            let names = defs.get_hashset("TrigFunctionNames").unwrap();
    4257
            // UEB seems to think "Sin" (etc) is used for "sin", so we move to lower case
    4258
    10
            return names.contains(&base_name.to_ascii_lowercase());
    4259
    10
          });
    4260
    10
        }
    4261
    5.06k
      }
    4262
    4263
    4264
      /*
    4265
        canonicalize_mrows_in_mrow is a simple(ish) operator precedence parser.
    4266
        It works by keeping a stack of 'StackInfo':
    4267
        'StackInfo' has three parts:
    4268
        1. the mrow being build
    4269
        2. info about the operator in the mrow being build
    4270
        3. bool to say whether the last thing is an operator or an operand
    4271
    4272
        When the op priority increases (eg, have "=" and get "+"), we push on
    4273
        1. a new mrow -- if the operator has a left operand, we remove the last node in the mrow and it becomes
    4274
           the first (only so far) child of the new mrow
    4275
        2. the operator info
    4276
    4277
        When the op priority decreases, we do the following loop until the this new priority > priority on top of stack
    4278
        1. pop the StackInfo
    4279
        2. add the StackInfo's mrow  as the last child to the new top of the stack
    4280
        We also do this when we hit the end of the mrow (we can treat this case as if we have a negative precedence)
    4281
    4282
        +/- are treated as nary operators and don't push/pop in those cases.
    4283
        consecutive operands such as nary times are also considered n-ary operators and don't push/pop in those cases.
    4284
      */
    4285
    7.48k
      fn canonicalize_mrows_in_mrow<'a>(&self, mrow: Element<'a>) -> Result<Element<'a>> {
    4286
    7.48k
        let is_ok_to_merge_child = mrow.children().len() != 1 || CanonicalizeContext::is_ok_to_merge_mrow_child56(mrow56);
    4287
    7.48k
        let saved_mrow_attrs = mrow.attributes();
    4288
    7.48k
        assert_eq!(name(mrow), "mrow");
    4289
    4290
        // FIX: don't touch/canonicalize
    4291
        // 1. if intent is given -- anything intent references
    4292
        // 2. if the mrow starts or ends with a fence, don't merge into parent (parse children only) -- allows for "]a,b["
    4293
    7.48k
        let mut parse_stack = vec![StackInfo::new(mrow.document())];
    4294
    7.48k
        let mut children = mrow.children();
    4295
    7.48k
        let num_children = children.len();
    4296
    4297
    36.7k
        for i_child in 0..num_children7.48k {
    4298
          // debug!("\nDealing with child #{}: {}", i_child, mml_to_string(as_element(children[i_child])));
    4299
    36.7k
          let mut current_child = self.canonicalize_mrows(as_element(children[i_child]))?0;
    4300
    36.7k
          children[i_child] = ChildOfElement::Element( current_child );
    4301
    36.7k
          let base_of_child = get_possible_embellished_node(current_child);
    4302
    36.7k
          let acts_as_ch = current_child.attribute_value(ACT_AS_OPERATOR);
    4303
    36.7k
          let mut current_op = OperatorPair::new();
    4304
          // figure what the current operator is -- it either comes from the 'mo' (if we have an 'mo') or it is implied
    4305
    36.7k
          if (name(base_of_child) == "mo" &&
    4306
    13.9k
              !( base_of_child.children().is_empty() || as_text(base_of_child) == "\u{00A0}" )) || // shouldn't have empty mo node, but...
    4307
    22.8k
             acts_as_ch.is_some() {
    4308
    13.9k
            let previous_op = if top(&parse_stack).is_operand {None10.9k} else {Some( top(&parse_stack).op_pair.op )2.95k};
    4309
    13.9k
            let next_node = if i_child + 1 < num_children {Some(11.9kas_element11.9k(children[i_child+1]))} else {None1.99k};
    4310
    13.9k
            if let Some(acts_as_ch20) = acts_as_ch {
    4311
    20
              // ∇× (etc) hack, including ∇ being a vector (maybe eventually others)
    4312
    20
              let temp_mo = create_mathml_element(&current_child.document(), "mo");
    4313
    20
              temp_mo.set_text(acts_as_ch);
    4314
    20
              current_op = OperatorPair{
    4315
    20
                ch: acts_as_ch,
    4316
    20
                op: CanonicalizeContext::find_operator(Some(self), temp_mo, previous_op,
    4317
    20
                    top(&parse_stack).last_child_in_mrow(), next_node)
    4318
    20
              };
    4319
    13.9k
            } else {
    4320
    13.9k
              current_op = OperatorPair{
    4321
    13.9k
                ch: as_text(base_of_child),
    4322
    13.9k
                op: CanonicalizeContext::find_operator(Some(self), base_of_child, previous_op,
    4323
    13.9k
                    top(&parse_stack).last_child_in_mrow(), next_node)
    4324
    13.9k
              };
    4325
    13.9k
    4326
    13.9k
              // deal with vertical bars which might be infix, open, or close fences
    4327
    13.9k
              // note: mrow shrinks as we iterate through it (removing children from it)
    4328
    13.9k
              current_op.op = self.determine_vertical_bar_op(
    4329
    13.9k
                current_op.op,
    4330
    13.9k
                base_of_child,
    4331
    13.9k
                next_node,
    4332
    13.9k
                &mut parse_stack,
    4333
    13.9k
                self.n_vertical_bars_on_right(&children[i_child+1..], current_op.ch)
    4334
    13.9k
              );
    4335
    13.9k
            }
    4336
          } else {
    4337
    22.8k
            let previous_child = top(&parse_stack).last_child_in_mrow();
    4338
    22.8k
            if let Some(previous_child17.9k) = previous_child {
    4339
    17.9k
              let base_of_previous_child = get_possible_embellished_node(previous_child);
    4340
    17.9k
              let acts_as_ch = previous_child.attribute_value(ACT_AS_OPERATOR);
    4341
    17.9k
              if name(base_of_previous_child) != "mo" && acts_as_ch6.57k.is_none6.57k() {
    4342
    6.55k
                let likely_function_name = self.is_function_name(previous_child, Some(&children[i_child..]));
    4343
    6.55k
                if name(base_of_child) == "mtext" && as_text(base_of_child) == "\u{00A0}"184 {
    4344
    1
                  base_of_child.set_attribute_value("data-function-likelihood", &(likely_function_name == FunctionNameCertainty::True).to_string());
    4345
    1
                  base_of_child.remove_attribute("data-was-mo");
    4346
    1
                  set_mathml_name(base_of_child, "mo");
    4347
    1
                  let mut top_of_stack = parse_stack.pop().unwrap();
    4348
    1
                  top_of_stack.add_child_to_mrow(current_child, OperatorPair{ ch: "\u{00A0}", op: *INVISIBLE_FUNCTION_APPLICATION});    // whitespace -- make part of mrow to keep out of parse
    4349
    1
                  parse_stack.push(top_of_stack);
    4350
    1
                  continue;
    4351
    6.55k
                }
    4352
                // consecutive operands -- add an invisible operator as appropriate
    4353
    6.55k
                current_op = if likely_function_name == FunctionNameCertainty::True {
    4354
    753
                      OperatorPair{ ch: "\u{2061}", op: *INVISIBLE_FUNCTION_APPLICATION }
    4355
    5.79k
                    } else if self.is_mixed_fraction(previous_child, &children[i_child..])?0 {
    4356
    70
                      OperatorPair{ ch: "\u{2064}", op: *IMPLIED_INVISIBLE_PLUS }
    4357
    5.72k
                    } else if self.is_implied_comma(previous_child, current_child, mrow) {
    4358
    81
                      OperatorPair{ch: "\u{2063}", op: *IMPLIED_INVISIBLE_COMMA }
    4359
    5.64k
                    } else if self.is_implied_chemical_bond(previous_child, current_child) {
    4360
    419
                      OperatorPair{ch: "\u{2063}", op: &IMPLIED_CHEMICAL_BOND }
    4361
    5.22k
                    } else if self.is_implied_separator(previous_child, current_child) {
    4362
    166
                      OperatorPair{ch: "\u{2063}", op: &IMPLIED_SEPARATOR_HIGH_PRIORITY }
    4363
    5.06k
                    } else if self.is_trig_arg(base_of_previous_child, base_of_child, &mut parse_stack) {
    4364
    9
                      OperatorPair{ch: "\u{2062}", op: &IMPLIED_TIMES_HIGH_PRIORITY }
    4365
                    } else {
    4366
    5.05k
                      OperatorPair{ ch: "\u{2062}", op: *IMPLIED_TIMES }
    4367
                    };
    4368
    6.55k
                if let Some(attr_val262) = base_of_child.attribute_value(CHANGED_ATTR)
    4369
    262
                  && attr_val == "data-was-mo" {
    4370
    0
                    // it really should be an operator
    4371
    0
                    base_of_child.remove_attribute(CHANGED_ATTR);
    4372
    0
                    set_mathml_name(base_of_child, "mo");
    4373
    6.55k
                  }
    4374
    6.55k
                if name(base_of_child) == "mo" {
    4375
    1
                  current_op.ch = as_text(base_of_child);
    4376
    1
                  // debug!("  Found whitespace op '{}'/{}", show_invisible_op_char(current_op.ch), current_op.op.priority);
    4377
    1
                } else {
    4378
    6.54k
                  let implied_mo = create_mo(current_child.document(), current_op.ch, ADDED_ATTR_VALUE);
    4379
    6.54k
                  if likely_function_name == FunctionNameCertainty::Maybe {
    4380
    33
                    implied_mo.set_attribute_value("data-function-guess", "true");
    4381
    6.51k
                  }
    4382
                  // debug!("  Found implicit op {}/{} [{:?}]", show_invisible_op_char(current_op.ch), current_op.op.priority, likely_function_name);
    4383
    6.54k
                  self.reduce_stack(&mut parse_stack, current_op.op.priority);
    4384
    6.54k
                  let shift_result = self.shift_stack(&mut parse_stack, implied_mo, current_op.clone());
    4385
                  // ignore shift_result.0 which is just 'implied_mo'
    4386
    6.54k
                  assert_eq!(implied_mo, shift_result.0);
    4387
    6.54k
                  assert!( ptr_eq(current_op.op, shift_result.1.op) );
    4388
    6.54k
                  let mut top_of_stack = parse_stack.pop().unwrap();
    4389
    6.54k
                  top_of_stack.add_child_to_mrow(implied_mo, current_op);
    4390
    6.54k
                  parse_stack.push(top_of_stack);
    4391
    6.54k
                  current_op = OperatorPair::new();
    4392
                }
    4393
    11.3k
              }
    4394
    4.88k
            }
    4395
          }
    4396
    4397
    36.7k
          if !ptr_eq(current_op.op, &ILLEGAL_OPERATOR_INFO) {
    4398
    13.9k
            if current_op.op.is_left_fence() || current_op.op12.0k.is_prefix12.0k() {
    4399
    2.95k
              if top(&parse_stack).is_operand {
    4400
                // will end up with duplicate operands -- need to choose operator associated with prev child
    4401
                // we use the original input here because in this case, we need to look to the right of the ()s to deal with chemical states
    4402
    232
                let likely_function_name = self.is_function_name(as_element(children[i_child-1]), Some(&children[i_child..]));
    4403
    232
                let implied_operator = if likely_function_name== FunctionNameCertainty::True {
    4404
    98
                    OperatorPair{ ch: "\u{2061}", op: *INVISIBLE_FUNCTION_APPLICATION }
    4405
                  } else {
    4406
    134
                    OperatorPair{ ch: "\u{2062}", op: *IMPLIED_TIMES }
    4407
                  };
    4408
                // debug!("  adding implied {}", if ptr_eq(implied_operator.op,*IMPLIED_TIMES) {"times"} else {"function apply"});
    4409
    4410
    232
                let implied_mo = create_mo(current_child.document(), implied_operator.ch, ADDED_ATTR_VALUE);
    4411
    232
                if likely_function_name == FunctionNameCertainty::Maybe {
    4412
    55
                  implied_mo.set_attribute_value("data-function-guess", "true");
    4413
    177
                }
    4414
    232
                self.reduce_stack(&mut parse_stack, implied_operator.op.priority);            let shift_result = self.shift_stack(&mut parse_stack, implied_mo, implied_operator.clone());
    4415
                // ignore shift_result.0 which is just 'implied_mo'
    4416
    232
                assert_eq!(implied_mo, shift_result.0);
    4417
    232
                assert!( ptr_eq(implied_operator.op, shift_result.1.op) );
    4418
    232
                let mut top_of_stack = parse_stack.pop().unwrap();
    4419
    232
                top_of_stack.add_child_to_mrow(implied_mo, implied_operator);
    4420
    232
                parse_stack.push(top_of_stack);
    4421
    2.72k
              }
    4422
              // starting a new mrow
    4423
    2.95k
              parse_stack.push( StackInfo::new(current_child.document()) );
    4424
            } else {
    4425
              // One of infix, postfix, or right fence -- all should have a left operand
    4426
              // pop the stack if it is lower precedence (it forms an mrow)
    4427
    4428
              // hack to get linear mixed fractions to parse correctly
    4429
    10.9k
              if current_op.ch == "/" && top(&parse_stack).op_pair.ch == "\u{2064}"41 {
    4430
    2
                  current_op.op = &IMPLIED_PLUS_SLASH_HIGH_PRIORITY;
    4431
    10.9k
              }
    4432
    10.9k
              self.reduce_stack(&mut parse_stack, current_op.op.priority);
    4433
              // push new operator on stack (already handled n-ary case)
    4434
    10.9k
              let shift_result = self.shift_stack(&mut parse_stack, current_child, current_op);
    4435
    10.9k
              current_child = shift_result.0;
    4436
    10.9k
              current_op = shift_result.1;
    4437
            }
    4438
    22.7k
          }
    4439
    36.7k
          let mut top_of_stack = parse_stack.pop().unwrap();
    4440
    36.7k
          top_of_stack.add_child_to_mrow(current_child, current_op);
    4441
    36.7k
          parse_stack.push(top_of_stack);
    4442
        }
    4443
    4444
        // Reached the end -- force reduction of what's left on the stack
    4445
    7.48k
        self.reduce_stack(&mut parse_stack, LEFT_FENCEPOST.priority);
    4446
    4447
        // We essentially have 'terminator( mrow terminator)'
    4448
        //   in other words, we have an extra mrow with one child due to the initial start -- remove it
    4449
    7.48k
        let mut top_of_stack = parse_stack.pop().unwrap();
    4450
    7.48k
        assert_eq!(parse_stack.len(), 0);
    4451
    4452
    7.48k
        let mut parsed_mrow = top_of_stack.mrow;
    4453
    7.48k
        assert_eq!( name(top_of_stack.mrow), "mrow");
    4454
    7.48k
        if parsed_mrow.children().len() == 1 && is_ok_to_merge_child {
    4455
    7.46k
          parsed_mrow = top_of_stack.remove_last_operand_from_mrow();
    4456
    7.46k
          // was synthesized, but is really the original top level mrow
    4457
    7.46k
        }15
    4458
    4459
    7.48k
        parsed_mrow.remove_attribute(CHANGED_ATTR);
    4460
    7.48k
        return Ok( add_attrs(parsed_mrow, &saved_mrow_attrs) );
    4461
    7.48k
      }
    4462
    }
    4463
    4464
    // ---------------- useful utility functions --------------------
    4465
    102k
    fn top<'s, 'a:'s, 'op:'a>(vec: &'s[StackInfo<'a, 'op>]) -> &'s StackInfo<'a, 'op> {
    4466
    102k
      return &vec[vec.len()-1];
    4467
    102k
    }
    4468
    // Replace the attrs of 'mathml' with 'attrs' and keep the global attrs of 'mathml' (i.e, lift 'attrs' to 'mathml' for replacing children)
    4469
    10.0k
    pub fn add_attrs<'a>(mathml: Element<'a>, attrs: &[Attribute]) -> Element<'a> {
    4470
      static GLOBAL_ATTRS: phf::Set<&str> = phf_set! {
    4471
        "class", "dir", "displaystyle", "id", "mathbackground", "mathcolor", "mathsize",
    4472
        "mathvariant", "nonce", "scriptlevel", "style", "tabindex",
    4473
        "intent", "arg",
    4474
      };
    4475
    4476
      // debug!(   "Adding back {} attr(s) to {}", attrs.len(), name(mathml));
    4477
      // remove non-global attrs
    4478
    10.0k
      for attr740 in mathml.attributes() {
    4479
    740
        let attr_name = attr.name().local_part();
    4480
    740
        if !( attr_name.starts_with("data-") || GLOBAL_ATTRS534.contains534(attr_name534) ||
    4481
    278
              attr_name.starts_with("on") ) {     // allows too much - cheapo way to allow event handlers like "onchange"
    4482
    278
          mathml.remove_attribute(attr.name());
    4483
    462
        }
    4484
      }
    4485
    4486
      // add in 'attrs'
    4487
    10.0k
      for attr5.22k in attrs {
    4488
    5.22k
        mathml.set_attribute_value(attr.name(), attr.value());
    4489
    5.22k
      }
    4490
    10.0k
      return mathml;
    4491
    10.0k
    }
    4492
    4493
    4494
    2.91M
    pub fn name(node: Element<'_>) -> &str {
    4495
    2.91M
      return node.name().local_part();
    4496
    2.91M
    }
    4497
    4498
    /// The child of a non-leaf element must be an element
    4499
    // Note: can't use references as that results in 'returning use of local variable'
    4500
    1.14M
    pub fn as_element(child: ChildOfElement) -> Element {
    4501
    1.14M
      return match child {
    4502
    1.14M
        ChildOfElement::Element(e) => e,
    4503
        _ => {
    4504
    0
          panic!("as_element: internal error -- found non-element child (text? '{:?}')", child.text());
    4505
        },
    4506
      };
    4507
    1.14M
    }
    4508
    4509
    /// The child of a leaf element must be text (previously trimmed)
    4510
    /// Note: trim() combines all the Text children into a single string
    4511
    603k
    pub fn as_text(leaf_child: Element<'_>) -> &str {
    4512
    603k
      assert!(is_leaf(leaf_child));
    4513
    603k
      let children = leaf_child.children();
    4514
    603k
      if children.is_empty() {
    4515
    401
        return "";
    4516
    602k
      }
    4517
    602k
      assert!(children.len() == 1);
    4518
    602k
      return match children[0] {
    4519
    602k
        ChildOfElement::Text(t) => t.text(),
    4520
    0
        _ => panic!("as_text: internal error -- found non-text child of leaf element"),
    4521
      }
    4522
    603k
    }
    4523
    4524
    /// Returns the parent of the argument.
    4525
    /// Warning: this assumes the parent exists
    4526
    239k
    pub fn get_parent(mathml: Element) -> Element {
    4527
    239k
      return mathml.parent().unwrap().element().unwrap();
    4528
    239k
    }
    4529
    4530
    #[allow(dead_code)] // for debugging
    4531
    0
    pub fn element_summary(mathml: Element) -> String {
    4532
    0
      return format!("{}<{}>", name(mathml),
    4533
    0
                    if is_leaf(mathml) {show_invisible_op_char(as_text(mathml)).to_string()}
    4534
              else
    4535
    0
                         {mathml.children().len().to_string()});
    4536
    0
    }
    4537
    4538
    6.86k
    fn create_mo<'a, 'd:'a>(doc: Document<'d>, ch: &'a str, attr_value: &str) -> Element<'d> {
    4539
    6.86k
      let implied_mo = create_mathml_element(&doc, "mo");
    4540
    6.86k
      implied_mo.set_attribute_value(CHANGED_ATTR, attr_value);
    4541
    6.86k
      let mo_text = doc.create_text(ch);
    4542
    6.86k
      implied_mo.append_child(mo_text);
    4543
    6.86k
      return implied_mo;
    4544
    6.86k
    }
    4545
    4546
    /// return 'node' or if it is adorned, return its base (recursive)
    4547
    130k
    pub fn get_possible_embellished_node(node: Element) -> Element {
    4548
    130k
      let mut node = node;
    4549
    138k
      while IsNode::is_modified(node) {
    4550
    8.56k
        node = as_element(node.children()[0]);
    4551
    8.56k
      }
    4552
    130k
      return node;
    4553
    130k
    }
    4554
    4555
    #[allow(dead_code)] // for debugging with println
    4556
    0
    fn show_invisible_op_char(ch: &str) -> &str {
    4557
    0
      return match ch.chars().next().unwrap() {
    4558
    0
        '\u{2061}' => "&#x2061;",
    4559
    0
        '\u{2062}' => "&#x2062;",
    4560
    0
        '\u{2063}' => "&#x2063;",
    4561
    0
        '\u{2064}' => "&#x2064;",
    4562
    0
        '\u{E000}' => "&#xE000;",
    4563
    0
        _        => ch
    4564
      };
    4565
    0
    }
    4566
    4567
    4568
    #[cfg(test)]
    4569
    mod canonicalize_tests {
    4570
      use crate::errors::Result;
    4571
      use crate::{are_strs_canonically_equal_result, are_strs_canonically_equal_with_locale};
    4572
    4573
    #[allow(unused_imports)]
    4574
      use super::super::init_logger;
    4575
      use super::super::abs_rules_dir_path;
    4576
        use super::*;
    4577
        use sxd_document::parser;
    4578
    4579
    4580
        #[test]
    4581
    1
        fn canonical_same() -> Result<()> {
    4582
    1
            let target_str = "<math><mrow><mo>-</mo><mi>a</mi></mrow></math>";
    4583
    1
            are_strs_canonically_equal_result(target_str, target_str, &[])
    4584
    1
        }
    4585
    4586
      #[test]
    4587
    1
        fn plane1_common() -> Result<()> {
    4588
    1
            let test_str = "<math>
    4589
    1
            <mi mathvariant='normal'>sin</mi> <mo>,</mo>    <!-- shouldn't change -->
    4590
    1
            <mi mathvariant='italic'>bB4</mi> <mo>,</mo>    <!-- shouldn't change -->
    4591
    1
            <mi mathvariant='bold'>a</mi> <mo>,</mo>      <!-- single char id tests -->
    4592
    1
            <mi mathvariant='bold'>Z</mi> <mo>,</mo>
    4593
    1
            <mn mathvariant='bold'>19=&#x1D7D7;</mn> <mo>,</mo> <!-- '=' and plane1 shouldn't change -->
    4594
    1
            <mn mathvariant='double-struck'>024689</mn> <mo>,</mo>  <!-- '=' and plane1 shouldn't change -->
    4595
    1
            <mi mathvariant='double-struck'>yzCHNPQRZ</mi> <mo>,</mo>
    4596
    1
            <mi mathvariant='fraktur'>0yACHIRZ</mi> <mo>,</mo>  <!-- 0 stays as ASCII -->
    4597
    1
            <mi mathvariant='bold-fraktur'>nC</mi> <mo>,</mo>
    4598
    1
            <mi mathvariant='script'>ABEFHILMRegow</mi> <mo>,</mo>
    4599
    1
            <msup>
    4600
    1
              <mi mathvariant='bold-script'>fG</mi>
    4601
    1
              <mo mathvariant='bold-script'>*</mo>        <!-- '*' shouldn't change -->
    4602
    1
            </msup>
    4603
    1
          </math>";
    4604
    1
            let target_str = "<math>
    4605
    1
          <mrow data-changed='added'>
    4606
    1
            <mi mathvariant='normal'>sin</mi>
    4607
    1
            <mo >,</mo>
    4608
    1
            <mi mathvariant='italic'>bB4</mi>
    4609
    1
            <mo>,</mo>
    4610
    1
            <mi mathvariant='bold'>𝐚</mi>
    4611
    1
            <mo>,</mo>
    4612
    1
            <mi mathvariant='bold'>𝐙</mi>
    4613
    1
            <mo>,</mo>
    4614
    1
            <mn mathvariant='bold'>𝟏𝟗=𝟗</mn>
    4615
    1
            <mo>,</mo>
    4616
    1
            <mn mathvariant='double-struck'>𝟘𝟚𝟜𝟞𝟠𝟡</mn>
    4617
    1
            <mo>,</mo>
    4618
    1
            <mi mathvariant='double-struck'>𝕪𝕫ℂℍℕℙℚℝℤ</mi>
    4619
    1
            <mo>,</mo>
    4620
    1
            <mi mathvariant='fraktur'>0𝔶𝔄ℭℌℑℜℨ</mi>
    4621
    1
            <mo>,</mo>
    4622
    1
            <mi mathvariant='bold-fraktur'>𝖓𝕮</mi>
    4623
    1
            <mo>,</mo>
    4624
    1
            <mi mathvariant='script'>𝒜ℬℰℱℋℐℒℳℛℯℊℴ𝓌</mi>
    4625
    1
            <mo>,</mo>
    4626
    1
            <msup>
    4627
    1
              <mi mathvariant='bold-script'>𝓯𝓖</mi>
    4628
    1
              <mo mathvariant='bold-script'>*</mo>        <!-- '*' shouldn't change -->
    4629
    1
            </msup>
    4630
    1
          </mrow>
    4631
    1
        </math>";
    4632
    1
        are_strs_canonically_equal_result(test_str, target_str, &[])
    4633
    1
      }
    4634
    4635
      #[test]
    4636
    1
        fn plane1_font_styles() -> Result<()> {
    4637
    1
            let test_str = "<math>
    4638
    1
            <mi mathvariant='sans-serif'>aA09=</mi> <mo>,</mo>      <!-- '=' shouldn't change -->
    4639
    1
            <mi mathvariant='bold-sans-serif'>zZ09</mi> <mo>,</mo>
    4640
    1
            <mi mathvariant='sans-serif-italic'>azAZ09</mi> <mo>,</mo>  <!-- italic digits don't exist: revert to sans-serif -->
    4641
    1
            <mi mathvariant='sans-serif-bold-italic'>AZaz09</mi> <mo>,</mo> <!--  italic digits don't exist: revert to just bold -->
    4642
    1
            <mi mathvariant='monospace'>aA09</mi>
    4643
    1
          </math>";
    4644
    1
            let target_str = "<math>
    4645
    1
            <mrow data-changed='added'>
    4646
    1
              <mi mathvariant='sans-serif'>𝖺𝖠𝟢𝟫=</mi>
    4647
    1
              <mo>,</mo>
    4648
    1
              <mi mathvariant='bold-sans-serif'>𝘇𝗭𝟬𝟵</mi>
    4649
    1
              <mo>,</mo>
    4650
    1
              <mi mathvariant='sans-serif-italic'>𝘢𝘻𝘈𝘡𝟢𝟫</mi>
    4651
    1
              <mo>,</mo>
    4652
    1
              <mi mathvariant='sans-serif-bold-italic'>𝘼𝙕𝙖𝙯𝟬𝟵</mi>
    4653
    1
              <mo>,</mo>
    4654
    1
              <mi mathvariant='monospace'>𝚊𝙰𝟶𝟿</mi>
    4655
    1
            </mrow>
    4656
    1
          </math>";
    4657
    1
        are_strs_canonically_equal_result(test_str, target_str, &[])
    4658
    1
      }
    4659
    4660
      #[test]
    4661
    1
        fn plane1_greek() -> Result<()> {
    4662
    1
            let test_str = "<math>
    4663
    1
            <mi mathvariant='normal'>ΑΩαω∇∂ϵ=</mi> <mo>,</mo>    <!-- shouldn't change -->
    4664
    1
            <mi mathvariant='italic'>ϴΑΩαω∇∂ϵ</mi> <mo>,</mo>
    4665
    1
            <mi mathvariant='bold'>ΑΩαωϝϜ</mi> <mo>,</mo>
    4666
    1
            <mi mathvariant='double-struck'>Σβ∇</mi> <mo>,</mo>   <!-- shouldn't change -->
    4667
    1
            <mi mathvariant='fraktur'>ΞΦλϱ</mi> <mo>,</mo>      <!-- shouldn't change -->
    4668
    1
            <mi mathvariant='bold-fraktur'>ψΓ</mi> <mo>,</mo>   <!-- map to bold -->
    4669
    1
            <mi mathvariant='script'>μΨ</mi> <mo>,</mo>       <!-- shouldn't change -->
    4670
    1
            <mi mathvariant='bold-script'>Σπ</mi>         <!-- map to bold -->
    4671
    1
          </math>";
    4672
    1
            let target_str = "<math>
    4673
    1
            <mrow data-changed='added'>
    4674
    1
              <mi mathvariant='normal'>ΑΩαω∇∂ϵ=</mi>
    4675
    1
              <mo>,</mo>
    4676
    1
              <mi mathvariant='italic'>𝛳𝛢𝛺𝛼𝜔𝛻𝜕𝜖</mi>
    4677
    1
              <mo>,</mo>
    4678
    1
              <mi mathvariant='bold'>𝚨𝛀𝛂𝛚𝟋𝟊</mi>
    4679
    1
              <mo>,</mo>
    4680
    1
              <mi mathvariant='double-struck'>Σβ∇</mi>
    4681
    1
              <mo>,</mo>
    4682
    1
              <mi mathvariant='fraktur'>ΞΦλϱ</mi>
    4683
    1
              <mo>,</mo>
    4684
    1
              <mi mathvariant='bold-fraktur'>𝛙𝚪</mi>
    4685
    1
              <mo>,</mo>
    4686
    1
              <mi mathvariant='script'>μΨ</mi>
    4687
    1
              <mo>,</mo>
    4688
    1
              <mi mathvariant='bold-script'>𝚺𝛑</mi>
    4689
    1
            </mrow>
    4690
    1
          </math>";
    4691
    1
        are_strs_canonically_equal_result(test_str, target_str, &[])
    4692
    1
      }
    4693
    4694
      #[test]
    4695
    1
        fn plane1_greek_font_styles() -> Result<()> {
    4696
    1
            let test_str = "<math>
    4697
    1
            <mi mathvariant='sans-serif'>ΑΩαω∇∂ϵ=</mi> <mo>,</mo>      <!-- '=' shouldn't change -->
    4698
    1
            <mi mathvariant='bold-sans-serif'>ϴ0ΑΩαω∇∂ϵ</mi> <mo>,</mo>
    4699
    1
            <mi mathvariant='sans-serif-italic'>aΑΩαω∇∂ϵ</mi> <mo>,</mo> <!-- italic digits don't exist: revert to sans-serif -->
    4700
    1
            <mi mathvariant='sans-serif-bold-italic'>ZΑΩαωϰϕϱϖ</mi> <mo>,</mo>  <!--  italic digits don't exist: revert to just bold -->
    4701
    1
            <mi mathvariant='monospace'>zΑΩαω∇∂</mi>
    4702
    1
          </math>";
    4703
    1
            let target_str = "<math>
    4704
    1
            <mrow data-changed='added'>
    4705
    1
              <mi mathvariant='sans-serif'>ΑΩαω∇∂ϵ=</mi>
    4706
    1
              <mo>,</mo>
    4707
    1
              <mi mathvariant='bold-sans-serif'>𝝧𝟬𝝖𝝮𝝰𝞈𝝯𝞉𝞊</mi>
    4708
    1
              <mo>,</mo>
    4709
    1
              <mi mathvariant='sans-serif-italic'>𝘢ΑΩαω∇∂ϵ</mi>
    4710
    1
              <mo>,</mo>
    4711
    1
              <mi mathvariant='sans-serif-bold-italic'>𝙕𝞐𝞨𝞪𝟂𝟆𝟇𝟈𝟉</mi>
    4712
    1
              <mo>,</mo>
    4713
    1
              <mi mathvariant='monospace'>𝚣ΑΩαω∇∂</mi>
    4714
    1
            </mrow>
    4715
    1
          </math>";
    4716
    1
        are_strs_canonically_equal_result(test_str, target_str, &[])
    4717
    1
      }
    4718
    4719
        #[test]
    4720
    1
        fn short_and_long_dash() -> Result<()> {
    4721
    1
            let test_str = "<math><mi>x</mi> <mo>=</mo> <mi>--</mi><mo>+</mo><mtext>----</mtext></math>";
    4722
    1
            let target_str = "<math>
    4723
    1
          <mrow data-changed='added'>
    4724
    1
          <mi>x</mi>
    4725
    1
          <mo>=</mo>
    4726
    1
          <mrow data-changed='added'>
    4727
    1
            <mi>—</mi>
    4728
    1
            <mo>+</mo>
    4729
    1
            <mtext>―</mtext>
    4730
    1
          </mrow>
    4731
    1
          </mrow>
    4732
    1
        </math>";
    4733
    1
        are_strs_canonically_equal_result(test_str, target_str, &[])
    4734
    1
        }
    4735
    4736
        #[test]
    4737
    1
        fn illegal_mathml_element() {
    4738
        use crate::interface::*;
    4739
    1
            let test_str = "<math><foo><mi>f</mi></foo></math>";
    4740
    1
            let package1 = &parser::parse(test_str).expect("Failed to parse test input");
    4741
    1
        let mathml = get_element(package1);
    4742
    1
        trim_element(mathml, false);
    4743
    1
        assert!(canonicalize(mathml).is_err());
    4744
    1
        }
    4745
    4746
        #[test]
    4747
    1
        fn illegal_mtd_element() {
    4748
        use crate::interface::*;
    4749
    1
            let test_str = "<math>
    4750
    1
          <mtable>
    4751
    1
            <mtr>
    4752
    1
              <mtd>
    4753
    1
              <mtext></mtext>
    4754
    1
              </mtd>
    4755
    1
              <mrow>
    4756
    1
              <mi>E</mi>
    4757
    1
              <mo>=</mo>
    4758
    1
              <mrow>
    4759
    1
              <mtd>
    4760
    1
                <mi>m</mi>
    4761
    1
                <mo>⁢<!--INVISIBLE TIMES--></mo>
    4762
    1
                <msup>
    4763
    1
                <mi>c</mi>
    4764
    1
                <mn>2</mn>
    4765
    1
                </msup>
    4766
    1
                </mtd></mrow>
    4767
    1
              </mrow>
    4768
    1
    4769
    1
            </mtr>
    4770
    1
          </mtable>
    4771
    1
        </math>";
    4772
    1
            let package1 = &parser::parse(test_str).expect("Failed to parse test input");
    4773
    1
        let mathml = get_element(package1);
    4774
    1
        trim_element(mathml, false);
    4775
    1
        assert!(canonicalize(mathml).is_err());
    4776
    1
        }
    4777
    4778
    4779
        #[test]
    4780
    1
        fn a_to_mrow() -> Result<()> {
    4781
    1
            let test_str = "<math>
    4782
    1
          <a href='https://www.example.com'>
    4783
    1
            <mo>(</mo>
    4784
    1
            <a href='#its_relative'>
    4785
    1
              <mi>x</mi>
    4786
    1
              <mo>,</mo>
    4787
    1
              <mi>y</mi>
    4788
    1
            </a>
    4789
    1
            <mo>)</mo>
    4790
    1
          </a>
    4791
    1
          </math>
    4792
    1
    ";
    4793
    1
            let target_str = " <math>
    4794
    1
          <mrow href='https://www.example.com'>
    4795
    1
            <mo>(</mo>
    4796
    1
            <mrow href='#its_relative'>
    4797
    1
            <mi>x</mi>
    4798
    1
            <mo>,</mo>
    4799
    1
            <mi>y</mi>
    4800
    1
            </mrow>
    4801
    1
            <mo>)</mo>
    4802
    1
          </mrow>
    4803
    1
        </math>";
    4804
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    4805
    1
        }
    4806
    4807
        #[test]
    4808
    1
        fn mfenced_no_children() -> Result<()> {
    4809
    1
            let test_str = "<math><mi>f</mi><mfenced><mrow/></mfenced></math>";
    4810
    1
            let target_str = "<math>
    4811
    1
          <mrow data-changed='added'>
    4812
    1
            <mi>f</mi>
    4813
    1
            <mo data-changed='added'>&#x2061;</mo>
    4814
    1
            <mrow>
    4815
    1
              <mo data-changed='from_mfenced'>(</mo>
    4816
    1
              <mo data-changed='from_mfenced'>)</mo>
    4817
    1
            </mrow>
    4818
    1
          </mrow>
    4819
    1
        </math>";
    4820
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    4821
    1
        }
    4822
    4823
        #[test]
    4824
    1
        fn mfenced_one_child() -> Result<()> {
    4825
    1
            let test_str = "<math><mi>f</mi><mfenced open='[' close=']'><mi>x</mi></mfenced></math>";
    4826
    1
            let target_str = " <math>
    4827
    1
          <mrow data-changed='added'>
    4828
    1
          <mi>f</mi>
    4829
    1
          <mo data-changed='added'>&#x2061;</mo>
    4830
    1
          <mrow>
    4831
    1
            <mo data-changed='from_mfenced'>[</mo>
    4832
    1
            <mi>x</mi>
    4833
    1
            <mo data-changed='from_mfenced'>]</mo>
    4834
    1
          </mrow>
    4835
    1
          </mrow>
    4836
    1
        </math>";
    4837
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    4838
    1
        }
    4839
    4840
        #[test]
    4841
    1
        fn mfenced_no_attrs() -> Result<()> {
    4842
    1
            let test_str = "<math><mi>f</mi><mfenced><mrow><mi>x</mi><mo>,</mo><mi>y</mi><mo>,</mo><mi>z</mi></mrow></mfenced></math>";
    4843
    1
            let target_str = " <math>
    4844
    1
          <mrow data-changed='added'>
    4845
    1
          <mi>f</mi>
    4846
    1
          <mo data-changed='added'>&#x2061;</mo>
    4847
    1
          <mrow>
    4848
    1
            <mo data-changed='from_mfenced'>(</mo>
    4849
    1
            <mrow>
    4850
    1
            <mi>x</mi>
    4851
    1
            <mo>,</mo>
    4852
    1
            <mi>y</mi>
    4853
    1
            <mo>,</mo>
    4854
    1
            <mi>z</mi>
    4855
    1
            </mrow>
    4856
    1
            <mo data-changed='from_mfenced'>)</mo>
    4857
    1
          </mrow>
    4858
    1
          </mrow>
    4859
    1
        </math>";
    4860
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    4861
    1
        }
    4862
    4863
        #[test]
    4864
    1
        fn mfenced_with_separators() -> Result<()> {
    4865
    1
            let test_str = "<math><mi>f</mi><mfenced separators=',;'><mi>x</mi><mi>y</mi><mi>z</mi><mi>a</mi></mfenced></math>";
    4866
    1
            let target_str = "<math>
    4867
    1
          <mrow data-changed='added'>
    4868
    1
          <mi>f</mi>
    4869
    1
          <mo data-changed='added'>&#x2061;</mo>
    4870
    1
          <mrow>
    4871
    1
            <mo data-changed='from_mfenced'>(</mo>
    4872
    1
            <mrow data-changed='added'>
    4873
    1
            <mrow data-changed='added'>
    4874
    1
              <mi>x</mi>
    4875
    1
              <mo data-changed='from_mfenced'>,</mo>
    4876
    1
              <mi>y</mi>
    4877
    1
            </mrow>
    4878
    1
            <mo data-changed='from_mfenced'>;</mo>
    4879
    1
            <mrow data-changed='added'>
    4880
    1
              <mi>z</mi>
    4881
    1
              <mo data-changed='from_mfenced'>,</mo>
    4882
    1
              <mi>a</mi>
    4883
    1
            </mrow>
    4884
    1
            </mrow>
    4885
    1
            <mo data-changed='from_mfenced'>)</mo>
    4886
    1
          </mrow>
    4887
    1
          </mrow>
    4888
    1
        </math>";
    4889
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    4890
    1
        }
    4891
    4892
        #[test]
    4893
    1
        fn canonical_one_element_mrow_around_mrow() -> Result<()> {
    4894
    1
            let test_str = "<math><mrow><mrow><mo>-</mo><mi>a</mi></mrow></mrow></math>";
    4895
    1
            let target_str = "<math><mrow><mo>-</mo><mi>a</mi></mrow></math>";
    4896
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    4897
    1
        }
    4898
    4899
        #[test]
    4900
    1
        fn canonical_mtext_in_mtd_477() -> Result<()> {
    4901
        // make sure mtext doesn't go away
    4902
    1
            let test_str = r#"<math>
    4903
    1
          <mtable>
    4904
    1
            <mtr>
    4905
    1
              <mtd>
    4906
    1
                <mstyle scriptlevel="0">
    4907
    1
                  <mspace width="2em"/>
    4908
    1
                </mstyle>
    4909
    1
                <mstyle scriptlevel="0">
    4910
    1
                  <mspace width="1em"/>
    4911
    1
                </mstyle>
    4912
    1
              </mtd>
    4913
    1
            </mtr>
    4914
    1
          </mtable>
    4915
    1
        </math>"#;
    4916
    1
            let target_str = r#"   <math>
    4917
    1
          <mtable>
    4918
    1
            <mtr>
    4919
    1
            <mtd>
    4920
    1
              <mtext data-width='1' data-following-space-width='4' scriptlevel='0' data-changed='added'> </mtext>
    4921
    1
            </mtd>
    4922
    1
            </mtr>
    4923
    1
          </mtable>
    4924
    1
        </math>"#;
    4925
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    4926
    1
        }
    4927
    4928
        #[test]
    4929
    1
        fn canonical_mtext_in_mtr() -> Result<()> {
    4930
        // make sure mtext doesn't go away
    4931
    1
            let test_str = "<math> <mtable> <mtr> <mtext> </mtext> </mtr> <mtr> <mtext> </mtext> </mtr> </mtable> </math>";
    4932
    1
            let target_str = "   <math>
    4933
    1
          <mtable>
    4934
    1
            <mtr>
    4935
    1
              <mtext data-changed='empty_content' data-width='0' data-empty-in-2D='true'> </mtext>
    4936
    1
            </mtr>
    4937
    1
            <mtr>
    4938
    1
              <mtext data-changed='empty_content' data-width='0' data-empty-in-2D='true'> </mtext>
    4939
    1
            </mtr>
    4940
    1
          </mtable>
    4941
    1
        </math>";
    4942
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    4943
    1
        }
    4944
    4945
        #[test]
    4946
    1
        fn canonical_mtext_in_mtable() -> Result<()> {
    4947
        // make sure mtext doesn't go away
    4948
    1
            let test_str = r"<math> <mtable> <mtr> <mtd> <mi>L</mi> </mtd> <mtd> <mrow> <mi>&lt;mi/&gt;</mi> <mo>=</mo>
    4949
    1
                <mrow> <mo>[</mo> <mtable> <mtext> </mtext> </mtable> <mo>]</mo> </mrow> </mrow> </mtd> </mtr> </mtable> </math>";
    4950
    1
            let target_str = r"<math>
    4951
    1
          <mtable>
    4952
    1
          <mtr>
    4953
    1
            <mtd>
    4954
    1
            <mi>L</mi>
    4955
    1
            </mtd>
    4956
    1
            <mtd>
    4957
    1
            <mrow>
    4958
    1
              <mi>&lt;mi/&gt;</mi>
    4959
    1
              <mo>=</mo>
    4960
    1
              <mrow>
    4961
    1
              <mo>[</mo>
    4962
    1
              <mtable>
    4963
    1
                <mtext data-changed='empty_content' data-width='0' data-empty-in-2D='true'> </mtext>
    4964
    1
              </mtable>
    4965
    1
              <mo>]</mo>
    4966
    1
              </mrow>
    4967
    1
            </mrow>
    4968
    1
            </mtd>
    4969
    1
          </mtr>
    4970
    1
          </mtable>
    4971
    1
        </math>";
    4972
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    4973
    1
        }
    4974
    4975
        #[test]
    4976
    1
        fn mrow_with_intent_and_single_child() -> Result<()> {
    4977
        use crate::interface::*;
    4978
        use sxd_document::parser;
    4979
        use crate::canonicalize::canonicalize;
    4980
        // this forces initialization
    4981
    1
        crate::interface::set_rules_dir(abs_rules_dir_path()).unwrap();
    4982
    1
        crate::speech::SPEECH_RULES.with(|_| true);
    4983
    4984
        // we don't want to remove the mrow because the intent on the mi would reference itself
    4985
    1
            let test = "<math><mrow intent='log($x)'><mi arg='x'>X</mi></mrow></math>";
    4986
    4987
    1
        let package1 = &parser::parse(test).expect("Failed to parse test input");
    4988
    1
        let mathml = get_element(package1);
    4989
    1
        trim_element(mathml, false);
    4990
    1
        let mathml_test = canonicalize(mathml).unwrap();
    4991
    1
        let first_child = as_element( mathml_test.children()[0] );
    4992
    1
        assert_eq!(name(first_child), "mrow");
    4993
    1
        assert_eq!(first_child.children().len(), 1);
    4994
    1
        let mi = as_element(first_child.children()[0]);
    4995
    1
        assert_eq!(name(mi), "mi");
    4996
    1
        Ok(())
    4997
    1
        }
    4998
    4999
        #[test]
    5000
    1
        fn empty_mrow_with_intent() -> Result<()> {
    5001
        // we don't want to remove the mrow because the intent on the mi would reference itself
    5002
        use crate::interface::*;
    5003
        use sxd_document::parser;
    5004
        use crate::canonicalize::canonicalize;
    5005
        // this forces initialization
    5006
    1
        crate::interface::set_rules_dir(abs_rules_dir_path()).unwrap();
    5007
    1
        crate::speech::SPEECH_RULES.with(|_| true);
    5008
    5009
        // we don't want to remove the mrow because the intent needs to stick around
    5010
    1
            let test = "<math><mrow intent='log(x)'/></math>";
    5011
    5012
    1
        let package1 = &parser::parse(test).expect("Failed to parse test input");
    5013
    1
        let mathml = get_element(package1);
    5014
    1
        trim_element(mathml, false);
    5015
    1
        let mathml_test = canonicalize(mathml).unwrap();
    5016
    1
        let first_child = as_element( mathml_test.children()[0] );
    5017
    1
        assert_eq!(name(first_child), "mrow");
    5018
    1
        assert_eq!(first_child.children().len(), 1);
    5019
    1
        let mtext = as_element(first_child.children()[0]);
    5020
    1
        assert_eq!(name(mtext), "mtext");
    5021
    1
        Ok(())
    5022
    1
        }
    5023
    5024
        #[test]
    5025
    1
        fn mn_with_negative_sign() -> Result<()> {
    5026
    1
            let test_str = "<math><mfrac>
    5027
    1
            <mrow><mn>-1</mn></mrow>
    5028
    1
            <mn>−987</mn>
    5029
    1
            </mfrac></math>";
    5030
    1
            let target_str = "<math><mfrac>
    5031
    1
          <mrow data-changed='added'><mo>-</mo><mn>1</mn></mrow>
    5032
    1
          <mrow data-changed='added'><mo>-</mo><mn>987</mn></mrow>
    5033
    1
          </mfrac></math>";
    5034
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5035
    1
        }
    5036
    5037
        #[test]
    5038
    1
        fn mn_with_degree_sign() -> Result<()> {
    5039
    1
            let test_str = "<math> <mrow> <mi>cos</mi> <mo>⁡</mo> <mrow> <mo>(</mo> <mn>150°</mn> <mo>)</mo> </mrow> </mrow> </math>";
    5040
    1
            let target_str = "<math>
    5041
    1
          <mrow>
    5042
    1
            <mi>cos</mi> <mo>&#x2061;</mo>
    5043
    1
            <mrow>
    5044
    1
              <mo>(</mo>
    5045
    1
              <msup data-changed='added'> <mn>150</mn> <mo>°</mo> </msup>
    5046
    1
              <mo>)</mo>
    5047
    1
            </mrow>
    5048
    1
          </mrow>
    5049
    1
        </math>";
    5050
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5051
    1
        }
    5052
    5053
        #[test]
    5054
    1
        fn canonical_one_element_mrow_around_mo() -> Result<()> {
    5055
    1
            let test_str = "<math><mrow><mrow><mo>-</mo></mrow><mi>a</mi></mrow></math>";
    5056
    1
            let target_str = "<math><mrow><mo>-</mo><mi>a</mi></mrow></math>";
    5057
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5058
    1
        }
    5059
    5060
        #[test]
    5061
    1
        fn canonical_flat_to_times_and_plus() -> Result<()> {
    5062
    1
            let test_str = "<math><mi>c</mi><mo>+</mo><mi>x</mi><mi>y</mi></math>";
    5063
    1
            let target_str = "<math>
    5064
    1
        <mrow data-changed='added'><mi>c</mi><mo>+</mo>
    5065
    1
          <mrow data-changed='added'><mi>x</mi><mo data-changed='added'>&#x2062;</mo><mi>y</mi></mrow>
    5066
    1
        </mrow></math>";
    5067
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5068
    1
        }
    5069
    5070
        #[test]
    5071
    1
        fn canonical_prefix_and_infix() -> Result<()> {
    5072
    1
            let test_str = "<math><mrow><mo>-</mo><mi>a</mi><mo>-</mo><mi>b</mi></mrow></math>";
    5073
    1
            let target_str = "<math>
    5074
    1
        <mrow>
    5075
    1
          <mrow data-changed='added'>
    5076
    1
          <mo>-</mo>
    5077
    1
          <mi>a</mi>
    5078
    1
          </mrow>
    5079
    1
          <mo>-</mo>
    5080
    1
          <mi>b</mi>
    5081
    1
        </mrow>
    5082
    1
         </math>";
    5083
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5084
    1
        }
    5085
    5086
    5087
        #[test]
    5088
    1
        fn canonical_prefix_implied_times_prefix() -> Result<()> {
    5089
    1
            let test_str = "<math><mrow><mo>∂</mo><mi>x</mi><mo>∂</mo><mi>y</mi></mrow></math>";
    5090
    1
            let target_str = "<math>
    5091
    1
          <mrow>
    5092
    1
          <mrow data-changed='added'><mo>∂</mo><mi>x</mi></mrow>
    5093
    1
          <mo data-changed='added'>&#x2062;</mo>
    5094
    1
          <mrow data-changed='added'><mo>∂</mo><mi>y</mi></mrow>
    5095
    1
          </mrow>
    5096
    1
        </math>";
    5097
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5098
    1
        }
    5099
    5100
        #[test]
    5101
    1
        fn function_with_single_arg() -> Result<()> {
    5102
    1
            let test_str = "<math><mrow>
    5103
    1
          <mi>sin</mi><mo>(</mo><mi>x</mi><mo>)</mo>
    5104
    1
          <mo>+</mo>
    5105
    1
          <mi>f</mi><mo>(</mo><mi>x</mi><mo>)</mo>
    5106
    1
          <mo>+</mo>
    5107
    1
          <mi>t</mi><mrow><mo>(</mo><mi>x</mi><mo>)</mo></mrow>
    5108
    1
        </mrow></math>";
    5109
    1
            let target_str = "<math>
    5110
    1
        <mrow>
    5111
    1
          <mrow data-changed='added'>
    5112
    1
          <mi>sin</mi>
    5113
    1
          <mo data-changed='added'>&#x2061;</mo>
    5114
    1
          <mrow data-changed='added'>
    5115
    1
            <mo>(</mo>
    5116
    1
            <mi>x</mi>
    5117
    1
            <mo>)</mo>
    5118
    1
          </mrow>
    5119
    1
          </mrow>
    5120
    1
          <mo>+</mo>
    5121
    1
          <mrow data-changed='added'>
    5122
    1
          <mi>f</mi>
    5123
    1
          <mo data-changed='added'>&#x2061;</mo>
    5124
    1
          <mrow data-changed='added'>
    5125
    1
            <mo>(</mo>
    5126
    1
            <mi>x</mi>
    5127
    1
            <mo>)</mo>
    5128
    1
          </mrow>
    5129
    1
          </mrow>
    5130
    1
          <mo>+</mo>
    5131
    1
          <mrow data-changed='added'>
    5132
    1
          <mi>t</mi>
    5133
    1
          <mo data-changed='added'>&#x2061;</mo>
    5134
    1
          <mrow>
    5135
    1
            <mo>(</mo>
    5136
    1
            <mi>x</mi>
    5137
    1
            <mo>)</mo>
    5138
    1
          </mrow>
    5139
    1
          </mrow>
    5140
    1
        </mrow>
    5141
    1
         </math>";
    5142
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5143
    1
        }
    5144
    5145
      #[test]
    5146
    1
      fn maybe_function() -> Result<()> {
    5147
    1
        let test_str = "<math>
    5148
    1
            <mrow>
    5149
    1
              <mi>P</mi>
    5150
    1
              <mo>(</mo>
    5151
    1
              <mi>A</mi>
    5152
    1
              <mo>∩</mo>
    5153
    1
              <mi>B</mi>
    5154
    1
              <mo>)</mo>
    5155
    1
            </mrow>
    5156
    1
          </math>";
    5157
    1
        let target_str = "<math>
    5158
    1
            <mrow>
    5159
    1
            <mi>P</mi>
    5160
    1
            <mo data-function-guess='true' data-changed='added'>&#x2062;</mo>
    5161
    1
            <mrow data-changed='added'>
    5162
    1
              <mo>(</mo>
    5163
    1
              <mrow data-changed='added'>
    5164
    1
              <mi>A</mi>
    5165
    1
              <mo>∩</mo>
    5166
    1
              <mi>B</mi>
    5167
    1
              </mrow>
    5168
    1
              <mo>)</mo>
    5169
    1
            </mrow>
    5170
    1
            </mrow>
    5171
    1
          </math>";
    5172
    1
        are_strs_canonically_equal_result(test_str, target_str, &[])
    5173
    1
      }
    5174
    5175
        #[test]
    5176
    1
        fn function_with_multiple_args() -> Result<()> {
    5177
    1
            let test_str = "<math>
    5178
    1
        <mi>sin</mi><mo>(</mo><mi>x</mi><mo>+</mo><mi>y</mi><mo>)</mo>
    5179
    1
          <mo>+</mo>
    5180
    1
         <mi>f</mi><mo>(</mo><mi>x</mi><mo>+</mo><mi>y</mi><mo>)</mo>
    5181
    1
          <mo>+</mo>
    5182
    1
         <mi>t</mi><mo>(</mo><mi>x</mi><mo>+</mo><mi>y</mi><mo>)</mo>
    5183
    1
          <mo>+</mo>
    5184
    1
         <mi>w</mi><mo>(</mo><mi>x</mi><mo>,</mo><mi>y</mi><mo>)</mo>
    5185
    1
        </math>";
    5186
    1
            let target_str = " <math>
    5187
    1
        <mrow data-changed='added'>
    5188
    1
        <mrow data-changed='added'>
    5189
    1
          <mi>sin</mi>
    5190
    1
          <mo data-changed='added'>&#x2061;</mo>
    5191
    1
          <mrow data-changed='added'>
    5192
    1
          <mo>(</mo>
    5193
    1
          <mrow data-changed='added'>
    5194
    1
            <mi>x</mi>
    5195
    1
            <mo>+</mo>
    5196
    1
            <mi>y</mi>
    5197
    1
          </mrow>
    5198
    1
          <mo>)</mo>
    5199
    1
          </mrow>
    5200
    1
        </mrow>
    5201
    1
        <mo>+</mo>
    5202
    1
        <mrow data-changed='added'>
    5203
    1
          <mi>f</mi>
    5204
    1
          <mo data-changed='added'>&#x2061;</mo>
    5205
    1
          <mrow data-changed='added'>
    5206
    1
          <mo>(</mo>
    5207
    1
          <mrow data-changed='added'>
    5208
    1
            <mi>x</mi>
    5209
    1
            <mo>+</mo>
    5210
    1
            <mi>y</mi>
    5211
    1
          </mrow>
    5212
    1
          <mo>)</mo>
    5213
    1
          </mrow>
    5214
    1
        </mrow>
    5215
    1
        <mo>+</mo>
    5216
    1
        <mrow data-changed='added'>
    5217
    1
          <mi>t</mi>
    5218
    1
          <mo data-changed='added' data-function-guess='true'>&#x2062;</mo>
    5219
    1
          <mrow data-changed='added'>
    5220
    1
          <mo>(</mo>
    5221
    1
          <mrow data-changed='added'>
    5222
    1
            <mi>x</mi>
    5223
    1
            <mo>+</mo>
    5224
    1
            <mi>y</mi>
    5225
    1
          </mrow>
    5226
    1
          <mo>)</mo>
    5227
    1
          </mrow>
    5228
    1
        </mrow>
    5229
    1
        <mo>+</mo>
    5230
    1
        <mrow data-changed='added'>
    5231
    1
          <mi>w</mi>
    5232
    1
          <mo data-changed='added'>&#x2061;</mo>
    5233
    1
          <mrow data-changed='added'>
    5234
    1
          <mo>(</mo>
    5235
    1
          <mrow data-changed='added'>
    5236
    1
            <mi>x</mi>
    5237
    1
            <mo>,</mo>
    5238
    1
            <mi>y</mi>
    5239
    1
          </mrow>
    5240
    1
          <mo>)</mo>
    5241
    1
          </mrow>
    5242
    1
        </mrow>
    5243
    1
        </mrow>
    5244
    1
          </math>";
    5245
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5246
    1
        }
    5247
    5248
        #[test]
    5249
    1
        fn function_with_no_args() -> Result<()> {
    5250
    1
            let test_str = "<math><mrow>
    5251
    1
        <mi>sin</mi><mi>x</mi>
    5252
    1
          <mo>+</mo>
    5253
    1
         <mi>f</mi><mi>x</mi>
    5254
    1
          <mo>+</mo>
    5255
    1
         <mi>t</mi><mi>x</mi>
    5256
    1
        </mrow></math>";
    5257
    1
            let target_str = " <math>
    5258
    1
        <mrow>
    5259
    1
          <mrow data-changed='added'>
    5260
    1
          <mi>sin</mi>
    5261
    1
          <mo data-changed='added'>&#x2061;</mo>
    5262
    1
          <mi>x</mi>
    5263
    1
          </mrow>
    5264
    1
          <mo>+</mo>
    5265
    1
          <mrow data-changed='added'>
    5266
    1
          <mi>f</mi>
    5267
    1
          <mo data-changed='added'>&#x2062;</mo>
    5268
    1
          <mi>x</mi>
    5269
    1
          </mrow>
    5270
    1
          <mo>+</mo>
    5271
    1
          <mrow data-changed='added'>
    5272
    1
          <mi>t</mi>
    5273
    1
          <mo data-changed='added'>&#x2062;</mo>
    5274
    1
          <mi>x</mi>
    5275
    1
          </mrow>
    5276
    1
        </mrow>
    5277
    1
         </math>";
    5278
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5279
    5280
    1
      }
    5281
    5282
    5283
        #[test]
    5284
    1
        fn function_call_vs_implied_times() -> Result<()> {
    5285
    1
            let test_str = "<math><mi>f</mi><mo>(</mo><mi>x</mi><mo>)</mo><mi>y</mi></math>";
    5286
    1
            let target_str = "<math>
    5287
    1
          <mrow data-changed='added'>
    5288
    1
            <mrow data-changed='added'>
    5289
    1
              <mi>f</mi>
    5290
    1
              <mo data-changed='added'>&#x2061;</mo>
    5291
    1
              <mrow data-changed='added'> <mo>(</mo> <mi>x</mi> <mo>)</mo> </mrow>
    5292
    1
            </mrow>
    5293
    1
          <mo data-changed='added'>&#x2062;</mo>
    5294
    1
          <mi>y</mi>    </mrow>
    5295
    1
         </math>";
    5296
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5297
    1
        }
    5298
    5299
        #[test]
    5300
    1
        fn implied_plus() -> Result<()> {
    5301
    1
            let test_str = "<math><mrow>
    5302
    1
        <mn>2</mn><mfrac><mn>3</mn><mn>4</mn></mfrac>
    5303
    1
        </mrow></math>";
    5304
    1
            let target_str = "<math>
    5305
    1
          <mrow>
    5306
    1
            <mn>2</mn>
    5307
    1
            <mo data-changed='added'>&#x2064;</mo>
    5308
    1
            <mfrac>
    5309
    1
              <mn>3</mn>
    5310
    1
              <mn>4</mn>
    5311
    1
            </mfrac>
    5312
    1
          </mrow>
    5313
    1
        </math>";
    5314
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5315
    1
        }
    5316
    5317
        #[test]
    5318
    1
        fn implied_plus_linear() -> Result<()> {
    5319
    1
            let test_str = "<math><mrow>
    5320
    1
          <mn>2</mn><mspace width='0.278em'></mspace><mn>3</mn><mo>/</mo><mn>4</mn>
    5321
    1
          </mrow></math>";
    5322
    1
            let target_str = "<math>
    5323
    1
          <mrow>
    5324
    1
            <mn>2</mn>
    5325
    1
            <mo data-changed='added'>&#x2064;</mo>
    5326
    1
            <mrow data-changed='added'>>
    5327
    1
              <mn data-previous-space-width='0.278'>3</mn>
    5328
    1
              <mo>/</mo>
    5329
    1
              <mn>4</mn>
    5330
    1
            </mrow>
    5331
    1
          </mrow>
    5332
    1
        </math>";
    5333
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5334
    1
        }
    5335
    5336
        #[test]
    5337
    1
        fn implied_plus_linear2() -> Result<()> {
    5338
    1
            let test_str = "<math><mrow>
    5339
    1
          <mn>2</mn><mrow><mn>3</mn><mo>/</mo><mn>4</mn></mrow>
    5340
    1
          </mrow></math>";
    5341
    1
            let target_str = "<math>
    5342
    1
          <mrow>
    5343
    1
            <mn>2</mn>
    5344
    1
            <mo data-changed='added'>&#x2064;</mo>
    5345
    1
            <mrow>
    5346
    1
              <mn>3</mn>
    5347
    1
              <mo>/</mo>
    5348
    1
              <mn>4</mn>
    5349
    1
            </mrow>
    5350
    1
          </mrow>
    5351
    1
        </math>";
    5352
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5353
    1
        }
    5354
    5355
        #[test]
    5356
    1
        fn implied_comma() -> Result<()> {
    5357
    1
            let test_str = "<math><msub><mi>b</mi><mrow><mn>1</mn><mn>2</mn></mrow></msub></math>";
    5358
    1
            let target_str = "<math>
    5359
    1
           <msub><mi>b</mi><mrow><mn>1</mn><mo data-changed='added'>&#x2063;</mo><mn>2</mn></mrow></msub>
    5360
    1
        </math>";
    5361
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5362
    1
        }
    5363
    5364
        #[test]
    5365
    1
        fn no_implied_comma() -> Result<()> {
    5366
    1
            let test_str = "<math><mfrac><mi>b</mi><mrow><mn>1</mn><mn>2</mn></mrow></mfrac></math>";
    5367
    1
            let target_str = "<math>
    5368
    1
           <mfrac><mi>b</mi><mrow><mn>1</mn><mo data-changed='added'>&#x2062;</mo><mn>2</mn></mrow></mfrac>
    5369
    1
        </math>";
    5370
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5371
    1
        }
    5372
    5373
        #[test]
    5374
    1
        fn vertical_bars() -> Result<()> {
    5375
    1
            let test_str = "<math>
    5376
    1
        <mo>|</mo> <mi>x</mi> <mo>|</mo><mo>+</mo><mo>|</mo>
    5377
    1
         <mi>a</mi><mo>+</mo><mn>1</mn> <mo>|</mo>
    5378
    1
        </math>";
    5379
    1
        let target_str = " <math>
    5380
    1
        <mrow data-changed='added'>
    5381
    1
        <mrow data-changed='added'>
    5382
    1
          <mo>|</mo>
    5383
    1
          <mi>x</mi>
    5384
    1
          <mo>|</mo>
    5385
    1
        </mrow>
    5386
    1
        <mo>+</mo>
    5387
    1
        <mrow data-changed='added'>
    5388
    1
          <mo>|</mo>
    5389
    1
          <mrow data-changed='added'>
    5390
    1
          <mi>a</mi>
    5391
    1
          <mo>+</mo>
    5392
    1
          <mn>1</mn>
    5393
    1
          </mrow>
    5394
    1
          <mo>|</mo>
    5395
    1
        </mrow>
    5396
    1
        </mrow>
    5397
    1
       </math>";
    5398
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5399
    1
        }
    5400
    5401
    5402
        #[test]
    5403
    1
        fn vertical_bars_nested() -> Result<()> {
    5404
    1
            let test_str = "<math><mo>|</mo><mi>x</mi><mo>|</mo><mi>y</mi><mo>|</mo><mi>z</mi><mo>|</mo></math>";
    5405
    1
        let target_str = "<math>
    5406
    1
        <mrow data-changed='added'>
    5407
    1
        <mrow data-changed='added'>
    5408
    1
          <mo>|</mo>
    5409
    1
          <mi>x</mi>
    5410
    1
          <mo>|</mo>
    5411
    1
        </mrow>
    5412
    1
        <mo data-changed='added'>&#x2062;</mo>
    5413
    1
        <mi>y</mi>
    5414
    1
        <mo data-changed='added'>&#x2062;</mo>
    5415
    1
        <mrow data-changed='added'>
    5416
    1
          <mo>|</mo>
    5417
    1
          <mi>z</mi>
    5418
    1
          <mo>|</mo>
    5419
    1
        </mrow>
    5420
    1
        </mrow>
    5421
    1
       </math>";
    5422
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5423
    1
        }
    5424
    5425
        #[test]
    5426
    1
        fn double_vertical_bars() -> Result<()> {
    5427
    1
          let test_str = "<math><mrow><mo>||</mo><mi>x</mi><mo>||</mo><mo>||</mo><mi>y</mi><mo>||</mo></mrow></math>";
    5428
    1
        let target_str = "<math>
    5429
    1
          <mrow>
    5430
    1
            <mrow data-changed='added'><mo>‖</mo><mi>x</mi><mo>‖</mo></mrow>
    5431
    1
            <mo data-changed='added'>&#x2062;</mo>
    5432
    1
            <mrow data-changed='added'><mo>‖</mo><mi>y</mi><mo>‖</mo></mrow>
    5433
    1
          </mrow>
    5434
    1
        </math>";
    5435
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5436
    1
        }
    5437
    5438
        #[test]
    5439
    1
        fn double_vertical_bars_mo() -> Result<()> {
    5440
    1
          let test_str = "<math><mo>|</mo><mo>|</mo><mi>a</mi><mo>|</mo><mo>|</mo></math>";
    5441
    1
        let target_str = "<math><mrow data-changed='added'><mo>‖</mo><mi>a</mi><mo>‖</mo></mrow></math>";
    5442
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5443
    1
        }
    5444
    5445
        #[test]
    5446
    1
        fn no_double_vertical_bars_mo() -> Result<()> {
    5447
    1
          let test_str = "<math><mo>|</mo><mi>x</mi><mo>|</mo><mo>|</mo><mi>y</mi><mo>|</mo></math>";
    5448
    1
            let target_str = "<math>  <mrow data-changed='added'>
    5449
    1
            <mrow data-changed='added'><mo>|</mo><mi>x</mi><mo>|</mo></mrow>
    5450
    1
            <mo data-changed='added'>&#x2062;</mo>
    5451
    1
            <mrow data-changed='added'><mo>|</mo><mi>y</mi><mo>|</mo></mrow>
    5452
    1
          </mrow> </math>";
    5453
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5454
    1
        }
    5455
    5456
        #[test]
    5457
    1
        fn vertical_bar_such_that() -> Result<()> {
    5458
    1
            let test_str = "<math>
    5459
    1
            <mo>{</mo><mi>x</mi><mo>|</mo><mi>x</mi><mo>&#x2208;</mo><mi>S</mi><mo>}</mo>
    5460
    1
                </math>";
    5461
    1
            let target_str = "<math>
    5462
    1
        <mrow data-changed='added'>
    5463
    1
          <mo>{</mo>
    5464
    1
          <mrow data-changed='added'>
    5465
    1
          <mi>x</mi>
    5466
    1
          <mo>|</mo>
    5467
    1
          <mrow data-changed='added'>
    5468
    1
            <mi>x</mi>
    5469
    1
            <mo>∈</mo>
    5470
    1
            <mi>S</mi>
    5471
    1
          </mrow>
    5472
    1
          </mrow>
    5473
    1
          <mo>}</mo>
    5474
    1
        </mrow>
    5475
    1
         </math>";
    5476
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5477
    1
        }
    5478
    5479
        #[test]
    5480
      #[ignore]  // need to figure out a test for this ("|" should have a precedence around ":" since that is an alternative notation for "such that", but "∣" is higher precedence)
    5481
    0
        fn vertical_bar_divides() -> Result<()> {
    5482
    0
            let test_str = "<math>
    5483
    0
        <mi>x</mi><mo>+</mo><mi>y</mi> <mo>|</mo><mn>12</mn>
    5484
    0
                </math>";
    5485
    0
            let target_str = "<math>
    5486
    0
            <mrow data-changed='added'>
    5487
    0
            <mrow data-changed='added'>
    5488
    0
              <mi>x</mi>
    5489
    0
              <mo>+</mo>
    5490
    0
              <mi>y</mi>
    5491
    0
            </mrow>
    5492
    0
            <mo>∣ <!--divides--></mo>
    5493
    0
            <mn>12</mn>
    5494
    0
            </mrow>
    5495
    0
          </math>";
    5496
    0
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5497
    0
        }
    5498
    5499
    5500
        #[test]
    5501
    1
        fn trig_mo() -> Result<()> {
    5502
    1
            let test_str = "<math><mo>sin</mo><mi>x</mi>
    5503
    1
            <mo>+</mo><mo>cos</mo><mi>y</mi>
    5504
    1
            <mo>+</mo><munder><mo>lim</mo><mi>D</mi></munder><mi>y</mi>
    5505
    1
          </math>";
    5506
    1
            let target_str = "<math>
    5507
    1
        <mrow data-changed='added'>
    5508
    1
          <mrow data-changed='added'>
    5509
    1
          <mi>sin</mi>
    5510
    1
          <mo data-changed='added'>&#x2061;</mo>
    5511
    1
          <mi>x</mi>
    5512
    1
          </mrow>
    5513
    1
          <mo>+</mo>
    5514
    1
          <mrow data-changed='added'>
    5515
    1
          <mi>cos</mi>
    5516
    1
          <mo data-changed='added'>&#x2061;</mo>
    5517
    1
          <mi>y</mi>
    5518
    1
          </mrow>
    5519
    1
          <mo>+</mo>
    5520
    1
          <mrow data-changed='added'>
    5521
    1
          <munder>
    5522
    1
            <mi>lim</mi>
    5523
    1
            <mi>D</mi>
    5524
    1
          </munder>
    5525
    1
          <mo data-changed='added'>&#x2061;</mo>
    5526
    1
          <mi>y</mi>
    5527
    1
          </mrow>
    5528
    1
        </mrow>
    5529
    1
         </math>";
    5530
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5531
    1
        }
    5532
    5533
        #[test]
    5534
    1
        fn trig_mtext() -> Result<()> {
    5535
    1
            let test_str = "<math><mtext>sin</mtext><mi>x</mi>
    5536
    1
            <mo>+</mo><mtext>cos</mtext><mi>y</mi>
    5537
    1
            <mo>+</mo><munder><mtext>lim</mtext><mi>D</mi></munder><mi>y</mi>
    5538
    1
          </math>";
    5539
    1
            let target_str = "<math>
    5540
    1
        <mrow data-changed='added'>
    5541
    1
          <mrow data-changed='added'>
    5542
    1
          <mi>sin</mi>
    5543
    1
          <mo data-changed='added'>&#x2061;</mo>
    5544
    1
          <mi>x</mi>
    5545
    1
          </mrow>
    5546
    1
          <mo>+</mo>
    5547
    1
          <mrow data-changed='added'>
    5548
    1
          <mi>cos</mi>
    5549
    1
          <mo data-changed='added'>&#x2061;</mo>
    5550
    1
          <mi>y</mi>
    5551
    1
          </mrow>
    5552
    1
          <mo>+</mo>
    5553
    1
          <mrow data-changed='added'>
    5554
    1
          <munder>
    5555
    1
            <mi>lim</mi>
    5556
    1
            <mi>D</mi>
    5557
    1
          </munder>
    5558
    1
          <mo data-changed='added'>&#x2061;</mo>
    5559
    1
          <mi>y</mi>
    5560
    1
          </mrow>
    5561
    1
        </mrow>
    5562
    1
         </math>";
    5563
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5564
    1
        }
    5565
    5566
        #[test]
    5567
    1
        fn trig_negative_args() -> Result<()> {
    5568
    1
            let test_str = "<math><mi>sin</mi><mo>-</mo><mn>2</mn><mi>π</mi><mi>x</mi></math>";
    5569
    1
            let target_str = "<math>
    5570
    1
        <mrow data-changed='added'>
    5571
    1
          <mi>sin</mi>
    5572
    1
          <mo data-changed='added'>&#x2061;</mo>
    5573
    1
          <mrow data-changed='added'>
    5574
    1
          <mrow data-changed='added'>
    5575
    1
            <mo>-</mo>
    5576
    1
            <mn>2</mn>
    5577
    1
          </mrow>
    5578
    1
          <mo data-changed='added'>&#x2062;</mo>
    5579
    1
          <mi>π</mi>
    5580
    1
          <mo data-changed='added'>&#x2062;</mo>
    5581
    1
          <mi>x</mi>
    5582
    1
          </mrow>
    5583
    1
        </mrow>
    5584
    1
         </math>";
    5585
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5586
    1
        }
    5587
    5588
        #[test]
    5589
    1
        fn not_trig_negative_args() -> Result<()> {
    5590
        // this is here to make sure that only trig functions get the special treatment
    5591
    1
            let test_str = "<math><mi>ker</mi><mo>-</mo><mn>2</mn><mi>π</mi><mi>x</mi></math>";
    5592
    1
            let target_str = "<math>
    5593
    1
          <mrow data-changed='added'>
    5594
    1
              <mrow data-changed='added'>
    5595
    1
              <mi>ker</mi>
    5596
    1
              <mo data-changed='added'>&#x2061;</mo>
    5597
    1
              <mrow data-changed='added'>
    5598
    1
                <mo>-</mo>
    5599
    1
                <mn>2</mn>
    5600
    1
              </mrow>
    5601
    1
              </mrow>
    5602
    1
            <mo data-changed='added'>&#x2062;</mo>
    5603
    1
            <mi>π</mi>
    5604
    1
            <mo data-changed='added'>&#x2062;</mo>
    5605
    1
            <mi>x</mi>
    5606
    1
          </mrow>
    5607
    1
        </math>";
    5608
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5609
    1
        }
    5610
    5611
        #[test]
    5612
    1
        fn trig_args() -> Result<()> {
    5613
    1
            let test_str = "<math><mi>sin</mi><mn>2</mn><mi>π</mi><mi>x</mi></math>";
    5614
    1
            let target_str = "<math>
    5615
    1
        <mrow data-changed='added'>
    5616
    1
          <mi>sin</mi>
    5617
    1
          <mo data-changed='added'>&#x2061;</mo>
    5618
    1
          <mrow data-changed='added'>
    5619
    1
          <mn>2</mn>
    5620
    1
          <mo data-changed='added'>&#x2062;</mo>
    5621
    1
          <mi>π</mi>
    5622
    1
          <mo data-changed='added'>&#x2062;</mo>
    5623
    1
          <mi>x</mi>
    5624
    1
          </mrow>
    5625
    1
        </mrow>
    5626
    1
         </math>";
    5627
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5628
    1
        }
    5629
    5630
        #[test]
    5631
    1
        fn not_trig_args() -> Result<()> {
    5632
        // this is here to make sure that only trig functions get the special treatment
    5633
    1
            let test_str = "<math><mi>ker</mi><mn>2</mn><mi>π</mi><mi>x</mi></math>";
    5634
    1
            let target_str = "<math>
    5635
    1
        <mrow data-changed='added'>
    5636
    1
          <mrow data-changed='added'>
    5637
    1
            <mi>ker</mi>
    5638
    1
            <mo data-changed='added'>&#x2061;</mo>
    5639
    1
            <mn>2</mn>
    5640
    1
          </mrow>
    5641
    1
          <mo data-changed='added'>&#x2062;</mo>
    5642
    1
          <mi>π</mi>
    5643
    1
          <mo data-changed='added'>&#x2062;</mo>
    5644
    1
          <mi>x</mi>
    5645
    1
        </mrow>
    5646
    1
         </math>";
    5647
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5648
    1
        }
    5649
    5650
        #[test]
    5651
    1
        fn trig_trig() -> Result<()> {
    5652
    1
            let test_str = "<math><mi>sin</mi><mi>x</mi><mi>cos</mi><mi>y</mi></math>";
    5653
    1
            let target_str = "<math>
    5654
    1
        <mrow data-changed='added'>
    5655
    1
          <mrow data-changed='added'>
    5656
    1
            <mi>sin</mi>
    5657
    1
            <mo data-changed='added'>&#x2061;</mo>
    5658
    1
            <mi>x</mi>
    5659
    1
          </mrow>
    5660
    1
          <mo data-changed='added'>&#x2062;</mo>
    5661
    1
          <mrow data-changed='added'>
    5662
    1
            <mi>cos</mi>
    5663
    1
            <mo data-changed='added'>&#x2061;</mo>
    5664
    1
            <mi>y</mi>
    5665
    1
          </mrow>
    5666
    1
        </mrow>
    5667
    1
        </math>";
    5668
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5669
    1
        }
    5670
    5671
        #[test]
    5672
    1
        fn trig_function_composition() -> Result<()> {
    5673
    1
            let test_str = "<math><mo>(</mo><mi>sin</mi><mo>-</mo><mi>cos</mi><mo>)</mo><mi>x</mi></math>";
    5674
    1
            let target_str = "<math>
    5675
    1
        <mrow data-changed='added'>
    5676
    1
          <mrow data-changed='added'>
    5677
    1
          <mo>(</mo>
    5678
    1
          <mrow data-changed='added'>
    5679
    1
            <mi>sin</mi>
    5680
    1
            <mo>-</mo>
    5681
    1
            <mi>cos</mi>
    5682
    1
          </mrow>
    5683
    1
          <mo>)</mo>
    5684
    1
          </mrow>
    5685
    1
          <mo data-changed='added'>&#x2062;</mo>
    5686
    1
          <mi>x</mi>
    5687
    1
        </mrow>
    5688
    1
         </math>";
    5689
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5690
    1
        }
    5691
    5692
    5693
      #[test]
    5694
    1
        fn currency_in_leaf_prefix() -> Result<()> {
    5695
    1
            let test_str = "<math><mn>$8.54</mn></math>";
    5696
    1
            let target_str = "<math>
    5697
    1
          <mrow data-changed='added'>
    5698
    1
          <mi>$</mi>
    5699
    1
          <mo data-changed='added'>&#x2062;</mo>
    5700
    1
          <mn>8.54</mn>
    5701
    1
          </mrow>
    5702
    1
        </math>";
    5703
    1
        are_strs_canonically_equal_result(test_str, target_str, &[])
    5704
    1
      }
    5705
    5706
      #[test]
    5707
    1
        fn currency_in_leaf_postfix() -> Result<()> {
    5708
    1
            let test_str = "<math><mn>188,23€</mn></math>";
    5709
    1
            let target_str = " <math>
    5710
    1
          <mrow data-changed='added'>
    5711
    1
            <mo data-changed='added'>&#x2062;</mo>
    5712
    1
            <mn>188,23</mn>
    5713
    1
            <mo data-changed='added'>&#x2062;</mo>
    5714
    1
            <mi>€</mi>
    5715
    1
          </mrow>
    5716
    1
        </math>";
    5717
    1
       are_strs_canonically_equal_with_locale(test_str, target_str, &[], ".", ",")
    5718
    1
    }
    5719
    5720
      #[test]
    5721
    1
        fn currency_in_leaf_infix() -> Result<()> {
    5722
    1
            let test_str = "<math><mn>1€23</mn></math>";
    5723
    1
            let target_str = " <math>
    5724
    1
          <mrow data-changed='added'>
    5725
    1
            <mn>1</mn>
    5726
    1
            <mo data-changed='added'>&#x2062;</mo>
    5727
    1
            <mi>€</mi>
    5728
    1
            <mo data-changed='added'>&#x2062;</mo>
    5729
    1
            <mn>23</mn>
    5730
    1
          </mrow>
    5731
    1
        </math>";
    5732
    1
       are_strs_canonically_equal_with_locale(test_str, target_str, &[], ".", ",")
    5733
    1
    }
    5734
    5735
      #[test]
    5736
    1
        fn mtext_whitespace_string() -> Result<()> {
    5737
    1
            let test_str = "<math><mi>t</mi><mtext>&#x00A0;&#x205F;</mtext></math>";
    5738
    1
            let target_str = "<math><mi data-following-space-width='0.922'>t</mi></math>";
    5739
    1
        are_strs_canonically_equal_result(test_str, target_str, &[])
    5740
    1
      }
    5741
    5742
      #[test]
    5743
    1
        fn mtext_whitespace_string_before() -> Result<()> {
    5744
    1
            let test_str = "<math><mtext>&#x00A0;&#x205F;</mtext><mi>t</mi></math>";
    5745
    1
            let target_str = "<math><mi data-previous-space-width='0.922'>t</mi></math>";
    5746
    1
        are_strs_canonically_equal_result(test_str, target_str, &[])
    5747
    1
      }
    5748
    5749
      #[test]
    5750
    1
        fn mtext_whitespace_1() -> Result<()> {
    5751
    1
            let test_str = "<math><mi>t</mi><mtext>&#x00A0;&#x205F;</mtext>
    5752
    1
            <mrow><mo>(</mo><mi>x</mi><mo>+</mo><mi>y</mi><mo>)</mo></mrow></math>";
    5753
    1
            let target_str = " <math>
    5754
    1
        <mrow data-changed='added'>
    5755
    1
          <mi>t</mi>
    5756
    1
          <mo data-changed='added' data-function-guess='true'>&#x2062;</mo>
    5757
    1
          <mrow data-previous-space-width='0.922'>
    5758
    1
          <mo>(</mo>
    5759
    1
          <mrow data-changed='added'>
    5760
    1
            <mi>x</mi>
    5761
    1
            <mo>+</mo>
    5762
    1
            <mi>y</mi>
    5763
    1
          </mrow>
    5764
    1
          <mo>)</mo>
    5765
    1
          </mrow>
    5766
    1
        </mrow>
    5767
    1
         </math>";
    5768
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5769
    1
      }
    5770
    5771
      #[test]
    5772
    1
        fn mtext_whitespace_2() -> Result<()> {
    5773
    1
            let test_str = "<math><mi>f</mi><mtext>&#x00A0;&#x205F;</mtext>
    5774
    1
            <mrow><mo>(</mo><mi>x</mi><mo>+</mo><mi>y</mi><mo>)</mo></mrow></math>";
    5775
    1
            let target_str = " <math>
    5776
    1
        <mrow data-changed='added'>
    5777
    1
          <mi>f</mi>
    5778
    1
          <mo data-changed='added'>&#x2061;</mo>
    5779
    1
          <mrow  data-previous-space-width='0.922'>
    5780
    1
          <mo>(</mo>
    5781
    1
          <mrow data-changed='added'>
    5782
    1
            <mi>x</mi>
    5783
    1
            <mo>+</mo>
    5784
    1
            <mi>y</mi>
    5785
    1
          </mrow>
    5786
    1
          <mo>)</mo>
    5787
    1
          </mrow>
    5788
    1
        </mrow>
    5789
    1
         </math>";
    5790
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5791
    1
      }
    5792
    5793
      #[test]
    5794
    1
        fn remove_mtext_whitespace_3() -> Result<()> {
    5795
    1
            let test_str = "<math><mi>t</mi>
    5796
    1
            <mrow><mtext>&#x2009;</mtext><mo>(</mo><mi>x</mi><mo>+</mo><mi>y</mi><mo>)</mo></mrow></math>";
    5797
    1
            let target_str = "<math>
    5798
    1
        <mrow data-changed='added'>
    5799
    1
          <mi>t</mi>
    5800
    1
          <mo data-changed='added' data-function-guess='true'>&#x2062;</mo>
    5801
    1
          <mrow>
    5802
    1
          <mo data-previous-space-width='0.167'>(</mo>
    5803
    1
          <mrow data-changed='added'>
    5804
    1
            <mi>x</mi>
    5805
    1
            <mo>+</mo>
    5806
    1
            <mi>y</mi>
    5807
    1
          </mrow>
    5808
    1
          <mo>)</mo>
    5809
    1
          </mrow>
    5810
    1
        </mrow>
    5811
    1
         </math>";
    5812
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5813
    1
      }
    5814
    5815
      #[test]
    5816
    1
        fn do_not_remove_any_whitespace() -> Result<()> {
    5817
    1
            let test_str = "<math><mfrac>
    5818
    1
              <mrow><mspace width='3em'/></mrow>
    5819
    1
              <mtext>&#x2009;</mtext>
    5820
    1
            </mfrac></math>";
    5821
    1
            let target_str = " <math>
    5822
    1
          <mfrac>
    5823
    1
            <mtext width='3em' data-changed='was-mspace' data-width='3' data-empty-in-2D='true'> </mtext>
    5824
    1
            <mtext data-width='0.167' data-empty-in-2D='true'> </mtext>
    5825
    1
          </mfrac>
    5826
    1
         </math>";
    5827
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5828
    1
      }
    5829
    5830
      #[test]
    5831
    1
        fn remove_mo_whitespace() -> Result<()> {
    5832
    1
            let test_str = "<math><mi>cos</mi><mo>&#xA0;</mo><mi>x</mi></math>";
    5833
    1
            let target_str = "<math>
    5834
    1
            <mrow data-changed='added'>
    5835
    1
              <mi>cos</mi>
    5836
    1
              <mo data-changed='added'>&#x2061;</mo>
    5837
    1
              <mi data-previous-space-width='0.7'>x</mi>
    5838
    1
            </mrow>
    5839
    1
            </math>";
    5840
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5841
    1
      }
    5842
    5843
      #[test]
    5844
    1
        fn do_not_remove_some_whitespace() -> Result<()> {
    5845
    1
            let test_str = "<math><mroot>
    5846
    1
              <mrow><mi>b</mi><mphantom><mi>y</mi></mphantom></mrow>
    5847
    1
              <mtext>&#x2009;</mtext>
    5848
    1
            </mroot></math>";
    5849
    1
            let target_str = "<math><mroot>
    5850
    1
            <mi>b</mi>
    5851
    1
            <mtext data-empty-in-2D='true' data-width='0.167'>&#xA0;</mtext>
    5852
    1
          </mroot></math>";
    5853
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5854
    1
      }
    5855
    5856
      #[test]
    5857
    1
        fn remove_all_extra_elements() -> Result<()> {
    5858
    1
            let test_str = "<math><msqrt>
    5859
    1
              <mstyle> <mi>b</mi> </mstyle>
    5860
    1
              <mphantom><mi>y</mi></mphantom>
    5861
    1
              <mtext>&#x2009;</mtext>
    5862
    1
              <mspace width='3em'/>
    5863
    1
            </msqrt></math>";
    5864
    1
            let target_str = "<math><msqrt>
    5865
    1
            <mi data-following-space-width='3.167'>b</mi>
    5866
    1
          </msqrt></math>";
    5867
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5868
    1
      }
    5869
    5870
      #[test]
    5871
    1
        fn empty_content() -> Result<()> {
    5872
    1
            let test_str = "<math></math>";
    5873
    1
            let target_str = " <math><mtext data-added='missing-content' data-width='0.700'> </mtext></math>";
    5874
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5875
    1
      }
    5876
    5877
      #[test]
    5878
    1
        fn empty_content_after_cleanup() -> Result<()> {
    5879
    1
            let test_str = "<math><mrow><mphantom><mn>1</mn></mphantom></mrow></math>";
    5880
    1
            let target_str = " <math><mtext data-added='missing-content' data-width='0'> </mtext></math>";
    5881
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5882
    1
      }
    5883
    5884
      #[test]
    5885
    1
        fn empty_content_fix_num_children() -> Result<()> {
    5886
    1
            let test_str = "  <math><mfrac><menclose notation='box'><mrow/></menclose><mrow/></mfrac></math>";
    5887
    1
            let target_str = "<math>
    5888
    1
        <mfrac>
    5889
    1
          <menclose notation='box'>
    5890
    1
          <mtext data-added='missing-content' data-empty-in-2D='true' data-width='0'> </mtext>
    5891
    1
          </menclose>
    5892
    1
          <mtext data-changed='empty_content' data-empty-in-2D='true' data-width='0'> </mtext>
    5893
    1
        </mfrac>
    5894
    1
         </math>";
    5895
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5896
    1
      }
    5897
    5898
    5899
      #[test]
    5900
    1
        fn clean_semantics() -> Result<()> {
    5901
        // this comes from LateXML
    5902
    1
            let test_str = "<math>
    5903
    1
            <semantics>
    5904
    1
              <mrow><mi>z</mi></mrow>
    5905
    1
              <annotation-xml encoding='MathML-Content'>
    5906
    1
                <ci>𝑧</ci>
    5907
    1
              </annotation-xml>
    5908
    1
              <annotation encoding='application/x-tex'>z</annotation>
    5909
    1
              <annotation encoding='application/x-llamapun'>italic_z</annotation>
    5910
    1
            </semantics>
    5911
    1
          </math>";
    5912
        // the annotation-xml value is very touchy and must exactly match what mml-to-string() generates for the test to pass
    5913
    1
        let target_str = " <math>
    5914
    1
        <mi data-annotation-xml-MathML-Content=' &lt;annotation-xml encoding=&apos;MathML-Content&apos;&gt;
    5915
    1
      &lt;ci&gt;𝑧&lt;/ci&gt;
    5916
    1
     &lt;/annotation-xml&gt;
    5917
    1
    ' data-annotation-application_slash_x-tex='z' data-annotation-application_slash_x-llamapun='italic_z'>z</mi>
    5918
    1
         </math>";
    5919
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5920
    1
      }
    5921
    5922
      #[test]
    5923
    1
        fn clean_up_mi_operator() -> Result<()> {
    5924
    1
            let test_str = "<math><mrow><mi>∠</mi><mi>A</mi><mi>B</mi><mi>C</mi></mrow></math>";
    5925
    1
            let target_str = " <math>
    5926
    1
            <mrow>
    5927
    1
            <mo>∠</mo>
    5928
    1
            <mrow data-changed='added'>
    5929
    1
              <mi>A</mi>
    5930
    1
              <mo data-changed='added'>&#x2063;</mo>
    5931
    1
              <mi>B</mi>
    5932
    1
              <mo data-changed='added'>&#x2063;</mo>
    5933
    1
              <mi>C</mi>
    5934
    1
            </mrow>
    5935
    1
            </mrow>
    5936
    1
          </math>";
    5937
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5938
    1
      }
    5939
    5940
    5941
      #[test]
    5942
    1
        fn clean_up_arc() -> Result<()> {
    5943
    1
            let test_str = "<math><mtext>arc&#xA0;</mtext><mi>cos</mi><mi>x</mi></math>";
    5944
    1
            let target_str = "<math>
    5945
    1
          <mrow data-changed='added'>
    5946
    1
          <mi>arccos</mi>
    5947
    1
          <mo data-changed='added'>&#x2061;</mo>
    5948
    1
          <mi>x</mi>
    5949
    1
          </mrow>
    5950
    1
        </math>";
    5951
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5952
    1
      }
    5953
    5954
      #[test]
    5955
    1
        fn clean_up_arc_nospace() -> Result<()> {
    5956
    1
            let test_str = "<math><mtext>arc</mtext><mi>cos</mi><mi>x</mi></math>";
    5957
    1
            let target_str = "<math>
    5958
    1
          <mrow data-changed='added'>
    5959
    1
          <mi>arccos</mi>
    5960
    1
          <mo data-changed='added'>&#x2061;</mo>
    5961
    1
          <mi>x</mi>
    5962
    1
          </mrow>
    5963
    1
        </math>";
    5964
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5965
    1
      }
    5966
    5967
      #[test]
    5968
    1
        fn roman_numeral() -> Result<()> {
    5969
    1
            let test_str = "<math><mrow><mtext>XLVIII</mtext> <mo>+</mo><mn>mmxxvi</mn></mrow></math>";
    5970
        // turns out there is no need to mark them as Roman Numerals -- thought that was need for braille
    5971
    1
            let target_str = "<math><mrow>
    5972
    1
          <mn data-roman-numeral='true' data-number='48'>XLVIII</mn> <mo>+</mo><mn data-roman-numeral='true' data-number='2026'>mmxxvi</mn>
    5973
    1
          </mrow></math>";
    5974
            // let target_str = "<math><mrow><mtext>XLVIII</mtext> <mo>+</mo><mn>mmxxvi</mn></mrow></math>";
    5975
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5976
    1
      }
    5977
    5978
      // #[test]
    5979
        // fn roman_numeral_context() {
    5980
        //     let test_str = "<math><mi>vi</mi><mo>-</mo><mi mathvariant='normal'>i</mi><mo>=</mo><mtext>v</mtext></math>";
    5981
        //     let target_str = "<math> <mrow data-changed='added'>
    5982
      //    <mrow data-changed='added'><mn data-roman-numeral='true'>vi</mn><mo>-</mo><mn mathvariant='normal' data-roman-numeral='true'>i</mn></mrow>
    5983
      //    <mo>=</mo> <mn data-roman-numeral='true'>v</mn>
    5984
      //  </mrow> </math>";
    5985
        //     are_strs_canonically_equal_result(test_str, target_str, &[])
    5986
      // }
    5987
    5988
      #[test]
    5989
    1
        fn not_roman_numeral() -> Result<()> {
    5990
    1
            let test_str = "<math><mtext>cm</mtext></math>";
    5991
        // shouldn't change
    5992
    1
            let target_str = "<math><mtext>cm</mtext></math>";
    5993
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    5994
    1
      }
    5995
    5996
      #[test]
    5997
    1
        fn digit_block_binary() -> Result<()> {
    5998
    1
            let test_str = "<math><mo>(</mo><mn>0110</mn><mspace width=\"thickmathspace\"></mspace><mn>1110</mn><mspace width=\"thickmathspace\"></mspace><mn>0110</mn><mo>)</mo></math>";
    5999
    1
            let target_str = " <math>
    6000
    1
            <mrow data-changed='added'>
    6001
    1
            <mo>(</mo>
    6002
    1
            <mn>0110\u{00A0}1110\u{00A0}0110</mn>
    6003
    1
            <mo>)</mo>
    6004
    1
            </mrow>
    6005
    1
          </math>";
    6006
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6007
    1
      }
    6008
    6009
      #[test]
    6010
    1
        fn digit_block_decimal() -> Result<()> {
    6011
    1
            let test_str = "<math><mn>8</mn><mo>,</mo><mn>123</mn><mo>,</mo><mn>456</mn><mo>+</mo>
    6012
    1
                        <mn>4</mn><mo>.</mo><mn>32</mn></math>";
    6013
    1
            let target_str = " <math>
    6014
    1
            <mrow data-changed='added'>
    6015
    1
            <mn>8,123,456</mn>
    6016
    1
            <mo>+</mo>
    6017
    1
            <mn>4.32</mn>
    6018
    1
            </mrow>
    6019
    1
          </math>";
    6020
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6021
    1
      }
    6022
      #[test]
    6023
    1
        fn digit_block_comma() -> Result<()> {
    6024
    1
            let test_str = "<math><mn>8</mn><mo>.</mo><mn>123</mn><mo>.</mo><mn>456</mn><mo>+</mo>
    6025
    1
                        <mn>4</mn><mo>,</mo><mn>32</mn></math>";
    6026
    1
            let target_str = " <math>
    6027
    1
            <mrow data-changed='added'>
    6028
    1
            <mn>8.123.456</mn>
    6029
    1
            <mo>+</mo>
    6030
    1
            <mn>4,32</mn>
    6031
    1
            </mrow>
    6032
    1
          </math>";
    6033
    1
            are_strs_canonically_equal_with_locale(test_str, target_str, &[], ".", ", ")
    6034
    1
      }
    6035
    6036
      #[test]
    6037
    1
      fn digit_block_int() -> Result<()> {
    6038
    1
            let test_str = "<math><mn>12</mn><mo>,</mo><mn>345</mn><mo>+</mo>
    6039
    1
                        <mn>1</mn><mo>,</mo><mn>000</mn></math>";
    6040
    1
            let target_str = " <math>
    6041
    1
            <mrow data-changed='added'>
    6042
    1
            <mn>12,345</mn>
    6043
    1
            <mo>+</mo>
    6044
    1
            <mn>1,000</mn>
    6045
    1
            </mrow>
    6046
    1
          </math>";
    6047
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6048
    1
      }
    6049
    6050
      #[test]
    6051
    1
      fn digit_block_non_ascii_int() -> Result<()> {
    6052
    1
            let test_str = "<math><mn>𝟏𝟐</mn><mo>,</mo><mn>3𝟰𝟻</mn><mo>+</mo>
    6053
    1
                        <mn>𝟙</mn><mo>,</mo><mn>𝟬𝟬𝟬</mn></math>";
    6054
    1
            let target_str = " <math>
    6055
    1
            <mrow data-changed='added'>
    6056
    1
            <mn>𝟏𝟐,3𝟰𝟻</mn>
    6057
    1
            <mo>+</mo>
    6058
    1
            <mn>𝟙,𝟬𝟬𝟬</mn>
    6059
    1
            </mrow>
    6060
    1
          </math>";
    6061
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6062
    1
      }
    6063
    6064
      #[test]
    6065
    1
      fn digit_block_int_dots() -> Result<()> {
    6066
    1
            let test_str = "<math><mn>12</mn><mo>.</mo><mn>345</mn><mo>+</mo>
    6067
    1
                        <mn>1</mn><mo>.</mo><mn>000</mn></math>";
    6068
    1
            let target_str = " <math>
    6069
    1
            <mrow data-changed='added'>
    6070
    1
            <mn>12.345</mn>
    6071
    1
            <mo>+</mo>
    6072
    1
            <mn>1.000</mn>
    6073
    1
            </mrow>
    6074
    1
          </math>";
    6075
    1
            are_strs_canonically_equal_with_locale(test_str, target_str, &[], ".", ", ")
    6076
    1
      }
    6077
    6078
      #[test]
    6079
    1
        fn digit_block_decimal_pt() -> Result<()> {
    6080
    1
            let test_str = "<math><mn>8</mn><mo>,</mo><mn>123</mn><mo>.</mo>
    6081
    1
                    <mo>+</mo><mn>4</mn><mo>.</mo>
    6082
    1
                    <mo>+</mo><mo>.</mo><mn>01</mn></math>";
    6083
    1
            let target_str = " <math>
    6084
    1
            <mrow data-changed='added'>
    6085
    1
            <mn>8,123.</mn>
    6086
    1
            <mo>+</mo>
    6087
    1
            <mn>4.</mn>
    6088
    1
            <mo>+</mo>
    6089
    1
            <mn>.01</mn>
    6090
    1
            </mrow>
    6091
    1
          </math>";
    6092
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6093
    1
      }
    6094
    6095
      #[test]
    6096
    1
        fn number_with_decimal_pt() -> Result<()> {
    6097
        // this is output from WIRIS for "12.3"
    6098
    1
            let test_str = "<math><mn>12</mn><mo>.</mo><mn>3</mn></math>";
    6099
    1
            let target_str = "<math><mn>12.3</mn></math>";
    6100
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6101
    1
      }
    6102
    6103
      #[test]
    6104
    1
        fn number_with_comma_decimal_pt() -> Result<()> {
    6105
        // this is output from WIRIS for "12.3"
    6106
    1
            let test_str = "<math><mn>12</mn><mo>,</mo><mn>3</mn></math>";
    6107
    1
            let target_str = "<math><mn>12,3</mn></math>";
    6108
    1
            are_strs_canonically_equal_with_locale(test_str, target_str, &[], ".", ", ")
    6109
    1
      }
    6110
    6111
      #[test]
    6112
    1
        fn addition_with_decimal_point_at_end() -> Result<()> {
    6113
        // in this case, the trailing "." is probably a decimal point" -- testing special case combine the "."
    6114
        // this comes from WIRIS
    6115
    1
            let test_str = "<math><mn>1</mn><mo>.</mo><mn>3</mn><mo>+</mo><mn>2</mn><mo>.</mo></math>";
    6116
    1
            let target_str = "<math><mrow data-changed='added'><mn>1.3</mn><mo>+</mo><mn>2.</mn></mrow></math>";
    6117
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6118
    1
      }
    6119
    6120
      #[test]
    6121
    1
        fn addition_with_decimal_point_at_end_and_comma_decimal_separator() -> Result<()> {
    6122
        // in this case, the trailing "." is probably a decimal point" -- testing special case combine the "."
    6123
        // this comes from WIRIS
    6124
    1
            let test_str = "<math><mn>1</mn><mo>,</mo><mn>3</mn><mo>+</mo><mn>2</mn><mo>,</mo></math>";
    6125
    1
            let target_str = "<math><mrow data-changed='added'><mn>1,3</mn><mo>+</mo><mn>2,</mn></mrow></math>";
    6126
    1
            are_strs_canonically_equal_with_locale(test_str, target_str, &[], ".", ", ")
    6127
    1
      }
    6128
    6129
      #[test]
    6130
    1
        fn sequence_with_period() -> Result<()> {
    6131
        // in this case, we don't want "5." -- testing special case to avoid combining the period.
    6132
    1
            let test_str = "<math><mn>1</mn><mo>,</mo><mn>3</mn><mo>,</mo><mn>5</mn><mo>.</mo></math>";
    6133
    1
            let target_str = "<math><mrow data-changed='added'>
    6134
    1
            <mrow data-changed='added'><mn>1</mn><mo>,</mo><mn>3</mn><mo>,</mo><mn>5</mn></mrow><mo>.</mo>
    6135
    1
          </mrow></math>";
    6136
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6137
    1
      }
    6138
    6139
      #[test]
    6140
    1
        fn addition_decimal_pt() -> Result<()> {
    6141
    1
            let test_str = "<math><mo>.</mo><mn>4</mn><mo>=</mo><mn>0</mn><mo>.</mo><mn>4</mn></math>";
    6142
    1
            let target_str = "<math><mrow data-changed='added'><mn>.4</mn><mo>=</mo><mn>0.4</mn></mrow></math>";
    6143
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6144
    1
      }
    6145
    6146
      #[test]
    6147
    1
        fn fraction_decimal_pt() -> Result<()> {
    6148
    1
            let test_str = "<math><mfrac><mrow><mn>1</mn><mo>.</mo></mrow><mrow><mn>2</mn><mo>.</mo></mrow></mfrac></math>";
    6149
    1
            let target_str = "<math><mfrac><mn>1.</mn><mn>2.</mn></mfrac></math>";
    6150
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6151
    1
      }
    6152
    6153
      #[test]
    6154
    1
        fn fraction_decimal_pt_no_split() -> Result<()> {
    6155
        // don't split off the '.'
    6156
    1
            let test_str = "<math><mfrac><mn>1.</mn><mn>2.</mn></mfrac></math>";
    6157
    1
            let target_str = "<math><mfrac><mn>1.</mn><mn>2.</mn></mfrac></math>";
    6158
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6159
    1
      }
    6160
    6161
      #[test]
    6162
    1
        fn not_digit_block_parens() -> Result<()> {
    6163
    1
            let test_str = "<math><mo>(</mo><mn>451</mn><mo>,</mo><mn>231</mn><mo>)</mo></math>";
    6164
    1
            let target_str = " <math> <mrow data-changed='added'>
    6165
    1
            <mo>(</mo>
    6166
    1
            <mrow data-changed='added'>
    6167
    1
            <mn>451</mn> <mo>,</mo> <mn>231</mn>
    6168
    1
            </mrow>
    6169
    1
            <mo>)</mo>
    6170
    1
          </mrow></math>";
    6171
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6172
    1
      }
    6173
    6174
      #[test]
    6175
    1
        fn not_digit_block_parens_mrow() -> Result<()> {
    6176
    1
            let test_str = "<math><mo>(</mo><mrow><mn>451</mn><mo>,</mo><mn>231</mn></mrow><mo>)</mo></math>";
    6177
    1
            let target_str = " <math> <mrow data-changed='added'>
    6178
    1
            <mo>(</mo>
    6179
    1
            <mrow>
    6180
    1
            <mn>451</mn> <mo>,</mo> <mn>231</mn>
    6181
    1
            </mrow>
    6182
    1
            <mo>)</mo>
    6183
    1
          </mrow></math>";
    6184
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6185
    1
      }
    6186
    6187
      #[test]
    6188
    1
        fn not_digit_block_decimal() -> Result<()> {
    6189
    1
        let test_str = "<math><mn>8</mn><mo>,</mo><mn>49</mn><mo>,</mo><mn>456</mn><mo>+</mo>
    6190
    1
                        <mn>4</mn><mtext> </mtext><mn>32</mn><mo>+</mo>
    6191
    1
                      <mn>1</mn><mo>,</mo><mn>234</mn><mo>,</mo><mn>56</mn></math>";
    6192
    1
            let target_str = "<math>
    6193
    1
            <mrow data-changed='added'>
    6194
    1
            <mn>8</mn>
    6195
    1
            <mo>,</mo>
    6196
    1
            <mn>49</mn>
    6197
    1
            <mo>,</mo>
    6198
    1
            <mrow data-changed='added'>
    6199
    1
              <mn>456</mn>
    6200
    1
              <mo>+</mo>
    6201
    1
              <mrow data-changed='added'>
    6202
    1
              <mn>4</mn>
    6203
    1
              <mo data-changed='added'>&#x2062;</mo>
    6204
    1
              <mn>32</mn>
    6205
    1
              </mrow>
    6206
    1
              <mo>+</mo>
    6207
    1
              <mn>1</mn>
    6208
    1
            </mrow>
    6209
    1
            <mo>,</mo>
    6210
    1
            <mn>234</mn>
    6211
    1
            <mo>,</mo>
    6212
    1
            <mn>56</mn>
    6213
    1
            </mrow>
    6214
    1
          </math>";
    6215
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6216
    1
      }
    6217
    6218
      #[test]
    6219
    1
        fn not_digit_block_ellipsis() -> Result<()> {
    6220
    1
            let test_str = "<math><mrow><mn>8</mn><mo>,</mo><mn>123</mn><mo>,</mo><mn>456</mn><mo>,</mo>
    6221
    1
                        <mi>…</mi></mrow></math>";
    6222
    1
            let target_str = "<math>
    6223
    1
        <mrow>
    6224
    1
          <mn>8</mn>
    6225
    1
          <mo>,</mo>
    6226
    1
          <mn>123</mn>
    6227
    1
          <mo>,</mo>
    6228
    1
          <mn>456</mn>
    6229
    1
          <mo>,</mo>
    6230
    1
          <mi>…</mi>
    6231
    1
        </mrow>
    6232
    1
         </math>";
    6233
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6234
    1
      }
    6235
    6236
      #[test]
    6237
    1
        fn not_digit_block_negative_numbers_euro() -> Result<()> {
    6238
    1
            let test_str = "<math><mrow>
    6239
    1
          <mo>-</mo><mn>1</mn><mo>,</mo>
    6240
    1
          <mo>-</mo><mn>2</mn><mo>,</mo>
    6241
    1
          <mo>-</mo><mn>3</mn><mo>,</mo>
    6242
    1
          <mo>&#x2026;</mo>
    6243
    1
        </mrow></math>";
    6244
    1
            let target_str = "<math><mrow>
    6245
    1
            <mrow data-changed='added'>
    6246
    1
              <mo>-</mo>
    6247
    1
              <mn>1</mn>
    6248
    1
            </mrow>
    6249
    1
            <mo>,</mo>
    6250
    1
            <mrow data-changed='added'>
    6251
    1
              <mo>-</mo>
    6252
    1
              <mn>2</mn>
    6253
    1
            </mrow>
    6254
    1
            <mo>,</mo>
    6255
    1
            <mrow data-changed='added'>
    6256
    1
              <mo>-</mo>
    6257
    1
              <mn>3</mn>
    6258
    1
            </mrow>
    6259
    1
            <mo>,</mo>
    6260
    1
            <mi>…</mi>
    6261
    1
          </mrow></math>";
    6262
    1
          are_strs_canonically_equal_with_locale(test_str, target_str, &[], " .", ",")
    6263
    1
      }
    6264
    6265
      #[test]
    6266
    1
        fn ellipsis() -> Result<()> {
    6267
    1
            let test_str = "<math><mn>5</mn><mo>,</mo><mo>.</mo><mo>.</mo><mo>.</mo><mo>,</mo><mn>8</mn><mo>,</mo>
    6268
    1
            <mn>9</mn><mo>,</mo><mo>.</mo><mo>.</mo><mo>.</mo><mo>,</mo><mn>11</mn><mo>,</mo>
    6269
    1
            <mn>5</mn><mo>,</mo><mo>.</mo><mo>.</mo><mo>,</mo><mn>8</mn>
    6270
    1
          </math>";
    6271
    1
            let target_str = "<math><mrow data-changed='added'>
    6272
    1
          <mn>5</mn><mo>,</mo><mi>…</mi><mo>,</mo><mn>8</mn><mo>,</mo>
    6273
    1
          <mn>9</mn><mo>,</mo><mi>…</mi><mo>,</mo><mn>11</mn><mo>,</mo>
    6274
    1
          <mn>5</mn><mo>,</mo><mrow data-changed='added'><mo>.</mo><mo>.</mo></mrow>
    6275
    1
          <mo>,</mo><mn>8</mn></mrow></math>";
    6276
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6277
    1
      }
    6278
    6279
    6280
      #[test]
    6281
    1
        fn no_merge_271() -> Result<()> {
    6282
    1
            let test_str = "<math><mrow><mo>{</mo>
    6283
    1
            <mrow><mn>2</mn><mo>,</mo><mn>4</mn><mo>,</mo><mn>6</mn></mrow>
    6284
    1
          <mo>}</mo></mrow></math>";
    6285
    1
            let target_str = "<math><mrow><mo>{</mo>
    6286
    1
            <mrow><mn>2</mn><mo>,</mo><mn>4</mn><mo>,</mo><mn>6</mn></mrow>
    6287
    1
          <mo>}</mo></mrow></math>";
    6288
    1
          are_strs_canonically_equal_with_locale(test_str, target_str, &[], " .", ",")
    6289
    1
      }
    6290
    6291
      #[test]
    6292
    1
        fn not_digit_block_271() -> Result<()> {
    6293
    1
            let test_str = "<math><mrow>
    6294
    1
            <mi>…</mi><mo>,</mo>
    6295
    1
            <mo>-</mo><mn>2</mn><mo>,</mo>
    6296
    1
            <mo>-</mo><mn>1</mn><mo>,</mo>
    6297
    1
            <mn>0</mn>
    6298
    1
          </mrow></math>";
    6299
    1
            let target_str = "<math> <mrow>
    6300
    1
          <mi>…</mi>
    6301
    1
          <mo>,</mo>
    6302
    1
          <mrow data-changed='added'><mo>-</mo><mn>2</mn></mrow>
    6303
    1
          <mo>,</mo>
    6304
    1
          <mrow data-changed='added'><mo>-</mo><mn>1</mn></mrow>
    6305
    1
          <mo>,</mo>
    6306
    1
          <mn>0</mn>
    6307
    1
          </mrow></math>";
    6308
    1
          are_strs_canonically_equal_with_locale(test_str, target_str, &[], " .", ",")
    6309
    1
      }
    6310
    6311
      #[test]
    6312
    1
        fn merge_decimal_in_list_271() -> Result<()> {
    6313
    1
            let test_str = "<math><mi>x</mi><mo>,</mo><mn>2</mn><mo>.</mo><mn>5</mn><mi>g</mi><mo>,</mo><mn>3</mn></math>";
    6314
    1
            let target_str = "<math> <mrow data-changed='added'>
    6315
    1
            <mi>x</mi>
    6316
    1
            <mo>,</mo>
    6317
    1
            <mrow data-changed='added'> <mn>2.5</mn> <mo data-changed='added'>&#x2062;</mo> <mi>g</mi> </mrow>
    6318
    1
            <mo>,</mo>
    6319
    1
            <mn>3</mn>
    6320
    1
          </mrow> </math>";
    6321
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6322
    1
      }
    6323
    6324
      #[test]
    6325
    1
        fn primes_common() -> Result<()> {
    6326
    1
            let test_str = "<math><msup><mn>5</mn><mo>'</mo></msup>
    6327
    1
                  <msup><mn>5</mn><mo>''</mo></msup>
    6328
    1
                  <msup><mn>8</mn><mrow><mo>'</mo><mo>'</mo></mrow></msup></math>";
    6329
    1
            let target_str = "<math>
    6330
    1
            <mrow data-changed='added'>
    6331
    1
            <msup>
    6332
    1
              <mn>5</mn>
    6333
    1
              <mo>′</mo>
    6334
    1
            </msup>
    6335
    1
            <mo data-changed='added'>&#x2062;</mo>
    6336
    1
            <msup>
    6337
    1
              <mn>5</mn>
    6338
    1
              <mo>″</mo>
    6339
    1
            </msup>
    6340
    1
            <mo data-changed='added'>&#x2062;</mo>
    6341
    1
            <msup>
    6342
    1
              <mn>8</mn>
    6343
    1
              <mo>″</mo>
    6344
    1
            </msup>
    6345
    1
            </mrow>
    6346
    1
          </math>";
    6347
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6348
    1
      }
    6349
    6350
      #[test]
    6351
    1
        fn primes_uncommon() -> Result<()> {
    6352
    1
            let test_str = "<math><msup><mn>5</mn><mo>''′</mo></msup>
    6353
    1
                  <msup><mn>5</mn><mo>''''</mo></msup>
    6354
    1
                  <msup><mn>8</mn><mrow><mo>′</mo><mo>⁗</mo></mrow></msup></math>";
    6355
    1
            let target_str = " <math>
    6356
    1
            <mrow data-changed='added'>
    6357
    1
            <msup>
    6358
    1
              <mn>5</mn>
    6359
    1
              <mo>‴</mo>
    6360
    1
            </msup>
    6361
    1
            <mo data-changed='added'>&#x2062;</mo>
    6362
    1
            <msup>
    6363
    1
              <mn>5</mn>
    6364
    1
              <mo>⁗</mo>
    6365
    1
            </msup>
    6366
    1
            <mo data-changed='added'>&#x2062;</mo>
    6367
    1
            <msup>
    6368
    1
              <mn>8</mn>
    6369
    1
              <mo>⁗′</mo>
    6370
    1
            </msup>
    6371
    1
            </mrow>
    6372
    1
          </math>";
    6373
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6374
    1
      }
    6375
    6376
      #[test]
    6377
    1
        fn merge_mi_test() -> Result<()> {
    6378
    1
            let test_str = "<math>
    6379
    1
          <mi>c</mi><mi>o</mi><mi>s</mi><mo>=</mo>
    6380
    1
          <mi>w</mi><mi>x</mi><mi>y</mi><mi>z</mi><mo>+</mo>
    6381
    1
          <mi>n</mi><mi>a</mi><mi>x</mi><mo>+</mo>
    6382
    1
            <mi>i</mi><mi>ω</mi><mi>t</mi><mo>+</mo>
    6383
    1
          <mi>f</mi><mi>l</mi><mi>o</mi><mi>w</mi><mo>+</mo>
    6384
    1
          <mi>m</mi><mi>a</mi><mi>x</mi>
    6385
    1
        </math>
    6386
    1
      ";
    6387
    1
            let target_str = "<math>
    6388
    1
        <mrow data-changed='added'>
    6389
    1
          <mi>cos</mi>
    6390
    1
          <mo>=</mo>
    6391
    1
          <mrow data-changed='added'>
    6392
    1
            <mrow data-changed='added'>
    6393
    1
              <mi>w</mi>
    6394
    1
              <mo data-changed='added'>&#x2062;</mo>
    6395
    1
              <mi>x</mi>
    6396
    1
              <mo data-changed='added'>&#x2062;</mo>
    6397
    1
              <mi>y</mi>
    6398
    1
              <mo data-changed='added'>&#x2062;</mo>
    6399
    1
              <mi>z</mi>
    6400
    1
            </mrow>
    6401
    1
            <mo>+</mo>
    6402
    1
            <mrow data-changed='added'>
    6403
    1
              <mi>n</mi>
    6404
    1
              <mo data-changed='added'>&#x2062;</mo>
    6405
    1
              <mi>a</mi>
    6406
    1
              <mo data-changed='added'>&#x2062;</mo>
    6407
    1
              <mi>x</mi>
    6408
    1
            </mrow>
    6409
    1
            <mo>+</mo>
    6410
    1
            <mrow data-changed='added'>
    6411
    1
              <mi>i</mi>
    6412
    1
              <mo data-changed='added'>&#x2062;</mo>
    6413
    1
              <mi>ω</mi>
    6414
    1
              <mo data-changed='added'>&#x2062;</mo>
    6415
    1
              <mi>t</mi>
    6416
    1
            </mrow>
    6417
    1
            <mo>+</mo>
    6418
    1
            <mi>flow</mi>
    6419
    1
            <mo>+</mo>
    6420
    1
            <mi>max</mi>
    6421
    1
          </mrow>
    6422
    1
          </mrow>
    6423
    1
        </math>";
    6424
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6425
    1
      }
    6426
    6427
      #[test]
    6428
    1
        fn merge_mi_with_script_test() -> Result<()> {
    6429
    1
            let test_str = "<math>
    6430
    1
          <mi>c</mi><mi>o</mi><msup><mi>s</mi><mn>2</mn></msup><mi>y</mi><mo>=</mo>
    6431
    1
          <mi>l</mi><mi>o</mi><msup><mi>g</mi><mn>2</mn></msup><mi>y</mi><mo>+</mo>
    6432
    1
          <mi>d</mi><mi>a</mi><msup><mi>g</mi><mn>2</mn></msup>
    6433
    1
        </math>";
    6434
    1
            let target_str = "<math>
    6435
    1
            <mrow data-changed='added'>
    6436
    1
              <mrow data-changed='added'>
    6437
    1
                <msup>
    6438
    1
                  <mi>cos</mi>
    6439
    1
                  <mn>2</mn>
    6440
    1
                </msup>
    6441
    1
                <mo data-changed='added'>&#x2061;</mo>
    6442
    1
                <mi>y</mi>
    6443
    1
              </mrow>
    6444
    1
              <mo>=</mo>
    6445
    1
              <mrow data-changed='added'>
    6446
    1
                <mrow data-changed='added'>
    6447
    1
                  <msup>
    6448
    1
                    <mi>log</mi>
    6449
    1
                    <mn>2</mn>
    6450
    1
                  </msup>
    6451
    1
                  <mo data-changed='added'>&#x2061;</mo>
    6452
    1
                  <mi>y</mi>
    6453
    1
                </mrow>
    6454
    1
                <mo>+</mo>
    6455
    1
                <mrow data-changed='added'>
    6456
    1
                  <mi>d</mi>
    6457
    1
                  <mo data-changed='added'>&#x2062;</mo>
    6458
    1
                  <mi>a</mi>
    6459
    1
                  <mo data-changed='added'>&#x2062;</mo>
    6460
    1
                  <msup>
    6461
    1
                    <mi>g</mi>
    6462
    1
                    <mn>2</mn>
    6463
    1
                  </msup>
    6464
    1
                </mrow>
    6465
    1
              </mrow>
    6466
    1
            </mrow>
    6467
    1
          </math>";
    6468
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6469
    1
      }
    6470
    6471
      #[test]
    6472
    1
        fn merge_mi_with_script_bug_333_test() -> Result<()> {
    6473
    1
            let test_str = "<math>
    6474
    1
          <mi>l</mi><mi>o</mi><msub><mrow><mi>g</mi></mrow><mrow><mn>2</mn></mrow></msub><mo>=</mo>
    6475
    1
          <mi>l</mi><mi>i</mi><msub><mrow><mi>m</mi></mrow><mrow><mi>n</mi><mo>→</mo><mi>∞</mi></mrow></msub>
    6476
    1
        </math>
    6477
    1
      ";
    6478
    1
            let target_str = " <math>
    6479
    1
            <mrow data-changed='added'>
    6480
    1
            <msub>
    6481
    1
              <mi>log</mi>
    6482
    1
              <mn>2</mn>
    6483
    1
            </msub>
    6484
    1
            <mo>=</mo>
    6485
    1
            <msub>
    6486
    1
              <mi>lim</mi>
    6487
    1
              <mrow>
    6488
    1
              <mi>n</mi>
    6489
    1
              <mo>→</mo>
    6490
    1
              <mi>∞</mi>
    6491
    1
              </mrow>
    6492
    1
            </msub>
    6493
    1
            </mrow>
    6494
    1
          </math>";
    6495
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6496
    1
      }
    6497
    6498
      #[test]
    6499
    1
        fn merge_mi_bug_545() -> Result<()> {
    6500
    1
            let test_str = "<math><mi>S</mi><mi>I</mi><msup><mi>N</mi><mrow><mo>-</mo><mn>1</mn></mrow></msup></math>";
    6501
    1
            let target_str = "<math><msup><mi mathvariant='normal'>SIN</mi><mrow><mo>-</mo><mn>1</mn></mrow></msup></math>";
    6502
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6503
    1
      }
    6504
    6505
      #[test]
    6506
    1
        fn parent_bug_94() -> Result<()> {
    6507
        // This is a test to make sure the crash in the bug report doesn't happen.
    6508
        // Note: in the bug, they behavior they would like is a single mn with content "0.02"
    6509
        // However, TeX input "1 2 3" will produce three consecutive <mn>s, so merging <mn>s isn't good in general
    6510
        // This test
    6511
    1
            let test_str = " <math>
    6512
    1
          <mrow>
    6513
    1
            <msqrt>
    6514
    1
              <mrow>
    6515
    1
                <mstyle mathvariant='bold' mathsize='normal'><mn>0</mn></mstyle>
    6516
    1
                <mstyle mathvariant='bold' mathsize='normal'><mo>.</mo><mn>0</mn><mn>2</mn></mstyle>
    6517
    1
              </mrow>
    6518
    1
            </msqrt>
    6519
    1
          </mrow>
    6520
    1
        </math>
    6521
    1
        ";
    6522
    1
          let target_str = "<math>
    6523
    1
          <msqrt>
    6524
    1
            <mn mathsize='normal' mathvariant='bold' data-changed='added'>0.02</mn>
    6525
    1
          </msqrt>
    6526
    1
        </math>";
    6527
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6528
    1
      }
    6529
    6530
      #[test]
    6531
    1
      fn mstyle_merge_bug_272() -> Result<()> {
    6532
    1
            let test_str = r#"<math>
    6533
    1
          <msup>
    6534
    1
            <mstyle mathvariant="bold" mathsize="normal">
    6535
    1
              <mn>6</mn>
    6536
    1
            </mstyle>
    6537
    1
            <mstyle mathvariant="bold" mathsize="normal">
    6538
    1
              <mn>9</mn>
    6539
    1
            </mstyle>
    6540
    1
          </msup>
    6541
    1
        </math>"#;
    6542
    1
          let target_str = "<math>
    6543
    1
          <msup>
    6544
    1
          <mn mathsize='normal' mathvariant='bold'>𝟔</mn>
    6545
    1
          <mn mathsize='normal' mathvariant='bold'>𝟗</mn>
    6546
    1
          </msup>
    6547
    1
        </math>";
    6548
    1
        are_strs_canonically_equal_result(test_str, target_str, &[])
    6549
    1
      }
    6550
    6551
    6552
      #[test]
    6553
    1
      fn munder_mspace_bug_296() -> Result<()> {
    6554
        // this was a "typo" bug that should have looking embellished base
    6555
    1
            let test_str = r#"<math>
    6556
    1
          <mrow><mn>5</mn><mfrac><mn>9</mn><mrow><mn>10</mn></mrow></mfrac>
    6557
    1
            <munder accentunder="true"><mspace width="2.7em" /><mo stretchy="true">_</mo></munder>
    6558
    1
            </mrow></math>"#;
    6559
    1
          let target_str = "<math><mrow>
    6560
    1
            <mrow data-changed='added'>
    6561
    1
              <mn>5</mn>
    6562
    1
              <mo data-changed='added'>&#x2064;</mo>
    6563
    1
              <mfrac> <mn>9</mn><mn>10</mn> </mfrac>
    6564
    1
            </mrow>
    6565
    1
            <munder accentunder='true'>
    6566
    1
              <mo width='2.7em' data-changed='was-mspace' data-width='2.7' data-empty-in-2D='true' data-function-likelihood='false'> </mo>
    6567
    1
              <mo stretchy='true'>¯</mo>
    6568
    1
            </munder>
    6569
    1
          </mrow></math>";
    6570
    1
        are_strs_canonically_equal_result(test_str, target_str, &[])
    6571
    1
      }
    6572
    6573
      #[test]
    6574
    1
      fn parse_scripted_open_paren_439() -> Result<()> {
    6575
        // this was a "typo" bug that should have looking embellished base
    6576
    1
            let test_str = r#"<math><mrow><msub><mo>(</mo><mn>2</mn></msub><mo>)</mo></mrow></math>"#;
    6577
    1
          let target_str = "<math><mrow><msub><mo>(</mo><mn>2</mn></msub><mo>)</mo></mrow></math>";
    6578
    1
        are_strs_canonically_equal_result(test_str, target_str, &[])
    6579
    1
      }
    6580
    6581
      #[test]
    6582
    1
        fn lift_script() -> Result<()> {
    6583
    1
            let test_str = "<math xmlns='http://www.w3.org/1998/Math/MathML' >
    6584
    1
        <mrow>
    6585
    1
          <mstyle scriptlevel='0' displaystyle='true'>
    6586
    1
          <mrow>
    6587
    1
            <msqrt>
    6588
    1
            <munder>
    6589
    1
              <mo>∑<!-- ∑ --></mo>
    6590
    1
              <mrow>
    6591
    1
              <mn>0</mn>
    6592
    1
              <mo>≤<!-- ≤ --></mo>
    6593
    1
              <mi>k</mi>
    6594
    1
              <mo>≤<!-- ≤ --></mo>
    6595
    1
              <mi>n</mi>
    6596
    1
              </mrow>
    6597
    1
            </munder>
    6598
    1
            <mrow>
    6599
    1
              <mo stretchy='false'>|</mo>
    6600
    1
            </mrow>
    6601
    1
            <msub>
    6602
    1
              <mi>a</mi>
    6603
    1
              <mrow>
    6604
    1
              <mi>k</mi>
    6605
    1
              </mrow>
    6606
    1
            </msub>
    6607
    1
            <msup>
    6608
    1
              <mrow>
    6609
    1
              <mo stretchy='false'>|</mo>
    6610
    1
              </mrow>
    6611
    1
              <mrow>
    6612
    1
              <mn>2</mn>
    6613
    1
              </mrow>
    6614
    1
            </msup>
    6615
    1
            </msqrt>
    6616
    1
          </mrow>
    6617
    1
          </mstyle>
    6618
    1
        </mrow>
    6619
    1
        </math>";
    6620
    1
            let target_str = "<math>
    6621
    1
        <msqrt scriptlevel='0' displaystyle='true'>
    6622
    1
          <mrow data-changed='added'>
    6623
    1
          <munder>
    6624
    1
            <mo>∑</mo>
    6625
    1
            <mrow>
    6626
    1
            <mn>0</mn>
    6627
    1
            <mo>≤</mo>
    6628
    1
            <mi>k</mi>
    6629
    1
            <mo>≤</mo>
    6630
    1
            <mi>n</mi>
    6631
    1
            </mrow>
    6632
    1
          </munder>
    6633
    1
          <msup>
    6634
    1
            <mrow data-changed='added'>
    6635
    1
            <mo stretchy='false'>|</mo>
    6636
    1
            <msub>
    6637
    1
              <mi>a</mi>
    6638
    1
              <mi>k</mi>
    6639
    1
            </msub>
    6640
    1
            <mo stretchy='false'>|</mo>
    6641
    1
            </mrow>
    6642
    1
            <mn>2</mn>
    6643
    1
          </msup>
    6644
    1
          </mrow>
    6645
    1
        </msqrt>
    6646
    1
         </math>";
    6647
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6648
    1
      }
    6649
    6650
      #[test]
    6651
    1
        fn pseudo_scripts() -> Result<()> {
    6652
    1
            let test_str = "<math><mrow>
    6653
    1
            <mi>cos</mi><mn>30</mn><mo>°</mo>
    6654
    1
            <mi>sin</mi><mn>60</mn><mo>′</mo>
    6655
    1
            </mrow></math>";
    6656
    1
            let target_str = "<math>
    6657
    1
        <mrow>
    6658
    1
          <mrow data-changed='added'>
    6659
    1
          <mi>cos</mi>
    6660
    1
          <mo data-changed='added'>&#x2061;</mo>
    6661
    1
          <msup data-changed='added'><mn>30</mn><mo>°</mo></msup>
    6662
    1
          </mrow>
    6663
    1
          <mo data-changed='added'>&#x2062;</mo>
    6664
    1
          <mrow data-changed='added'>
    6665
    1
          <mi>sin</mi>
    6666
    1
          <mo data-changed='added'>&#x2061;</mo>
    6667
    1
          <msup data-changed='added'><mn>60</mn><mo>′</mo></msup>
    6668
    1
          </mrow>
    6669
    1
        </mrow>
    6670
    1
         </math>";
    6671
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6672
    1
      }
    6673
    6674
      #[test]
    6675
    1
        fn pseudo_scripts_in_mi() -> Result<()> {
    6676
    1
            let test_str = "<math><mrow><mi>p'</mi><mo>=</mo><mi>µ°C</mi></mrow></math>";
    6677
    1
            let target_str = "<math><mrow><msup><mi>p</mi><mo>′</mo></msup><mo>=</mo><mi>µ°C</mi></mrow></math>";
    6678
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6679
    1
      }
    6680
    6681
      #[test]
    6682
    1
        fn prescript_only() -> Result<()> {
    6683
    1
            let test_str = "<math><msub><mtext/><mn>92</mn></msub><mi>U</mi></math>";
    6684
    1
            let target_str = "<math><mmultiscripts><mi>U</mi><mprescripts/> <mn>92</mn><none/> </mmultiscripts></math>";
    6685
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6686
    1
      }
    6687
    6688
      #[test]
    6689
    1
        fn pre_and_postscript_only() -> Result<()> {
    6690
    1
            let test_str = "<math>
    6691
    1
          <msub><mrow/><mn>0</mn></msub>
    6692
    1
          <msub><mi>F</mi><mn>1</mn></msub>
    6693
    1
          <mo stretchy='false'>(</mo>
    6694
    1
          <mi>a</mi><mo>,</mo><mi>b</mi><mo>;</mo><mi>c</mi><mo>;</mo><mi>z</mi>
    6695
    1
          <mo stretchy='false'>)</mo>
    6696
    1
        </math>";
    6697
    1
          let target_str = " <math>
    6698
    1
          <mrow data-changed='added'>
    6699
    1
          <mmultiscripts>
    6700
    1
            <mi>F</mi>
    6701
    1
            <mn>1</mn>
    6702
    1
            <none></none>
    6703
    1
            <mprescripts></mprescripts>
    6704
    1
            <mn>0</mn>
    6705
    1
            <none></none>
    6706
    1
          </mmultiscripts>
    6707
    1
          <mo data-changed='added'>&#x2061;</mo>
    6708
    1
          <mrow data-changed='added'>
    6709
    1
            <mo stretchy='false'>(</mo>
    6710
    1
            <mrow data-changed='added'>
    6711
    1
            <mrow data-changed='added'>
    6712
    1
              <mi>a</mi>
    6713
    1
              <mo>,</mo>
    6714
    1
              <mi>b</mi>
    6715
    1
            </mrow>
    6716
    1
            <mo>;</mo>
    6717
    1
            <mi>c</mi>
    6718
    1
            <mo>;</mo>
    6719
    1
            <mi>z</mi>
    6720
    1
            </mrow>
    6721
    1
            <mo stretchy='false'>)</mo>
    6722
    1
          </mrow>
    6723
    1
          </mrow>
    6724
    1
        </math>";
    6725
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6726
    1
      }
    6727
    6728
      #[test]
    6729
    1
        fn pointless_nones_in_mmultiscripts() -> Result<()> {
    6730
    1
            let test_str = "<math><mmultiscripts>
    6731
    1
            <mtext>C</mtext>
    6732
    1
            <none />
    6733
    1
            <none />
    6734
    1
            <mprescripts />
    6735
    1
            <mn>6</mn>
    6736
    1
            <mn>14</mn>
    6737
    1
          </mmultiscripts></math>";
    6738
    1
            let target_str = "<math>
    6739
    1
        <mmultiscripts data-chem-formula='6'>
    6740
    1
        <mtext data-chem-element='1'>C</mtext>
    6741
    1
        <mprescripts></mprescripts>
    6742
    1
        <mn>6</mn>
    6743
    1
        <mn>14</mn>
    6744
    1
        </mmultiscripts>
    6745
    1
        </math>";
    6746
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6747
    1
      }
    6748
    6749
      #[test]
    6750
    1
        fn empty_mmultiscripts_485() -> Result<()> {
    6751
    1
            let test_str = "<math><mmultiscripts>   </mmultiscripts></math>";
    6752
    1
            let target_str = ""; // shouldn't get to the point of comparing because the input is illegal.
    6753
    1
            let err = are_strs_canonically_equal_result(test_str, target_str, &[])
    6754
    1
                .expect_err("empty mmultiscripts should be rejected");
    6755
    1
            assert!(
    6756
    1
                err.to_string().contains("mmultiscripts has the wrong number of children:\n <mmultiscripts></mmultiscripts>"),
    6757
                "unexpected error message: {err}"
    6758
            );
    6759
    1
            Ok(())
    6760
    1
      }
    6761
    6762
      #[test]
    6763
    1
        fn empty_mmultiscripts_544() -> Result<()> {
    6764
    1
            let test_str = "<math><mmultiscripts><mrow/><mprescripts></mprescripts><mrow/><mrow/></mmultiscripts></math>";
    6765
    1
            let target_str = "<math> <mtext data-changed='empty_content' data-width='0'> </mtext></math>";
    6766
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6767
    1
      }
    6768
    6769
      #[test]
    6770
    1
        fn empty_mrows_in_mmultiscripts_306() -> Result<()> {
    6771
    1
            let test_str = "<math display='block'>
    6772
    1
          <mmultiscripts intent='_permutation:prefix(_of,$k,_from,$n)'>
    6773
    1
            <mi>P</mi>
    6774
    1
            <mi arg='k'>k</mi>
    6775
    1
            <mrow/>
    6776
    1
            <mprescripts/>
    6777
    1
            <mrow/>
    6778
    1
            <mi arg='n'>n</mi>
    6779
    1
          </mmultiscripts>
    6780
    1
        </math>";
    6781
    1
            let target_str = "<math display='block'>
    6782
    1
          <mmultiscripts intent='_permutation:prefix(_of,$k,_from,$n)'>
    6783
    1
            <mi>P</mi>
    6784
    1
            <mi arg='k'>k</mi>
    6785
    1
            <none></none>
    6786
    1
            <mprescripts></mprescripts>
    6787
    1
            <none></none>
    6788
    1
            <mi arg='n'>n</mi>
    6789
    1
          </mmultiscripts>
    6790
    1
        </math>";
    6791
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6792
    1
      }
    6793
    6794
    6795
      #[test]
    6796
      #[ignore] // this fails -- need to figure out grabbing base from previous or next child
    6797
    0
        fn tensor() -> Result<()> {
    6798
    0
            let test_str = "<math>
    6799
    0
            <msub><mi>R</mi><mi>i</mi></msub>
    6800
    0
            <msup><mrow/><mi>j</mi></msup>
    6801
    0
            <msub><mrow/><mi>k</mi></msub>
    6802
    0
            <msub><mrow/><mi>l</mi></msub>
    6803
    0
          </math>";
    6804
    0
        let target_str = "<math>
    6805
    0
          <mmultiscripts>
    6806
    0
            <mi> R </mi>
    6807
    0
            <mi> i </mi>
    6808
    0
            <none/>
    6809
    0
            <none/>
    6810
    0
            <mi> j </mi>
    6811
    0
            <mi> k </mi>
    6812
    0
            <none/>
    6813
    0
            <mi> l </mi>
    6814
    0
            <none/>
    6815
    0
          </mmultiscripts>
    6816
    0
        </math>";
    6817
    0
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6818
    0
      }
    6819
    6820
    6821
      #[test]
    6822
    1
        fn test_nonascii_function_name() -> Result<()> {
    6823
    1
            let test_str = r#"<math>
    6824
    1
            <mi mathvariant="bold-italic">x</mi>
    6825
    1
            <mo>=</mo>
    6826
    1
            <mn>2</mn>
    6827
    1
            <mrow>
    6828
    1
            <mi>𝒔𝒊𝒏</mi>
    6829
    1
            <mo>&#x2061;</mo>
    6830
    1
            <mrow><mi mathvariant="bold-italic">t</mi></mrow>
    6831
    1
            </mrow>
    6832
    1
            <mo>-</mo>
    6833
    1
            <mn>1</mn>
    6834
    1
          </math>"#;
    6835
    1
        let target_str = r#"<math>
    6836
    1
          <mrow data-changed='added'>
    6837
    1
          <mi mathvariant='bold-italic'>𝒙</mi>
    6838
    1
          <mo>=</mo>
    6839
    1
          <mrow data-changed='added'>
    6840
    1
            <mrow data-changed='added'>
    6841
    1
            <mn>2</mn>
    6842
    1
            <mo data-changed='added'>&#x2062;</mo>
    6843
    1
            <mrow>
    6844
    1
              <mi>sin</mi>
    6845
    1
              <mo>&#x2061;</mo>
    6846
    1
              <mi mathvariant='bold-italic'>𝒕</mi>
    6847
    1
            </mrow>
    6848
    1
            </mrow>
    6849
    1
            <mo>-</mo>
    6850
    1
            <mn>1</mn>
    6851
    1
          </mrow>
    6852
    1
          </mrow>
    6853
    1
        </math>"#;
    6854
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6855
    1
      }
    6856
    6857
      #[test]
    6858
    1
        fn test_nonascii_function_name_as_chars() -> Result<()> {
    6859
    1
            let test_str = r#"<math display="block">
    6860
    1
          <mi>&#x1D499;</mi>
    6861
    1
          <mo>=</mo>
    6862
    1
          <mrow>
    6863
    1
            <mrow>
    6864
    1
              <mi>&#x1D484;</mi>
    6865
    1
              <mi>&#x1D490;</mi>
    6866
    1
              <mi>&#x1D494;</mi>
    6867
    1
            </mrow>
    6868
    1
            <mo>&#x2061;</mo>
    6869
    1
            <mrow>
    6870
    1
              <mi>&#x1D495;</mi>
    6871
    1
            </mrow>
    6872
    1
          </mrow>
    6873
    1
          <mo>+</mo>
    6874
    1
          <mn>&#x1D7D0;</mn>
    6875
    1
        </math>"#;
    6876
    1
        let target_str = r#"<math display='block'>
    6877
    1
          <mrow data-changed='added'>
    6878
    1
            <mi>𝒙</mi>
    6879
    1
            <mo>=</mo>
    6880
    1
            <mrow data-changed='added'>
    6881
    1
              <mrow>
    6882
    1
              <mi>cos</mi>
    6883
    1
              <mo>&#x2061;</mo>
    6884
    1
              <mi>𝒕</mi>
    6885
    1
              </mrow>
    6886
    1
              <mo>+</mo>
    6887
    1
              <mn>𝟐</mn>
    6888
    1
            </mrow>
    6889
    1
          </mrow>
    6890
    1
        </math>"#;
    6891
    1
            are_strs_canonically_equal_result(test_str, target_str, &[])
    6892
    1
      }
    6893
    6894
    6895
    }