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

    Line
    Count
    Source
    1
    #![allow(clippy::needless_return)]
    2
    3
    // Chemistry terms used here:
    4
    // chemical formula -- this references a molecule (one or more elements with bonds between them), including its state.
    5
    // chemical equation -- this is a notation specialized to chemistry -- it has concentration, arrows, equality, "addition" along with
    6
    //    some special symbols for operators and (mostly) chemical formulas for operands.
    7
    //    Operand exceptions are the equilibrium constant, numbers, and identifiers.
    8
    //    Although a chemical equation is a superset of a chemical formula, because we want to distinguish the two (e.g., '=' is in both),
    9
    //      we require that chemical equation is an mrow
    10
    //    FIX?? -- can it be an adorned mrow?
    11
    //    Note: with the current definition, if any element in a potential chem equation is ruled out, the entire mrow is ruled out.
    12
    //
    13
    // The general flow is that for every element that looks like a chem formula/equation, we mark it with data-likely-[equation/formula]
    14
    // After we are done marking "likely", we go back and either delete them or replace them with data-[equation/formula].
    15
    // Note: anything already marked with data-[equation/formula] doesn't need recomputation later (essentially the result is cached)
    16
    //
    17
    // There is a chicken and egg problem with detecting chemistry: to more reliably detect it, we need good structure.
    18
    // However, to get the structure right (e.,g "=" being a double bond, not equality; chem elements being in 'mi's; ...),
    19
    //   we need to know "=" is part of a chemical formula.
    20
    // The imperfect solution used is:
    21
    //   As the final step of each recursive call to 'clean_mathml',
    22
    //     1. mi/mtext: is it a chemical element(s) or one of the symbols used in chemical formulas (not equations).
    23
    //        If so, mark it MAYBE_CHEMISTRY.
    24
    //     2. msub/msup/msubsup/mmultiscripts: is base marked MAYBE_CHEMISTRY and the scripts are potential adornments, mark it MAYBE_CHEMISTRY
    25
    //     3. mrows: these take a few passes (remember, they aren't structured properly yet)
    26
    //        On the assumption that chemistry is not common we implement a "show me" attitude before changing the structure.
    27
    //        Pass 1:
    28
    //        a) for any run of mi/mtext that can be re-split into chem elements, split them and mark them if it is at least 3 chars long
    29
    //        b) if there are any potential chem formula operators (e.g., "=" and ":") and the previous node is marked MAYBE_CHEMISTRY,
    30
    //           mark this as MAYBE_CHEMISTRY
    31
    //        Pass 2: (assuming something was marked in pass 1)
    32
    //        a) find the first marked child and then the last consecutive marked child and trim any mo's from the ends
    33
    //        b) evaluate the likelihood that the sequence is chemistry
    34
    //           yes: replace mathml children with new (potentially restructured) children
    35
    //           no: clear all the marks for the old children
    36
    // After canonicalization, we take another pass looking for chemical equations and marking them if found.
    37
    38
    use sxd_document::dom::{Element, Document, ChildOfElement};
    39
    use crate::canonicalize::*;
    40
    use crate::pretty_print::mml_to_string;
    41
    use crate::xpath_functions::{is_leaf, IsNode};
    42
    use regex::Regex;
    43
    use crate::xpath_functions::IsBracketed;
    44
    use phf::{phf_map, phf_set};
    45
    use std::convert::TryInto;
    46
    #[allow(unused_imports)]
    47
    use log::{error, debug};
    48
    use std::collections::HashSet;
    49
    use std::cmp::Ordering;
    50
    use crate::errors::*;
    51
    use std::sync::LazyLock;
    52
    53
    54
    pub static NOT_CHEMISTRY: i32 = -10000;  // should overwhelm any positive signal
    55
    static NOT_CHEMISTRY_THRESHOLD: i32 = -10000/2;  // value for testing -- that way some can be added to NOT_CHEMISTRY and still meet the test
    56
    static CHEMISTRY_THRESHOLD: i32 = 5;   // if this changes, change CHEMISTRY_THRESHOLD_STR
    57
    58
    59
    /// this might be chemistry -- should only exist during canonicalization
    60
    pub static MAYBE_CHEMISTRY: &str = "data-maybe-chemistry";
    61
    62
    /// Attr flag to indicate chemical equation
    63
    static CHEM_EQUATION: &str = "data-chem-equation";
    64
    /// Attr flag to indicate chemical formula
    65
    static CHEM_FORMULA: &str = "data-chem-formula";
    66
    /// Attr flag to indicate chemical element
    67
    static CHEM_ELEMENT: &str = "data-chem-element";
    68
    static CHEM_FORMULA_OPERATOR: &str = "data-chem-formula-op";
    69
    static CHEM_EQUATION_OPERATOR: &str = "data-chem-equation-op";
    70
    static CHEM_STATE: &str = "data-chem-state";
    71
    72
    /// mark a new chem element that happened due to splitting a leaf
    73
    pub static SPLIT_TOKEN: &str = "data-split";
    74
    75
    /// mark a new chem element that happened due to merging two leaves
    76
    static MERGED_TOKEN: &str = "data-merged";
    77
    78
    /// these can be in the base of an under/over script
    79
    6.64k
    fn is_chem_equation_arrow(ch: char) -> bool {
    80
    6.64k
        matches!6.44k(ch,
    81
            '→' | '➔' | '←' | '⟶' | '⟵' | '⤻' | '⇋' | '⇌' |
    82
            '↑' | '↓' | '↿' | '↾' | '⇃' | '⇂' | '⥮' | '⥯' | '⇷' | '⇸' | '⤉' | '⤈' |
    83
            '⥂' | '⥄' | '⥃' |
    84
            '\u{1f8d0}' | '\u{1f8d1}' | '\u{1f8d2}' | '\u{1f8d3}' | '\u{1f8d4}' | '\u{1f8d5}'  // proposed Unicode equilibrium arrows
    85
        )
    86
    6.64k
    }
    87
    88
    // Returns true if the 'property' (should have ":") is in the intent
    89
    195k
    fn has_chem_intent(mathml: Element, property: &str) -> bool {
    90
    195k
        if let Some(intent16.9k) = mathml.attribute_value(INTENT_ATTR) {
    91
    16.9k
            let head = intent.split('(').next().unwrap();
    92
    16.9k
            return head.contains(property);
    93
    179k
        }
    94
    179k
        return false;
    95
    195k
    }
    96
    97
    26.7k
    fn has_inherited_property(mathml: Element, property: &str) -> bool {
    98
    26.7k
        let mut current = mathml;
    99
        loop {
    100
    101k
            if has_chem_intent(current, property) {
    101
    0
                return true;
    102
    101k
            }
    103
            // chem might not be temp node without a 'math' parent
    104
    101k
            if name(current) == "math" || current.parent()74.6k.is_none74.6k() {
    105
    26.7k
                break;
    106
    74.6k
            }
    107
    74.6k
            current = get_parent(current);
    108
        }
    109
    26.7k
        return false;
    110
    26.7k
    }
    111
    112
    30.2k
    pub fn is_chemistry_off(mathml: Element) -> bool {
    113
    30.2k
        if has_chem_intent(mathml, ":chemical-formula") || has_chem_intent(mathml, ":chemical-equation") {
    114
    4
            return false;
    115
    30.2k
        }
    116
    30.2k
        let pref_manager = crate::prefs::PreferenceManager::get();
    117
    30.2k
        return pref_manager.borrow().pref_to_string("Chemistry") == "Off";
    118
    30.2k
    }
    119
    120
    10.1k
    pub fn clean_chemistry_mrow(mathml: Element) {
    121
    10.1k
        if is_chemistry_off(mathml) {
    122
    0
            return;
    123
    10.1k
        }
    124
        // debug!("clean_chemistry_mrow:\n{}", mml_to_string(mathml));
    125
    10.1k
        let mut children = mathml.children().iter()
    126
    31.3k
                    .map10.1k(|child| as_element(*child))
    127
    10.1k
                    .collect::<Vec<Element>>();
    128
    10.1k
        if let Some(new_children246) = clean_mrow_children_restructure_pass(&children) {
    129
    246
            mathml.replace_children(&new_children);
    130
    246
            children = new_children;
    131
    9.93k
        }
    132
    10.1k
        clean_mrow_children_mark_pass(&children);
    133
    10.1k
    }
    134
    135
    /// Do some aggressive structural changes and if they make this look like a chemistry formula, mark it as one else remove other marks
    136
    /// Note: the element is replaced with a new restructured element if it is marked as chemistry
    137
    ///        Pass 1:
    138
    ///        a) for any run of mi/mtext that can be re-split into chem elements, split them and mark them if it is at least 3 chars long.
    139
    ///           Also split "(g)", etc., when in mi/mtext
    140
    ///        b) if there are any potential chem formula operators (e.g., "=" and ":") and the previous node is marked MAYBE_CHEMISTRY,
    141
    ///           mark this as MAYBE_CHEMISTRY
    142
    10.1k
    fn clean_mrow_children_restructure_pass<'a>(old_children: &[Element<'a>]) -> Option<Vec<Element<'a>>> {
    143
    10.1k
        let mut changed = false;
    144
    10.1k
        let mut new_children = Vec::with_capacity(2*old_children.len());
    145
    10.1k
        let mut i = 0;
    146
    40.7k
        while i < old_children.len() {
    147
    30.6k
            if let Some(paren_mrow_aq1) = clean_aq_state(old_children, i) {
    148
    1
                new_children.push(paren_mrow_aq);
    149
    1
                i += 4;                                 // skipping "( a q )"
    150
    1
                changed = true;
    151
    1
                continue;
    152
            } else {
    153
    30.6k
                let child = old_children[i];
    154
    30.6k
                let child_name = name(child);
    155
    30.6k
                if  child_name == "mi" || (child_name == "mtext"22.0k && as_text(child).len() < 4228) {
    156
                    // break mi/mtext that is done as "(g)", etc. Even if it isn't 'g', 'l', etc., it probably shouldn't be an mi/text.
    157
    8.62k
                    let text = as_text(child);
    158
    8.62k
                    if text.starts_with('(') && text4.ends_with4(')') {
    159
    4
                        let doc = child.document();
    160
    4
                        let state = create_mathml_element(&doc, "mi");
    161
    4
                        state.set_text(&text[1..text.len()-1]);
    162
    4
                        let open = create_mathml_element(&doc, "mo");
    163
    4
                        open.set_text("(");
    164
    4
                        let close = create_mathml_element(&doc, "mo");
    165
    4
                        close.set_text(")");
    166
    4
                        let mrow = create_mathml_element(&doc, "mrow");
    167
    4
                        mrow.append_children(&[open,state,close]);
    168
    4
                        new_children.push(mrow);
    169
    4
                        i += 1;
    170
    4
                        changed = true;
    171
    4
                        continue;
    172
    8.62k
                    }
    173
    21.9k
                } else if i + 2 < old_children.len() {
    174
                    // wrap with an mrow if we are not already an 'mrow'
    175
    9.68k
                    let parent = get_parent(child); // safe since 'math' is always at root
    176
    9.68k
                    if !(name(parent) == "mrow" && i == 02.86k && old_children.len() == 31.44k) &&
    177
    8.68k
                        let Some(paren_mrow377) = make_mrow(old_children[i..i+3].try_into().unwrap()) {
    178
                            // debug!("make_mrow added mrow");
    179
    377
                            new_children.push(paren_mrow);
    180
    377
                            i += 3;
    181
    377
                            changed = true;
    182
    377
                            continue;
    183
    9.30k
                        }
    184
    12.3k
                }
    185
    30.2k
                if child_name == "mo" {
    186
    9.50k
                    let likely_chemistry_op = likely_chem_formula_operator(child);
    187
                    // debug!("clean_mrow_children_restructure_pass -- in mo: likely {}, {}", likely_chemistry_op, mml_to_string(child));
    188
    9.50k
                    if likely_chemistry_op >= 0 {
    189
                        // if possible chemistry to left and right, then override text for operator lookup
    190
                        // note: on the right, we haven't set chem flag for operators yet, so we skip them
    191
    2.98k
                        let preceding = child.preceding_siblings();
    192
    2.98k
                        let following = child.following_siblings();
    193
    2.98k
                        if !preceding.is_empty() &&
    194
    1.84k
                           ( has_inherited_property(child, "chemical-formula") ||
    195
    2.27k
                             preceding.iter()1.84k.all1.84k(|&child| {
    196
    2.27k
                                let child = as_element(child);
    197
    2.27k
                                name(child)=="mn" || child2.13k.attribute(MAYBE_CHEMISTRY).is_some2.13k()}) &&
    198
    574
                                !following.is_empty()273 && following.iter()246.all246(|&child| {
    199
    574
                                    let child = as_element(child);
    200
    574
                                    name(child)=="mo" || name(child)=="mn"437 || child351.attribute(MAYBE_CHEMISTRY).is_some351()
    201
    574
                                })) {
    202
    146
                            // "=", etc., should be treated as high priority separators
    203
    146
                            // debug!("clean_mrow_children_restructure: child = {}", mml_to_string(child));
    204
    146
                            child.set_attribute_value(CHEMICAL_BOND, "true");
    205
    146
                            child.set_attribute_value(CHEM_FORMULA_OPERATOR, &likely_chemistry_op.to_string());
    206
    146
                            child.set_attribute_value(MAYBE_CHEMISTRY, &likely_chemistry_op.to_string());
    207
    2.83k
                        }
    208
    6.52k
                    } else {
    209
    6.52k
                        likely_chem_equation_operator(child);   // need to mark MAYBE_CHEMISTRY for CHEMICAL_BOND tests
    210
    6.52k
                    }
    211
    20.7k
                } else if child_name == "mrow" &&
    212
    2.05k
                          let Some(latex_value1) = child.attribute_value("data-latex") &&
    213
    1
                          latex_value == r"\mathrel{\longrightleftharpoons}" {
    214
    0
                    child.set_attribute_value("data-unicode", "\u{1f8d2}");
    215
    0
                    child.set_attribute_value(MAYBE_CHEMISTRY, "2");    // same as is_hack_for_missing_arrows()
    216
    20.7k
                }
    217
    30.2k
                i += 1;
    218
    30.2k
                new_children.push(child);
    219
            }
    220
        }
    221
    222
    10.1k
        return if changed {Some(new_children)246} else {None9.93k};
    223
    224
    225
        /// if it looks like we have ChemFormula ( a q ), merge the 'a' and 'q' together into an 'mi'
    226
        /// if not already true, structure '( aq )' into a single mrow (might be other elements on either side)
    227
        /// returns the last char matched
    228
    30.6k
        fn clean_aq_state<'a>(children: &[Element<'a>], i: usize) -> Option<Element<'a>> {
    229
    30.6k
            if i+3 >= children.len() || (i > 010.8k && children[i-1]9.38k.attribute(MAYBE_CHEMISTRY).is_none9.38k()) {
    230
    27.8k
                return None;       // can't be '( a q )' -- not enough elements left or not Chem Formula on left
    231
    2.79k
            }
    232
    233
            // this is a little sloppy in that we allow matching text in any leaf element, but we can use the same function
    234
    2.79k
            if is_text(children[i], "(") &&
    235
    244
               is_text(children[i+1], "a") && is_text9(children[i+2]9, "q"9) &&
    236
    1
               is_text(children[i+3], ")") {
    237
    1
                let mi = create_mathml_element(&children[i].document(), "mi");
    238
    1
                mi.set_text("aq");
    239
    1
                return make_mrow([children[i], mi, children[i+3]]);
    240
    2.79k
            }
    241
    2.79k
            return None;
    242
    30.6k
        }
    243
    244
    12.3k
        fn is_text(node: Element, target: &str) -> bool {
    245
    12.3k
            return is_leaf(node) && as_text(node) == target11.1k;
    246
    12.3k
        }
    247
    248
        /// Converts  "( child )" to mrow with those elements as children.
    249
        /// This is to make ascertaining whether this is a chemical state easier, but it is correct even if not a chemical state.
    250
    8.68k
        fn make_mrow(children: [Element; 3]) -> Option<Element> {
    251
            // this is a little sloppy in that we allow matching text in any leaf element, but we can use the same function
    252
    8.68k
            if is_text(children[0], "(") &&
    253
    631
               is_text(children[2], ")") {
    254
    378
          let mrow = create_mathml_element(&children[0].document(), "mrow");
    255
    378
          mrow.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    256
    378
          mrow.append_children(children);
    257
    378
                return Some(mrow);
    258
    8.31k
            }
    259
    8.31k
            return None;
    260
    8.68k
        }
    261
    10.1k
    }
    262
    263
    /// Pass 2: (assuming something was marked in pass 1)
    264
    /// a) find the first marked child and then the last consecutive marked child and trim any mo's from the ends
    265
    /// b) evaluate the likelihood that the sequence is chemistry
    266
    10.1k
    fn clean_mrow_children_mark_pass(children: &[Element]) {
    267
    10.1k
        let mut start = None;
    268
    30.6k
        for i in 0..children.len()10.1k {
    269
    30.6k
            let child = children[i];
    270
    30.6k
            if child.attribute(MAYBE_CHEMISTRY).is_some()  {
    271
    4.64k
                if start.is_none() {
    272
    3.63k
                    if name(child) == "mo" {
    273
    2.38k
                        // debug!(" start.is_none(): removing MAYBE_CHEMISTRY on {}", as_text(child));
    274
    2.38k
                        child.remove_attribute(MAYBE_CHEMISTRY);
    275
    2.38k
                        child.remove_attribute(CHEM_FORMULA_OPERATOR);
    276
    2.38k
                        child.remove_attribute(CHEM_EQUATION_OPERATOR);
    277
    2.38k
                        child.remove_attribute(CHEMICAL_BOND);
    278
    2.38k
                    } else {
    279
    1.25k
                        start = Some(i);
    280
    1.25k
                    }
    281
    1.00k
                }
    282
    25.9k
            } else if let Some(seq_start804) = start &&
    283
    804
                      remove_operators_at_end_of_sequence(children, seq_start, i) {
    284
    804
                start = None;
    285
    25.1k
            }
    286
        }
    287
    288
    10.1k
        if let Some(seq_start452) = start {
    289
    452
            remove_operators_at_end_of_sequence(children, seq_start, children.len());
    290
    9.73k
        }
    291
    10.1k
        return;
    292
    293
    294
    1.25k
        fn remove_operators_at_end_of_sequence(children: &[Element], start: usize, end: usize) -> bool {
    295
            // debug!("  looking for ops at end of {}..{}, last is:{}", start, end, mml_to_string(children[end-1]));
    296
    1.45k
            for stop in (start..end1.25k).rev1.25k() {
    297
    1.45k
                let end_child = children[stop];
    298
    1.45k
                if name(end_child) == "mo" {
    299
    202
                    end_child.remove_attribute(MAYBE_CHEMISTRY);
    300
    202
                } else {
    301
    1.25k
                    return true;
    302
                }
    303
            }
    304
    0
            return false
    305
    1.25k
    }
    306
    10.1k
    }
    307
    308
    309
    /// Very little software gets the token elements for chemistry right.
    310
    /// Sometimes multiple elements are in a single token (e.g. "NaCl") and sometimes
    311
    /// a single element is spread across multiple tokens (e.g. "N", "a").
    312
    ///
    313
    /// Here we attempt one or the other repair, but not both on the assumption there is
    314
    /// consistency in the error.
    315
    ///
    316
    /// Returns a Vec of the chemical elements or None. If a merge happened, the tree is altered.
    317
    12.3k
    pub fn convert_leaves_to_chem_elements(mathml: Element) -> Option<Vec<Element>> {
    318
        // gather up all the consecutive mi/mtext
    319
    12.3k
        if !(name(mathml) == "mi" || name(mathml) == "mtext"942) {
    320
    0
            return None;       // do nothing
    321
    12.3k
        }
    322
    323
        // we play games with the string to avoid allocation...
    324
    12.3k
        let token_string = as_text(mathml);
    325
    12.3k
        if !token_string.is_ascii() {
    326
    2.67k
            return None;    // chemical elements are ASCII
    327
    9.62k
        }
    328
    9.62k
        let doc = mathml.document();
    329
    9.62k
        if token_string.len() > 1 {   // safe because all chars are ASCII
    330
    2.54k
            return split_string_chem_element(&doc, mathml);
    331
    7.08k
        }
    332
    7.08k
        let parent = get_parent(mathml);
    333
    7.08k
        let parent_name = name(parent);
    334
    7.08k
        if !(parent_name == "mrow" || parent_name == "math"4.28k) {  // not canonicalized yet
    335
    2.57k
            return None;    // only try to merge if in an mrow
    336
    4.50k
        }
    337
    4.50k
        let answer = merge_tokens_chem_element(&doc, mathml, &mathml.following_siblings());
    338
    4.50k
        return answer;
    339
    340
    341
    4.50k
        fn merge_tokens_chem_element<'a>(doc: &Document<'a>, leaf: Element<'a>, following_siblings: &[ChildOfElement<'a>]) -> Option<Vec<Element<'a>>> {
    342
    4.50k
            if following_siblings.is_empty() {
    343
    1.22k
                return None;
    344
    3.28k
            }
    345
    3.28k
            let second_element = as_element(following_siblings[0]);
    346
    3.28k
            let second_element_name = name(second_element);
    347
    3.28k
            if second_element_name != "mi" && second_element_name != "mtext"3.05k {
    348
    3.02k
                return None;
    349
    256
            }
    350
    256
            let second_element_text = as_text(second_element);
    351
    256
            if second_element_text.len() != 1 {
    352
    57
                return None;
    353
    199
            }
    354
    199
            let token_string = as_text(leaf);
    355
    199
            let chem_token_string = vec![token_string.as_bytes()[0], second_element_text.as_bytes()[0]];
    356
    199
            if let Some(chem_element4) = get_chem_element(doc, &chem_token_string, 2) {
    357
    4
                chem_element.set_text(as_text(chem_element));
    358
    4
                chem_element.set_attribute_value(MAYBE_CHEMISTRY, chem_element.attribute_value(MAYBE_CHEMISTRY).unwrap());
    359
    4
                chem_element.set_attribute_value(MERGED_TOKEN, "true");
    360
    4
                second_element.remove_from_parent();
    361
    4
                return Some(vec![chem_element]);
    362
    195
            }
    363
    195
            return None;
    364
    4.50k
        }
    365
    366
        /// split the string which has been checked to be all ASCII chars
    367
    2.54k
        fn split_string_chem_element<'a>(doc: &Document<'a>, leaf: Element<'a>) -> Option<Vec<Element<'a>>> {
    368
    2.54k
            let token_string = as_text(leaf).as_bytes();
    369
    2.54k
            let token_len = token_string.len();
    370
    2.54k
            let mut j = 0;
    371
    2.54k
            let mut new_children = Vec::with_capacity(token_string.len());
    372
    3.31k
            while j < token_len {
    373
                // try elements of length 2 and 1, preferring longer elements (e.g., prefer "Na" over "N")
    374
    2.94k
                if let Some(chem_element310) = get_chem_element(doc, &token_string[j..], 2) {
    375
    310
                    new_children.push(chem_element);
    376
    310
                    j += 2;
    377
    310
                    continue;
    378
    2.63k
                } else if let Some(chem_element457) = get_chem_element(doc, &token_string[j..], 1) {
    379
    457
                    new_children.push(chem_element);
    380
    457
                    j += 1;
    381
    457
                    continue;
    382
    2.18k
                }
    383
    2.18k
                return None;    // didn't find a valid chem element
    384
            }
    385
    362
            if new_children.len() <= 1 {
    386
    231
                return None;
    387
    131
            }
    388
    131
            add_attrs(new_children[new_children.len()-1], &leaf.attributes());
    389
    131
            new_children[new_children.len()-1].set_attribute_value(SPLIT_TOKEN, "true");
    390
            // debug!("split_string_chem_element: {} -> {}", String::from_utf8(token_string.to_vec()).unwrap(), new_children.len());
    391
    131
            return Some(new_children);
    392
    2.54k
        }
    393
    394
        /// Returns element or None
    395
    5.78k
        fn get_chem_element<'a>(doc: &Document<'a>, bytes_str: &[u8], n: usize) -> Option<Element<'a>> {
    396
            use std::str;
    397
    5.78k
            let len = bytes_str.len();
    398
    5.78k
            if n > len {
    399
    277
                return None;    // can't be an chemical letter
    400
    5.50k
            }
    401
    5.50k
            match str::from_utf8(&bytes_str[..n]) {
    402
    5.50k
                Ok(chem_element) => {
    403
    5.50k
                    if CHEMICAL_ELEMENT_ELECTRONEGATIVITY.contains_key(chem_element) {
    404
    771
                        return Some(new_chemical_element(doc, chem_element));
    405
    4.73k
                    }
    406
    4.73k
                    return None;
    407
                }
    408
    0
                Err(_) => return None,
    409
            }
    410
    5.78k
        }
    411
    412
    771
        fn new_chemical_element<'a>(doc: &Document<'a>, chem_element_str: &str) -> Element<'a> {
    413
    771
            let result = create_mathml_element(doc, "mi");
    414
    771
            result.set_text(chem_element_str);
    415
    771
            result.set_attribute_value(MAYBE_CHEMISTRY, if chem_element_str.len() == 1 {"1"457} else {"3"314});
    416
    771
            if chem_element_str.len() == 1 {
    417
    457
                result.set_attribute_value("mathvariant", "normal");
    418
    457
            }314
    419
    771
            return result;
    420
    771
        }
    421
    12.3k
    }
    422
    423
    /// Looks at the children of the element and uses heuristics to decide whether this is a chemical equation/formula
    424
    /// If it is, it is marked with either data-chem-equation or data-chem-formula
    425
    /// This function assumes proper structure
    426
    ///
    427
    /// Returns true if not chemistry -- added attrs, mrows, and leaves are removed in preparation for a second parse
    428
    5.05k
    pub fn scan_and_mark_chemistry(mathml: Element) -> bool {
    429
    5.05k
        if is_chemistry_off(mathml) {
    430
    0
            return true;
    431
    5.05k
        }
    432
    433
    5.05k
        let child = as_element(mathml.children()[0]);
    434
        // debug!("scan_and_mark_chemistry:\n{}", mml_to_string(child));
    435
    5.05k
        assert_eq!(name(mathml), "math");
    436
    5.05k
        let is_chemistry = if let Some(latex5) = mathml.attribute_value("data-latex") {
    437
            // MathJax v4 includes this really useful info -- if it starts \ce -- we have Chemistry
    438
            // need to determine if it is an equation or a formula
    439
    5
            latex.trim_start().starts_with(r"\ce")
    440
        } else {
    441
    5.05k
            has_chem_intent(mathml, ":chemical-formula") || has_chem_intent(mathml, ":chemical-equation")
    442
        };
    443
    444
    5.05k
        if is_chemistry || is_chemistry_sanity_check5.05k(mathml5.05k) {
    445
    669
            assert_eq!(mathml.children().len(), 1);
    446
    669
            let likelihood = likely_chem_formula(child);
    447
    669
            if likelihood >= CHEMISTRY_THRESHOLD || has_chem_intent458(mathml458, ":chemical-formula"458) {
    448
    211
                child.set_attribute_value(MAYBE_CHEMISTRY, std::cmp::max(CHEMISTRY_THRESHOLD, likelihood).to_string().as_str());
    449
    211
                set_marked_chemistry_attr(child, CHEM_FORMULA);
    450
    458
            }
    451
    452
    669
            if child.attribute(CHEM_FORMULA).is_none() {
    453
                // can't be both an equation and a formula...
    454
    458
                let likelihood = likely_chem_equation(child);
    455
    458
                if is_chemistry || likelihood >= CHEMISTRY_THRESHOLD455 || has_chem_intent422(mathml422, ":chemical-equation"422) {
    456
    36
                    child.set_attribute_value(MAYBE_CHEMISTRY, std::cmp::max(CHEMISTRY_THRESHOLD, likelihood).to_string().as_str());
    457
    36
                    set_marked_chemistry_attr(child, CHEM_EQUATION);
    458
    422
                }
    459
    211
            }
    460
    4.38k
        }
    461
        // debug!("...after marking:\n{}", mml_to_string(child));
    462
    463
    5.05k
        if child.attribute(CHEM_FORMULA).is_none() && child4.84k.attribute(CHEM_EQUATION).is_none4.84k() {
    464
    4.80k
            if !has_maybe_chemistry(mathml) {
    465
    3.68k
                return true;    // quick check avoids needing a second parse due to removing added elements
    466
    1.12k
            }
    467
    1.12k
            return !is_changed_after_unmarking_chemistry(mathml);
    468
        } else {
    469
    247
            return true;
    470
        }
    471
    5.05k
    }
    472
    473
    // returns the marked attr value or None
    474
    16.2k
    fn get_marked_value(mathml: Element) -> Option<i32> {
    475
    16.2k
        return mathml.attribute_value(MAYBE_CHEMISTRY).map(|value| value3.11k.parse3.11k().unwrap3.11k());
    476
    16.2k
    }
    477
    478
    /// Sets the attr 'chem'
    479
    /// Recurse through all the children that have MAYBE_CHEMISTRY set
    480
    4.24k
    fn set_marked_chemistry_attr(mathml: Element, chem: &str) {
    481
    4.24k
        let tag_name = name(mathml);
    482
    4.24k
        if let Some(maybe_attr2.88k) = mathml.attribute(MAYBE_CHEMISTRY) {
    483
    2.88k
            maybe_attr.remove_from_parent();
    484
    485
    2.88k
            match tag_name {
    486
    2.88k
                "mi" | "mtext"2.09k => {852mathml852.set_attribute_value852(CHEM_ELEMENT852, maybe_attr.value());},
    487
    2.03k
                "mo" => {
    488
    686
                    if mathml.attribute(CHEM_FORMULA_OPERATOR).is_none() && mathml589.attribute(CHEM_EQUATION_OPERATOR).is_none589(){
    489
                        // don't mark as both formula and equation
    490
    433
                        mathml.set_attribute_value(if chem == CHEM_FORMULA {CHEM_FORMULA_OPERATOR216} else {CHEM_EQUATION_OPERATOR217}, maybe_attr.value());
    491
    253
                    }
    492
                },
    493
    1.35k
                "mn" => ()87,
    494
    1.26k
                "mrow" | "msub"515 | "msup"275 | "msubsup"216 | "mmultiscripts"213 => {
    495
    1.25k
                    let mut chem_name = chem;
    496
    1.25k
                    if tag_name != "mrow" && chem != CHEM_FORMULA505{
    497
                        // look at base -- if an mi/mtext then this is really a chemical formula
    498
    69
                        let base = as_element(mathml.children()[0]);
    499
    69
                        let base_name = name(base);
    500
    69
                        if base_name == "mi" || base_name == "mtext"8 {
    501
    63
                            chem_name = CHEM_FORMULA;
    502
    63
                        }6
    503
    1.18k
                    }
    504
    505
    1.25k
                    if mathml.attribute(CHEM_FORMULA).is_none() {
    506
    1.23k
                        // don't mark as both formula and equation
    507
    1.23k
                        mathml.set_attribute_value(chem_name, maybe_attr.value());
    508
    1.23k
                    }18
    509
    3.92k
                    for child in mathml1.25k.children1.25k() {
    510
    3.92k
                        set_marked_chemistry_attr(as_element(child), chem);
    511
    3.92k
                    };
    512
                }
    513
    10
                "mfrac" => {
    514
    0
                    let children = mathml.children();
    515
                    // debug!("mfrac children: {}", mml_to_string(mathml));
    516
    0
                    let numerator_is_chem_equation = IsBracketed::is_bracketed(as_element(children[0]), "[", "]", false, true);
    517
    0
                    let denominator_is_chem_equation = IsBracketed::is_bracketed(as_element(children[1]), "[", "]", false, true);
    518
    0
                    if  numerator_is_chem_equation && denominator_is_chem_equation {
    519
    0
                        mathml.set_attribute_value(CHEM_EQUATION, "true");
    520
    0
                    }
    521
                }
    522
    10
                _ => error!("Internal error: {tag_name} should not be marked as 'MAYBE_CHEMISTRY'"),
    523
            }
    524
    1.35k
        } else if tag_name == "mrow" {
    525
            // could have been added during canonicalization, so never marked. Recurse to the children
    526
    68
            for child in mathml33.children33() {
    527
    68
                set_marked_chemistry_attr(as_element(child), chem);
    528
    68
            };
    529
    1.32k
        }
    530
    4.24k
    }
    531
    532
    /// returns true if MAYBE_CHEMISTRY's occur within the element
    533
    41.3k
    fn has_maybe_chemistry(mathml: Element) -> bool {
    534
    41.3k
        if mathml.attribute(MAYBE_CHEMISTRY).is_some() {
    535
    1.12k
            return true;
    536
    40.2k
        }
    537
    40.2k
        if !is_leaf(mathml) {
    538
    36.5k
            for child in mathml17.9k.children17.9k() {
    539
    36.5k
                if has_maybe_chemistry(as_element(child)) {
    540
    3.15k
                    return true;
    541
    33.3k
                }
    542
            }
    543
    22.2k
        }
    544
    37.0k
        return false;
    545
    41.3k
    }
    546
    547
    /// Clears MAYBE_CHEMISTRY from this element and its decedents
    548
    /// Also deletes added mrows and leaves; returns true if anything is deleted
    549
    19.7k
    fn is_changed_after_unmarking_chemistry(mathml: Element) -> bool {
    550
    19.7k
        mathml.remove_attribute(MAYBE_CHEMISTRY);
    551
    19.7k
        if is_leaf(mathml) {
    552
            // don't bother testing for the attr -- just remove and nothing bad happens if they aren't there
    553
    13.3k
            mathml.remove_attribute(CHEM_FORMULA_OPERATOR);
    554
    13.3k
            mathml.remove_attribute(CHEM_EQUATION_OPERATOR);
    555
    13.3k
            mathml.remove_attribute(CHEMICAL_BOND);
    556
    13.3k
            if mathml.attribute(MERGED_TOKEN).is_some() {
    557
    3
                unmerge_element(mathml);
    558
    3
                return true;    // need to re-parse
    559
    13.3k
            } else if mathml.attribute(SPLIT_TOKEN).is_some() {
    560
    33
                if let Err(err0) = merge_element(mathml) {
    561
    0
                    panic!("{}", err);
    562
    33
                }
    563
                // debug!("After merge_element:{}", mml_to_string(mathml));
    564
                // let parent = get_parent(mathml);
    565
                // debug!("After merge_element: -- parent{}", mml_to_string(parent));
    566
    567
    13.3k
            } else if let Some(changed_value2.14k) = mathml.attribute_value(CHANGED_ATTR) &&
    568
    2.14k
                      changed_value == ADDED_ATTR_VALUE &&
    569
    2.11k
                      name(mathml) != "mtext" {  // a hack fix for #477 (chem never modifies mtext, so this is ok)
    570
    2.11k
                mathml.remove_from_parent();
    571
    2.11k
                return true;
    572
    11.1k
            }
    573
    11.2k
            return false;
    574
    6.38k
        } else if IsNode::is_scripted(mathml) &&
    575
    1.04k
                  name(as_element(mathml.children()[0])) == "mi" &&
    576
    575
                  as_element(mathml.children()[0]).attribute(SPLIT_TOKEN).is_some() {
    577
            // Undo a split that happened in a scripted element.
    578
            // We put the preceding elements into the base and call merge_element on the last element of the base
    579
            // The first and/or the last child in the sequence could be a script that needs to be unwrapped
    580
    1
            let mut parent = get_parent(mathml);   // there is always a "math" node
    581
            // debug!("mathml:\n{}", mml_to_string(mathml));
    582
            // debug!("parent before merge:\n{}", mml_to_string(parent));
    583
            // debug!("grandparent before merge:\n{}", mml_to_string(get_parent(parent)));
    584
    585
    1
            let mut preceding_children = mathml.preceding_siblings();
    586
            // could be no preceding children to canonicalization creating mrows (see issue #303), so might need to use parent, etc
    587
    2
            while preceding_children.is_empty() {
    588
    1
                preceding_children = parent.preceding_siblings();
    589
    1
                if name(parent) == "math" {
    590
    0
                    break;  // consider {SIN}^{-1} -- no preceding child
    591
    1
                }
    592
    1
                parent = get_parent(parent);
    593
            }
    594
    595
    1
            let mut new_script_children = vec![];
    596
    1
            if !preceding_children.is_empty() {
    597
                // deal with the first element (if it needs unwrapping, it has only prescripts)
    598
    1
                let first_element_of_split = as_element(preceding_children[preceding_children.len()-1]);
    599
                // debug!("first_element_of_split: \n{}", mml_to_string(first_element_of_split));
    600
    1
                if name(first_element_of_split) == "mmultiscripts" {
    601
                    // take the base and make it the first child of preceding_children (what will get merged)
    602
                    // put the rest of the elements (the prescripts) at the end of the parent last element (mathml) which must be an mmultiscripts
    603
    0
                    let first_element_children = first_element_of_split.children();
    604
    0
                    assert_eq!(name(mathml), "mmultiscripts");
    605
    0
                    let mut script_children = mathml.children();
    606
    0
                    assert_eq!(name(as_element(script_children[0])), "mi");
    607
    0
                    assert!(!script_children.len().is_multiple_of(2));  // doesn't have <mprescripts/>
    608
    0
                    script_children.push(first_element_children[1]);    // mprescripts
    609
    0
                    script_children.push(first_element_children[2]);    // prescripts subscript
    610
    0
                    script_children.push(first_element_children[3]);    // prescripts superscript
    611
    612
    0
                    let base_of_first_element = first_element_children[0];  // base
    613
    0
                    assert_eq!(name(as_element(base_of_first_element)), "mi");
    614
    0
                    let script_base = as_element(script_children[0]);
    615
    0
                    let mut merged_base_text = as_text( as_element(base_of_first_element)).to_string();
    616
    0
                    merged_base_text.push_str(as_text(script_base));
    617
    0
                    script_base.set_text(&merged_base_text);
    618
    0
                    script_base.remove_attribute("mathvariant");
    619
    0
                    script_base.remove_attribute(ADDED_ATTR_VALUE);
    620
    0
                    script_base.remove_attribute(MAYBE_CHEMISTRY);
    621
    0
                    script_base.remove_attribute(SPLIT_TOKEN);
    622
    0
                    mathml.replace_children(script_children);
    623
    624
    0
                    first_element_of_split.remove_from_parent();
    625
    0
                    return true;
    626
    1
                }
    627
    1
                new_script_children.push(ChildOfElement::Element(first_element_of_split));
    628
    0
            }
    629
    1
            debug!("mathml after handling preceding children:\n{}", mml_to_string0(mathml0));
    630
    1
            let mut children_of_script = mathml.children();
    631
    1
            let split_child = as_element(children_of_script[0]);
    632
    1
            new_script_children.append(&mut children_of_script);
    633
    1
            mathml.replace_children(new_script_children);     // temporarily has bad number of children
    634
            // debug!("After making bad script:\n{}", mml_to_string(mathml));
    635
    1
            if let Err(err0) = merge_element(split_child) {
    636
    0
                panic!("{}", err);
    637
    1
            }
    638
    1
            return true;
    639
        } else {
    640
    6.37k
            let mut answer = false;
    641
    18.5k
            for child in mathml6.37k.children6.37k() {
    642
    18.5k
                let child = as_element(child);
    643
    18.5k
                if name(child) == "mtd" && child77.attribute(MAYBE_CHEMISTRY).is_some77() {
    644
    2
                    answer = true;  // each mtd acts as a potential island for chemistry, so don't clear it
    645
    18.5k
                } else {
    646
    18.5k
                    answer |= is_changed_after_unmarking_chemistry(child);
    647
    18.5k
                }
    648
            }
    649
    6.37k
            if name(mathml) == "mrow" {
    650
    3.58k
                if let Some(changed_value2.86k) = mathml.attribute_value(CHANGED_ATTR) {
    651
                    // we added an mrow, we can remove it -- but this might be already processed which is the case if "data-id-added" is true (exists)
    652
    2.86k
                    if changed_value == ADDED_ATTR_VALUE && mathml.attribute("data-id-added").is_none() {
    653
                        // mrows get added for several reasons. One of them is to canonicalize elements like msqrt that can have 1 or more children;
    654
                        //   those should not get removed because the re-parse doesn't add those
    655
                        // Although they would never be added, elements with fixed number of children also shouldn't have the mrow go away
    656
                        // We are left with only removing mrows with one child or mrows that are children of mrows (simpler test than ELEMENTS_WITH_ONE_CHILD)
    657
    2.86k
                        let parent = get_parent(mathml);   // mathml is mrow, so parent always exists
    658
    2.86k
                        if mathml.children().len() == 1 || name(parent) == "mrow"2.84k {
    659
    6.26k
                            let children2.31k = mathml.children().iter()2.31k.map2.31k(|&el| as_element(el)).collect2.31k::<Vec<Element>>();
    660
    2.31k
                            mathml.remove_attribute(CHANGED_ATTR);  // if just one child, the attrs are pushed onto the child
    661
                            // debug!("is_changed_after_unmarking: before replace - parent\n{}", mml_to_string(parent));
    662
    2.31k
                            replace_children(mathml, children);
    663
                            // debug!("is_changed_after_unmarking: parent\n{}", mml_to_string(parent));
    664
    665
    557
                        }
    666
    0
                    }
    667
    720
                }
    668
    3.58k
                return true;
    669
    2.79k
            }
    670
    2.79k
            return answer;
    671
        }
    672
    673
    3
        fn unmerge_element(mathml: Element) {
    674
            // a merged token occurs when two single letters get merged into one. Here we recreate the two tokens
    675
    3
            assert!(is_leaf(mathml));
    676
            // debug!("unmerge_element: {}", mml_to_string(mathml));
    677
    3
            let mut token_str = as_text(mathml).chars();
    678
    3
            let first = create_mathml_element(&mathml.document(), name(mathml));
    679
    3
            first.set_text(&token_str.next().unwrap().to_string());
    680
    3
            let second = create_mathml_element(&mathml.document(), name(mathml));
    681
    3
            second.set_text(&token_str.next().unwrap().to_string());
    682
    3
            replace_children(mathml, vec![first, second]);
    683
    3
        }
    684
    685
        /// Put the split pieces back together (undo the split)
    686
    34
        fn merge_element(mathml: Element) -> Result<()> {
    687
            // debug!("merge_element: {}", mml_to_string(mathml));
    688
            // debug!("merge_element parent: {}", mml_to_string(get_parent(mathml)));
    689
    34
            assert!(is_leaf(mathml));
    690
    34
            let mut preceding_children = mathml.preceding_siblings();
    691
            // debug!("preceding_children: {}", preceding_children.iter().map(|&el| name(as_element(el)).to_string()).collect::<Vec<String>>().join(", "));
    692
    34
            if preceding_children.is_empty() {
    693
                // handle:
    694
                // * case where we have mi mmultiscripts mi ... where the second mi needs to join with the first (see test mhchem_so4)
    695
                // * case where the child got buried in an added mrow (can only happen one level deep because invisible times should get inserted)
    696
    0
                let parent = get_parent(mathml);   // mathml is leaf, so parent always exists
    697
    0
                preceding_children = parent.preceding_siblings();
    698
    0
                if preceding_children.is_empty() ||
    699
    0
                   !(name(parent) == "mmultiscripts" ||
    700
    0
                    (name(parent) == "mrow" && parent.attribute_value(CHANGED_ATTR).is_some() &&
    701
    0
                     parent.attribute_value(CHANGED_ATTR).unwrap() == ADDED_ATTR_VALUE)) {
    702
    0
                        bail!("Internal error: {} should not have been split'", mml_to_string(mathml));
    703
    0
                }
    704
    34
            }
    705
            // Note: there was an invisible U+2063, but it was removed before we got here
    706
            // The parent mrow could have many children that couldn't have been part of a split -- only consider feasible children to split (mi/mtext)
    707
            // To figure this out, we walk backwards adding the text in reverse and then reverse that text in the end
    708
    34
            let mut merged_text = Vec::default();
    709
    46
            for &child in preceding_children.iter()34.rev34() {
    710
    46
                let child = as_element(child);
    711
                // because this is before canonicalization, there could be an mrow with just mi/mtext
    712
    46
                if name(child) == "mrow" && child.children().len() == 10 && child.attribute(INTENT_ATTR)0.is_none0() {
    713
    0
                    // "lift" the child up so all the links (e.g., siblings) are correct
    714
    0
                    let child = as_element(child.children()[0]);
    715
    0
                    set_mathml_name(child, name(child));
    716
    0
                    crate::canonicalize::add_attrs(child, &child.attributes());
    717
    0
                    child.replace_children(child.children());
    718
    46
                }
    719
    46
                if name(child) != "mi" && name(child) != "mtext"12 {
    720
    12
                    break;
    721
    34
                }
    722
    34
                merged_text.push(as_text(child));
    723
    34
                child.remove_from_parent();
    724
            }
    725
    34
            merged_text.reverse();
    726
    34
            let mut merged_text = merged_text.join("");
    727
    34
            merged_text.push_str(as_text(mathml));
    728
    34
            mathml.set_text(&merged_text);
    729
    34
            mathml.remove_attribute("mathvariant");
    730
    34
            mathml.remove_attribute(ADDED_ATTR_VALUE);
    731
    34
            mathml.remove_attribute(MAYBE_CHEMISTRY);
    732
    34
            mathml.remove_attribute(SPLIT_TOKEN);
    733
    34
            return Ok( () );
    734
    34
        }
    735
    19.7k
    }
    736
    737
    /// Returns true only if 'mathml' potentially is chemistry.
    738
    /// This assumes canonicalization has happened and that 'mathml' is the 'math' element
    739
    5.05k
    fn is_chemistry_sanity_check(mathml: Element) -> bool {
    740
        // This does some sanity checking. More can definitely be done
    741
        // Checks:
    742
        // * there should be chemical elements
    743
        // * if the child is an mrow with three children, the operator should be '=' (not CHEMICAL_BOND) or  an arrow
    744
        //   in this case, we gather up the elements on the lhs and rhs. The sets should be equal and non-empty.
    745
        //   the exception is if there are prescripts, in which as we might have radioactive decay so we don't require the sets to be equal
    746
        // * otherwise, we gather up all the chemical elements and make sure the set is non-empty
    747
        // * if it isn't an mrow, we leave it to likely_chem_equation() to rule it out
    748
    5.05k
        assert_eq!(name(mathml), "math");
    749
    5.05k
        assert_eq!(mathml.children().len(), 1);
    750
    5.05k
        let mathml = as_element(mathml.children()[0]);
    751
    5.05k
        if name(mathml) == "mrow" {
    752
    3.29k
            let mrow_children = mathml.children();
    753
    3.29k
            if mrow_children.len() == 3 && is_arrow_or_equal2.52k(as_element2.52k(mrow_children[1]2.52k)) {
    754
    371
                let mut lhs_elements = HashSet::with_capacity(8);   // likely more than anything we'll encounter -- bigger affects '=' op
    755
    371
                let lhs_has_prescripts = gather_chemical_elements(as_element(mrow_children[0]), &mut lhs_elements);
    756
                // need to include the arrow as it might have the addition of some chemical elements (see UEB/iceb.rs/chem_16_5_2)
    757
    371
                gather_chemical_elements(as_element(mrow_children[1]), &mut lhs_elements);
    758
    371
                let mut rhs_elements = HashSet::with_capacity(8);  // likely more than anything we'll encounter -- bigger affects '=' op
    759
    371
                let rhs_has_prescripts = gather_chemical_elements(as_element(mrow_children[2]), &mut rhs_elements);
    760
    371
                if lhs_elements.is_empty() {
    761
    269
                    return false;
    762
    102
                }
    763
                // debug!("lhs/rhs elements: {:?}, {:?}", lhs_elements, rhs_elements);
    764
                // debug!("lhs/rhs has prescripts: {}, {}", lhs_has_prescripts, rhs_has_prescripts);
    765
    102
                if lhs_elements == rhs_elements {
    766
    37
                    return !(lhs_has_prescripts ^ rhs_has_prescripts);      // seems reasonable that if the lhs has prescripts, so should the rhs
    767
    65
                }
    768
    65
                return lhs_has_prescripts && rhs_has_prescripts32;    // non-equal sets only if radioactive decay.
    769
    2.92k
            }
    770
    1.76k
        }
    771
    4.68k
        let mut chem_elements = HashSet::with_capacity(8);   // likely more than anything we'll encounter -- bigger affects '=' op
    772
    4.68k
        gather_chemical_elements(mathml, &mut chem_elements);
    773
    4.68k
        return !chem_elements.is_empty();
    774
    775
    776
    2.52k
        fn is_arrow_or_equal(mathml: Element) -> bool {
    777
    2.52k
            let base = get_possible_embellished_node(mathml);
    778
    2.52k
            if name(base) != "mo" || mathml.attribute(CHEMICAL_BOND)1.98k.is_some1.98k() {
    779
    542
                return false;
    780
    1.98k
            }
    781
    1.98k
            let text = as_text(base);
    782
    1.98k
            return text == "=" || is_single_char_matching1.67k(text1.67k, is_chem_equation_arrow);
    783
    784
    2.52k
        }
    785
    786
        /// Gather up all the chemical elements in the element and return true if it has numerical prescripts
    787
    48.0k
        fn gather_chemical_elements<'a>(mathml: Element<'a>, chem_elements: &mut HashSet<&'a str>) -> bool {
    788
    48.0k
            match name(mathml) {
    789
    48.0k
                "mi" | "mtext"37.4k => {
    790
    10.8k
                    if is_chemical_element(mathml) {
    791
    1.60k
                        chem_elements.insert(as_text(mathml));
    792
    9.26k
                    }
    793
    10.8k
                    return false;
    794
                },
    795
    37.1k
                "msub" | "msup"36.5k | "msubsup"35.4k | "mmultiscripts"35.3k | "mover"35.0k => {
    796
    2.40k
                    gather_chemical_elements(get_possible_embellished_node(mathml), chem_elements);
    797
    2.40k
                    return name(mathml) == "mmultiscripts" &&  has_numerical_prescripts291(mathml291);
    798
                },
    799
    34.7k
                "semantics" => {
    800
    0
                    return gather_chemical_elements( get_presentation_element(mathml).1, chem_elements );
    801
                },
    802
    34.7k
               _ => if is_leaf(mathml) { return false21.2k; }13.5k,
    803
            }
    804
    805
            // mrow, msqrt, etc
    806
    13.5k
            let mut has_prescripts = false;
    807
    39.8k
            for child in mathml13.5k.children13.5k() {
    808
    39.8k
                let child = as_element(child);
    809
    39.8k
                has_prescripts |= gather_chemical_elements(child, chem_elements);
    810
    39.8k
            }
    811
    13.5k
            return has_prescripts;
    812
    48.0k
        }
    813
    814
            /// find the mprescripts child and then check the following siblings for numerical prescripts
    815
    291
        fn has_numerical_prescripts(mathml: Element) -> bool {
    816
    291
            let children = mathml.children();
    817
            // quick check to see if there is an mprescripts child
    818
    291
            if !children.len().is_multiple_of(2) { // <mprescripts/> => even number of children
    819
    129
                return false;
    820
    162
            }
    821
            // we need enumerate because the "step_by" will cause any returned iterator to jump ahead by 2
    822
    162
            let i_mprescripts = children.iter()
    823
    162
                .enumerate()
    824
    162
                .skip(1)
    825
    162
                .step_by(2)
    826
    222
                .find162(|(_, child)| name(as_element(**child)) == "mprescripts")
    827
    162
                .map(|(i, _)| i);
    828
    829
    162
            if let Some(i) = i_mprescripts {
    830
    162
                let subscript = as_element(children[i+1]);  // can be +1/-1 for beta decay
    831
    162
                let superscript = as_element(children[i+2]);  // mass number, so always >= 0
    832
    162
                if name(superscript) != "mn" {
    833
    55
                    return false;
    834
    107
                }
    835
    107
                return name(subscript) == "mn" ||
    836
    36
                       (name(subscript) == "mrow" && subscript.children().len() == 331 &&
    837
    0
                        name(as_element(subscript.children()[3])) == "mm" &&
    838
    0
                        name(as_element(subscript.children()[1])) == "mo" &&
    839
    0
                        matches!(as_text(as_element(subscript.children()[1])), "+" | "-"));
    840
    0
            }
    841
    0
            return false;
    842
    291
        }
    843
    5.05k
    }
    844
    845
    /// Looks at the children of the element and uses heuristics to decide whether this is a chemical equation.
    846
    /// This assumes canonicalization of characters has happened
    847
    713
    fn likely_chem_equation(mathml: Element) -> i32 {
    848
        // mfrac -- could be a ratio of concentrations
    849
    713
        if name(mathml) != "mrow" && name(mathml) != "mtd"127 && name(mathml) != "mfrac"120 {
    850
    119
            return NOT_CHEMISTRY;
    851
    594
        }
    852
    853
        // debug!("start likely_chem_equation:\n{}", mml_to_string(mathml));
    854
      // mrow -- check the children to see if we are likely to be a chemical equation
    855
    856
        // concentrations should either be unscripted or have a superscript that isn't a charge
    857
        // they occur in an mrow or mfrac
    858
    594
        if IsBracketed::is_bracketed(mathml, "[", "]", false, true) {
    859
    10
            let parent_name = name(get_parent(mathml));
    860
    10
            if parent_name == "mfrac" || parent_name == "mrow"  || parent_name == "math"9 ||
    861
    0
               (parent_name == "msup" && likely_chem_superscript(as_element(mathml.following_siblings()[0])) < 0){
    862
    10
                return if as_element(mathml.children()[0]).attribute(CHEM_FORMULA).is_some() {CHEMISTRY_THRESHOLD0}  else {NOT_CHEMISTRY};
    863
    0
            }
    864
    584
        }
    865
    866
        // possible improvement -- give bonus points for consecutive (not counting invisible separators) chemical elements on top of the existing points
    867
    584
      let mut likelihood = 0;           // indicator of likely match
    868
    584
      let mut has_equilibrium_constant = false;
    869
    584
        let children = mathml.children();
    870
    1.22k
      for i in 0..children.len()584 {
    871
    1.22k
        let child = as_element(children[i]);
    872
            // debug!("   i={}, likelihood={}, child={}", i, likelihood, crate::canonicalize::element_summary(child));
    873
    1.22k
            if let Some(likely457) = get_marked_value(child) {
    874
    457
                likelihood += likely;
    875
    457
                continue;
    876
    771
            }
    877
    771
        if i == children.len()-1 {
    878
    195
                let likely = likely_chem_state(child);
    879
    195
                if likely > 0 {
    880
    0
                    likelihood += likely;
    881
    0
                    break;
    882
    195
          }
    883
                // otherwise, check the last element as normal
    884
    576
            }
    885
    771
            let tag_name = name(child);
    886
    771
            let likely = match tag_name {
    887
    771
                "mi" => likely_chem_element146(child146),
    888
    625
                "mn" => 09,       // not much info
    889
    616
                "mo" | "mover"372 | "munder"352 | "munderover"308 =>  likely_chem_equation_operator330(child330),
    890
    286
                "msub" | "msup"259 | "msubsup"254 | "mmultiscripts"252 => {
    891
    38
                    if is_equilibrium_constant(child) {
    892
    0
                        has_equilibrium_constant = true;
    893
    0
                        2
    894
                    } else {
    895
    38
                        likely_adorned_chem_formula(child)
    896
                    }
    897
                },
    898
    248
                "mfrac" => {
    899
    0
                    if has_equilibrium_constant {
    900
    0
                        2
    901
                    } else {
    902
    0
                        -3    // fraction tend only to appear after an equilibrium constant
    903
                    }
    904
                },
    905
    248
                "mrow" => {
    906
    248
                    let likely = likely_chem_formula(child);
    907
    248
                    if likely < 0 {
    908
    248
                        likely_chem_equation(child)
    909
                    } else {
    910
    0
                        likely
    911
                    }
    912
                },
    913
                // no need to check for mtr or mtd because they only exist in a table and the recursion is dealt with here.
    914
    0
                "mtable" => {
    915
    0
                    for mrow in child.children() {
    916
    0
                        let mrow = as_element(mrow);
    917
    0
                        for mtd in mrow.children() {
    918
    0
                            let mtd = as_element(mtd);
    919
    0
                            let mut likely = likely_chem_formula(mtd);
    920
    0
                            if likely < CHEMISTRY_THRESHOLD {
    921
    0
                                likely = likely_chem_equation(mtd);
    922
    0
                            }
    923
    0
                            if likely < CHEMISTRY_THRESHOLD {
    924
    0
                                is_changed_after_unmarking_chemistry(mtd);
    925
    0
                            }
    926
                        }
    927
                    }
    928
    0
                    NOT_CHEMISTRY
    929
                },
    930
    0
                "semantics" => {
    931
    0
                    likely_chem_equation(get_presentation_element(mathml).1)
    932
                },
    933
    0
                _ => NOT_CHEMISTRY,
    934
            };
    935
    771
            if likely >= 0 {
    936
    164
                child.set_attribute_value(MAYBE_CHEMISTRY, &likely.to_string());
    937
    607
            }
    938
    771
            likelihood += likely;
    939
    771
            if likelihood < NOT_CHEMISTRY_THRESHOLD {
    940
    396
                return NOT_CHEMISTRY;
    941
    375
            }
    942
        }
    943
    944
    188
        if likelihood >= 0 {
    945
    108
            mathml.set_attribute_value(MAYBE_CHEMISTRY, &likelihood.to_string());
    946
    108
        }80
    947
    188
        return likelihood;
    948
    713
    }
    949
    950
    951
    /// could be a number, a state ("(l)", "(g)", etc), or a number followed by a state
    952
    1.19k
    fn likely_chem_subscript(subscript: Element) -> i32 {
    953
    1.19k
        let subscript_name = name(subscript);
    954
    1.19k
        if  subscript_name == "mn" && !as_text(subscript).contains('.')676 {
    955
    674
            return 0;       // not really much chem info about an integer subscript
    956
    525
        } else if subscript_name == "mi" {
    957
    328
            let text = as_text(subscript);
    958
    328
            if text == "s" || text == "l"323 ||text == "g"323 ||text == "aq"323 {
    959
    6
                subscript.set_attribute_value(CHEM_STATE, "true");
    960
    6
                return 2;
    961
    322
            }
    962
    197
        } else if subscript_name == "mrow" {
    963
            // debug!("likely_chem_subscript:\n{}", mml_to_string(subscript));
    964
    184
            let children = subscript.children();
    965
    184
            if children.len() == 3 && IsBracketed::is_bracketed71(subscript71, "("71, ")"71, false, true) {
    966
    6
                return likely_chem_subscript(as_element(children[1]));
    967
    178
            }
    968
    178
            let i_first_child = as_element(children[0]);
    969
    178
            if children.len() == 2 &&
    970
    103
               name(i_first_child) == "mn" && !as_text(i_first_child).contains('.')81 &&
    971
    81
               name(as_element(children[1])) == "mrow" &&
    972
    0
               likely_chem_state(as_element(children[1])) > 0 { // notation used in en.wikipedia.org/wiki/Electrolyte#Formation
    973
    0
                    return 2;
    974
    178
            }
    975
    13
        }
    976
        // could be a variable 'n' or something else -- just not likely
    977
    513
        return -3
    978
    1.19k
    }
    979
    980
    17
    fn small_roman_to_number(text: &str) -> &str {
    981
        // simplest to do a look up
    982
        static ROMAN_TO_NUMBER: phf::Map<&str, &str> = phf_map! {
    983
            "I" => "1", "II" => "2", "III" => "3", "IV" => "4", "V" => "5", "VI" => "6", "VII" => "7", "VIII" => "8", "IX" => "9",
    984
        };
    985
    17
        return ROMAN_TO_NUMBER.get(text).unwrap_or(&"");
    986
    987
    17
    }
    988
    989
    1.65k
    fn likely_chem_superscript(sup: Element) -> i32 {
    990
        // either one or more '+'s (or '-'s) or a number followed by +/-
    991
        // also could be state (en.wikipedia.org/wiki/Nuclear_chemistry#PUREX_chemistry)
    992
        // bullet is radical (en.wikipedia.org/wiki/Radical_(chemistry)#Depiction_in_chemical_reactions); mhchem uses dot operator
    993
        //  these can stand alone, be followed by +/- or have a number in front "(2•)-"" [examples from mhchem documentation]
    994
        // roman numerals are "oxidation state" and range from -4 to +9
    995
    3
        static MULTIPLE_PLUS_OR_MINUS_OR_DOT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\++$|^-+$|^\U{2212}+$|^[⋅∙•][-+\U{2212}]*$").unwrap());
    996
    3
        static SINGLE_PLUS_OR_MINUS_OR_DOT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[+-\U{2212}⋅∙•]$").unwrap());
    997
        static DOTS: &[char; 3] = &['⋅', '∙', '•'];
    998
    1.65k
        let sup_name = name(sup);
    999
    1.65k
        if sup_name == "mo" && MULTIPLE_PLUS_OR_MINUS_OR_DOT226.is_match226(as_text(sup)) {
    1000
    113
            if as_text(sup).find(DOTS).is_some() {
    1001
    7
                sup.set_attribute_value(MAYBE_CHEMISTRY, "1");
    1002
    7
                sup.set_attribute_value(CHEM_FORMULA_OPERATOR, "1");   // value doesn't really matter
    1003
    106
            }
    1004
    113
            return if as_text(sup).len()==1 {198} else {215};
    1005
    1.54k
        } else if (sup_name == "mi" || sup_name == "mn"1.36k || sup_name=="mtext"548) && SMALL_UPPER_ROMAN_NUMERAL1.00k.is_match1.00k(as_text(sup)){
    1006
    17
            sup.set_attribute_value("data-number", small_roman_to_number(as_text(sup)));
    1007
    17
            sup.set_attribute_value(MAYBE_CHEMISTRY, "2");
    1008
    17
            return 2;
    1009
    1.52k
        } else if sup_name == "mrow" {
    1010
            // look for something like '2+'
    1011
    311
            let children = sup.children();
    1012
    311
            if children.len() == 2 {
    1013
    177
                let first = as_element(children[0]);
    1014
    177
                let second = as_element(children[1]);
    1015
    177
                if name(first) == "mn" && name(second) == "mo"79 && !as_text(first).contains('.')55 {
    1016
    55
                    let second_text = as_text(second);
    1017
    55
                    if SINGLE_PLUS_OR_MINUS_OR_DOT.is_match(second_text) {
    1018
    55
                        if second_text.find(DOTS).is_some() {
    1019
    0
                            second.set_attribute_value(MAYBE_CHEMISTRY, "2");
    1020
    0
                            second.set_attribute_value(CHEM_FORMULA_OPERATOR, "2");   // value doesn't really matter
    1021
    55
                        }
    1022
    55
                        sup.set_attribute_value(MAYBE_CHEMISTRY, "3");
    1023
    55
                        return 3;   // ending with a +/- makes it likely this is an ion
    1024
    0
                    }
    1025
    122
                }
    1026
    134
            }
    1027
            // gather up the text and see if it is all +, -, etc
    1028
    256
            let mut text = "".to_string();
    1029
    414
            for child in &children256 {    // 'children' used later, so need to borrow rather than move
    1030
    414
                let child = as_element(*child);
    1031
    414
                if name(child) == "mo" {
    1032
    169
                    text.push_str(as_text(child));
    1033
    169
                } else {
    1034
                    // could have something like 'mrow(mrow 2n, -)  (chem example 5-9) -- so fallback to still ok if ends with + or -
    1035
    245
                    let last_super_child = as_element(children[children.len()-1]);
    1036
    245
                    if name(last_super_child) == "mo" {
    1037
    7
                        let text = as_text(last_super_child);
    1038
    7
                        if text == "+" || text == "-" {
    1039
    1
                            sup.set_attribute_value(MAYBE_CHEMISTRY, "3");
    1040
    1
                            return 3;
    1041
    6
                        }
    1042
    238
                    }
    1043
    244
                    return NOT_CHEMISTRY;
    1044
                }
    1045
            }
    1046
    11
            if MULTIPLE_PLUS_OR_MINUS_OR_DOT.is_match(&text) {
    1047
    13
                for child in children6 {
    1048
    13
                    let child = as_element(child);
    1049
    13
                    if name(child) == "mo" && as_text(child).find(DOTS).is_some() {
    1050
    0
                        child.set_attribute_value(MAYBE_CHEMISTRY, "1");
    1051
    0
                        child.set_attribute_value(CHEM_FORMULA_OPERATOR, "1");   // value doesn't really matter
    1052
    13
                    }
    1053
                }
    1054
    6
                let likely = 2*text.len() as i32;
    1055
    6
                sup.set_attribute_value(MAYBE_CHEMISTRY, &likely.to_string());
    1056
    6
                return likely;
    1057
    5
            }
    1058
    1.21k
        }
    1059
    1.21k
        return NOT_CHEMISTRY
    1060
    1.65k
    }
    1061
    1062
    1063
    /// chem_formula is likely if it is one of:
    1064
    /// * a (possibly adorned) chemical element
    1065
    /// * an operator that represents a bond
    1066
    /// * fences around a chemical formula
    1067
    /// * an mrow made up of only chemical formulas
    1068
    15.0k
    fn likely_chem_formula(mathml: Element) -> i32 {
    1069
        // debug!("start likely_chem_formula:\n{}", mml_to_string(mathml));
    1070
    15.0k
        if let Some(value2.65k) = get_marked_value(mathml) {
    1071
    2.65k
            return value;       // already marked
    1072
    12.3k
        }
    1073
    1074
    12.3k
        let tag_name = name(mathml);
    1075
    12.3k
        let likelihood = match tag_name {
    1076
            // a parent may clear the chem flags if something says can't be chemistry (e.g, a non chemically valid script)
    1077
    12.3k
            "mi" => likely_chem_element2.01k(mathml2.01k),
    1078
    10.3k
            "mo" => likely_chem_formula_operator4.48k(mathml4.48k),
    1079
    5.90k
            "mtext" => 044,    // definitely need to skip empty mtext, but others are probably neutral also
    1080
    5.85k
            "mn" => 01.98k,       // no info
    1081
    3.87k
            "msub" | "msup"3.76k | "msubsup"3.70k | "mmultiscripts"3.69k => {
    1082
    225
                likely_chem_formula(as_element(mathml.children()[0]));  // set MAYBE_CHEMISTRY attribute
    1083
    225
                likely_adorned_chem_formula(mathml)
    1084
            },
    1085
    3.64k
            "mrow" => {
    1086
    3.41k
                let chem_state = likely_chem_state(mathml);
    1087
    3.41k
                if chem_state > 0 {
    1088
    18
                    chem_state
    1089
                } else {
    1090
    3.39k
                    likely_mrow_chem_formula(mathml)
    1091
                }
    1092
            },
    1093
    232
            "mfrac" => {
    1094
    73
                let children = mathml.children();
    1095
    73
                let num_likely = likely_chem_formula(as_element(children[0]));
    1096
    73
                let denom_likely = likely_chem_formula(as_element(children[1]));
    1097
    73
                let likely = num_likely.max(denom_likely);
    1098
    73
                if likely < CHEMISTRY_THRESHOLD {NOT_CHEMISTRY} else {likely0}
    1099
            }
    1100
    159
            "mtd" => {
    1101
    5
                let mut likely = likely_chem_formula(as_element(mathml.children()[0]));
    1102
    5
                if likely < CHEMISTRY_THRESHOLD {
    1103
    4
                    likely = likely_chem_equation(mathml);
    1104
    4
                }1
    1105
    5
                likely
    1106
            }
    1107
    154
            "mtable" => {
    1108
    4
                for mrow in mathml2.children2() {
    1109
    4
                    let mrow = as_element(mrow);
    1110
    5
                    for mtd in mrow4.children4() {
    1111
    5
                        let mtd = as_element(mtd);
    1112
    5
                        let mut likely = likely_chem_formula(mtd);
    1113
    5
                        if likely < CHEMISTRY_THRESHOLD {
    1114
    3
                            likely = likely_chem_equation(mtd);
    1115
    3
                        }2
    1116
    5
                        if likely < CHEMISTRY_THRESHOLD {
    1117
    3
                            is_changed_after_unmarking_chemistry(mtd);
    1118
    3
                        }2
    1119
                    }
    1120
                }
    1121
    2
                NOT_CHEMISTRY
    1122
            },
    1123
    152
            "semantics" => {
    1124
    0
                likely_chem_formula(get_presentation_element(mathml).1)
    1125
            },
    1126
            _ => {
    1127
    152
                if !is_leaf(mathml) {
    1128
                    // mfrac, msqrt, etc
    1129
    320
                    for child in mathml152.children152() {
    1130
    320
                        let child = as_element(child);
    1131
    320
                        let likelihood = likely_chem_formula(child);
    1132
    320
                        if  likelihood > 0 {
    1133
    77
                            child.set_attribute_value(MAYBE_CHEMISTRY, likelihood.to_string().as_str());
    1134
    243
                        };
    1135
                    }
    1136
    0
                }
    1137
                // debug!("NOT_CHEMISTRY:\n{}", mml_to_string(mathml));
    1138
    152
                NOT_CHEMISTRY
    1139
            }
    1140
        };
    1141
    12.3k
        if likelihood >= 0 {
    1142
    5.09k
            mathml.set_attribute_value(MAYBE_CHEMISTRY, &likelihood.to_string());
    1143
    7.30k
        }
    1144
        // debug!("likely_chem_formula {}:\n{}", likelihood, mml_to_string(mathml));
    1145
    1146
    12.3k
        return likelihood;
    1147
    1148
    3.39k
        fn likely_mrow_chem_formula(mrow: Element) -> i32 {
    1149
            // For parens, the only reason to add them is to group the children and then indicate that there is more than one molecule
    1150
    3.39k
            if IsBracketed::is_bracketed(mrow, "(", ")", false, false) ||
    1151
    3.14k
               IsBracketed::is_bracketed(mrow, "[", "]", false, false) {
    1152
                // If it is bracketed, it should have a subscript to indicate the number of the element.
    1153
                // We give a pass to unadorned bracketing chars
    1154
    310
                if mrow.children().len() != 3 {
    1155
    0
                    return NOT_CHEMISTRY;
    1156
    310
                }
    1157
    310
                let contents = as_element(mrow.children()[1]);
    1158
    310
                let parent = get_parent(mrow);
    1159
    310
                let parent_is_scripted = IsNode::is_scripted(parent);
    1160
    310
                if name(contents) != "mrow" && !parent_is_scripted82 {
    1161
    53
                    return NOT_CHEMISTRY;
    1162
    257
                }
    1163
    257
                let likely = likely_chem_formula(contents);
    1164
    257
                if parent_is_scripted {
    1165
    149
                    return likely + 3;
    1166
                } else {
    1167
    108
                    return likely;
    1168
                }
    1169
    3.08k
            }
    1170
    1171
    3.08k
            let mut likelihood = if is_order_ok(mrow) {0832} else {-42.25k};
    1172
    1173
            // check all the children and compute the likelihood of that this is a chemical formula
    1174
            // bonus point for consecutive chemical formula children (not counting invisible children)
    1175
    3.08k
            let mut last_was_likely_formula = 0;        // 0 is false, 1 is true
    1176
    3.08k
            let mut is_chem_formula = true;              // assume true until we prove otherwise (still want to mark the children)
    1177
    12.5k
            for child in mrow3.08k.children3.08k() {
    1178
    12.5k
                let child = as_element(child);
    1179
    12.5k
                let likely = likely_chem_formula(child);
    1180
                // debug!("   in mrow: likely={}, likelihood={}", likely, likelihood);
    1181
    12.5k
                match likely.cmp(&0) {
    1182
                    Ordering::Greater => {
    1183
    2.56k
                        likelihood += likely + last_was_likely_formula;
    1184
    2.56k
                        last_was_likely_formula = if name(child) == "mo" {0279} else {12.28k};
    1185
                    },
    1186
    5.86k
                    Ordering::Less => {
    1187
    5.86k
                        // debug!("in likely_chem_formula: FALSE: likelihood={}, child\n{}", likelihood, mml_to_string(child));
    1188
    5.86k
                        is_chem_formula = false;
    1189
    5.86k
                        last_was_likely_formula = 0;
    1190
    5.86k
                        likelihood += likely;
    1191
    5.86k
                    },
    1192
                    Ordering::Equal => {
    1193
    4.08k
                        if name(child) == "mo" {
    1194
    2.27k
                            let text = as_text(child);
    1195
    2.27k
                            if text != "\u{2062}" && text != "\u{2063}"466 {   // one of these, we don't change the status
    1196
    8
                                last_was_likely_formula = 0;
    1197
    2.26k
                            }
    1198
    1.81k
                        }
    1199
                    },
    1200
                }
    1201
                // debug!("in likely_chem_formula likelihood={}, child\n{}", likelihood, mml_to_string(child));
    1202
                // debug!("   likelihood={} (likely={})", likelihood, likely);
    1203
            }
    1204
    1205
    3.08k
            if !is_chem_formula || likelihood <= NOT_CHEMISTRY832 {
    1206
                // the children may have looked have looked right, but something has said "not likely"
    1207
    2.25k
                return NOT_CHEMISTRY;
    1208
    832
            } else if likelihood < CHEMISTRY_THRESHOLD && is_short_formula387(mrow387) {
    1209
                        // debug!("is_short_formula is true for:\n{}", mml_to_string(mrow));
    1210
    47
                        return CHEMISTRY_THRESHOLD
    1211
    785
            }
    1212
    785
            return likelihood;
    1213
    3.39k
        }
    1214
    1215
    15.0k
    }
    1216
    1217
    /// This does some checks that sort of follow IUPAC's "Red Book" in section IR-4.4.
    1218
    /// Those rules require knowledge that the program doesn't have (e.g., which bond is closest to the central atom).
    1219
    /// Instead, we mainly use the two main types of orderings: alphabetical and electronegativity.
    1220
    /// We first do a test to see if this looks like a structural formula -- if so, ordering doesn't apply.
    1221
    /// If a formula has groupings, each grouping is checked independently of the rest since
    1222
    ///   there are cases where the outer ordering doesn't match the inner ordering.
    1223
    /// For "generalized salts", we need to split the elements into positive and negative ions, and within each group
    1224
    ///   the order is suppose to be alphabetical but many use electronegativity (the point being there are two separate groups).
    1225
    /// This site has a nice summary of the rules: https://chemistry.stackexchange.com/questions/537/why-is-arsenous-acid-denoted-h3aso3/538#538
    1226
    /// Note: "(OH)" doesn't fit with the above, and Susan Jolly suggests allowing any sequence that ends with H, so we allow that.
    1227
    /// Also, Susan Jolly suggested allowing any compound with C, H, and O
    1228
    3.08k
    fn is_order_ok(mrow: Element) -> bool {
    1229
    3.08k
        assert_eq!(name(mrow), "mrow");
    1230
    3.08k
        if let Some(elements2.32k) = collect_elements(mrow) {
    1231
    2.73k
            if elements.iter()2.32k.any2.32k(|&e| !CHEMICAL_ELEMENT_ELECTRONEGATIVITY.contains_key(e)) {
    1232
    1.48k
                return false;
    1233
    846
            }
    1234
    846
            let n_elements = elements.len();
    1235
    846
            if n_elements < 2 {
    1236
    475
                return true;
    1237
    371
            } else if has_noble_element(&elements) {
    1238
    0
                return false;    // noble elements don't form compounds
    1239
            } else {
    1240
    371
                return elements[n_elements-1] == "H"   ||        // special case that includes "OH"
    1241
                        // has_non_metal_element(&elements) && !has_non_metal_element(&elements) &&    // must have a metal and non-metal
    1242
    295
                        has_c_h_o(&elements) ||
    1243
    291
                        is_structural(&elements) ||
    1244
    271
                        is_alphabetical(&elements) ||
    1245
    169
                        is_ordered_by_electronegativity(&elements) ||
    1246
    12
                        is_generalized_salt(&elements);
    1247
            }
    1248
        } else {
    1249
    759
            return false;
    1250
        }
    1251
    3.08k
    }
    1252
    1253
    // from https://learnwithdrscott.com/ionic-bond-definition/
    1254
    // I don't include the noble gases since they don't interact with other elements and are ruled out elsewhere
    1255
    // fn has_non_metal_element(elements: &[&str]) -> bool {
    1256
    //     static NON_METAL_ELEMENTS: phf::Set<&str> = phf_set! {
    1257
    //         "H", "B", "C", "N", "O", "F", "Si", "P", "S", "Cl", "As", "Se", "Br", "Te", "I", "At",
    1258
    //     };
    1259
    //     return elements.iter().any(|&e| NON_METAL_ELEMENTS.contains(e));
    1260
    // }
    1261
    1262
    1263
    374
    fn has_noble_element(elements: &[&str]) -> bool {
    1264
        static NOBLE_ELEMENTS: phf::Set<&str> = phf_set! {
    1265
            "He", "Ne", "Ar", "Kr", "Xe", "Rn", "Og" // Og might be reactive, but it is unstable
    1266
        };
    1267
    893
        return elements.iter()374.any374(|&e| NOBLE_ELEMENTS.contains(e));
    1268
    374
    }
    1269
    1270
    295
    fn has_c_h_o(elements: &[&str]) -> bool {
    1271
    295
        return elements.contains(&"C") && elements39.contains39(&"H"39) && elements8.contains8(&"O"8);
    1272
    295
    }
    1273
    1274
    1275
    295
    fn is_structural(elements: &[&str]) -> bool {
    1276
    295
        assert!(elements.len() > 1);   // already handled
    1277
    1278
        // debug!("is_structural: {:?}", elements);
    1279
    295
        let mut element_set = HashSet::with_capacity(elements.len());
    1280
    627
        elements295.iter295().for_each295(|&e| {element_set.insert(e);});
    1281
    295
        return element_set.len() < elements.len();
    1282
    295
    }
    1283
    1284
    /// collect up all the elements in the mrow.
    1285
    ///  Returns the elements (which can be an empty vector) or None if something (right now an operator) rules out them being elements
    1286
    3.10k
    fn collect_elements(mrow: Element<'_>) -> Option<Vec<&str>> {
    1287
    3.10k
        let mut elements = Vec::with_capacity(mrow.children().len()/2+1);       // don't bother with slots for operators
    1288
    8.86k
        for child in mrow3.10k.children3.10k() {
    1289
    8.86k
            let child = as_element(child);
    1290
    8.86k
            match name(child) {
    1291
    8.86k
                "mi" | "mtext"6.18k => elements2.80k.push2.80k(as_text2.80k(child2.80k)),
    1292
    6.06k
                "msub" | "msup"5.73k | "mmultiscripts"5.65k => {
    1293
    584
                    let base = as_element(child.children()[0]);
    1294
    584
                    let base_name = name(base);
    1295
    584
                    if base_name == "mi" || base_name == "mtext"115 {
    1296
    514
                        elements.push(as_text(base));
    1297
    514
                    }70   // else skip and let recursive likely_chem_formula call check the contents
    1298
                },
    1299
    5.48k
                "mo" if likely_chem_formula_operator3.22k(child3.22k) < 0759 => return None759,
    1300
    2.46k
                "mo" => (),
    1301
    2.25k
                _ => (),    // let loop in likely_chem_formula() deal with all the negatives
    1302
            }
    1303
        }
    1304
    2.34k
        return Some(elements);
    1305
    3.10k
    }
    1306
    1307
    /// check to make sure elements are ordered alphabetically
    1308
    /// Actually check Hill's system that puts 'C' followed by 'H' first if 'C' is present
    1309
    275
    fn is_alphabetical(elements: &[&str]) -> bool {
    1310
    275
        assert!(elements.len() > 1);   // already handled
    1311
        // debug!("is_alphabetical: {:?}", elements);
    1312
    275
        let mut elements = elements;
    1313
    275
        if elements[1..].contains(&"C") {  // "C" must be first if present
    1314
    22
            return false;
    1315
    253
        }
    1316
    253
        if elements[0] == "C" {
    1317
    10
            elements = if elements[1]=="H" {&elements[2..]2} else {&elements[1..]8};
    1318
    243
        }
    1319
    253
        return elements.len() < 2 || elements.windows(2)243.all243(|pair| pair[0]251 < pair[1]251);
    1320
    275
    }
    1321
    1322
    174
    fn is_ordered_by_electronegativity(elements: &[&str]) -> bool {
    1323
        // HPO_4^2 (Mono-hydrogen phosphate) doesn't fit this pattern, nor does HCO_3^- (Hydrogen carbonate) and some others
    1324
        // FIX: drop "H" from the ordering??
    1325
    174
        assert!(elements.len() > 1);   // already handled
    1326
    188
        return elements.windows(2)174.all174(|pair| CHEMICAL_ELEMENT_ELECTRONEGATIVITY.get(pair[0]).unwrap() < CHEMICAL_ELEMENT_ELECTRONEGATIVITY.get(pair[1]).unwrap());
    1327
    174
    }
    1328
    1329
    12
    fn is_generalized_salt(elements: &[&str]) -> bool {
    1330
    12
        assert!(!elements.is_empty());
    1331
    12
        return false;
    1332
    12
    }
    1333
    1334
    1335
    /// Returns the likelihood that the arg is an adorned chem formula
    1336
    /// Adornments are:
    1337
    ///   superscripts with +/- and optionally a number (charge)
    1338
    ///  numeric subscripts (e.g. H_2)
    1339
    /// In addition to chemical elements, we include nuclear decay since there is a lot of overlap in notation
    1340
    /// The nuclear decay notation is mostly taken from https://tinyurl.com/2f6b8e3a
    1341
    /// Basically it is a chemical element or 'e', 'p', 'n', 'α', 'β', or 'γ' with pre-sub/superscript
    1342
    /// There is also an instance with a charge on the referenced page, so we allow that also.
    1343
    ///
    1344
    /// Note: https://tinyurl.com/ysmr8cw2 says "++"/"--", etc., is sometimes used in a superscript particle physics instead of a "2"
    1345
    ///
    1346
    /// Note:  msubsup cleaning for an empty script hasn't happened and we consider an empty script a sign of attempting to vertically align sub/superscripts
    1347
    ///
    1348
    /// Note: 'mathml' is not necessarily canonicalized
    1349
    2.85k
    pub fn likely_adorned_chem_formula(mathml: Element) -> i32 {
    1350
    2.85k
        if !matches!2.85k(name(mathml), "msub" | "msup"1.94k | "msubsup"546 | "mmultiscripts"352 | "mover"1) {
    1351
    1
            return NOT_CHEMISTRY;
    1352
    2.85k
        }
    1353
        // some simple sanity checks on the scripts...
    1354
    2.85k
        let tag_name = name(mathml);
    1355
    2.85k
        let children = mathml.children();
    1356
    2.85k
        let mut likelihood = 0;
    1357
    2.85k
        let mut is_empty_subscript = false;
    1358
        // debug!("likely_adorned_chem_formula:\n{}", mml_to_string(mathml));
    1359
    2.85k
        if tag_name == "msub" || tag_name == "msubsup"1.94k {
    1360
            // subscripts should be just a number, although they could be 'n' or '2n' or other exprs.
    1361
    1.10k
            let subscript = as_element(children[1]);
    1362
    1.10k
            is_empty_subscript = name(subscript) == "mtext" && as_text(subscript).trim()3.is_empty3();
    1363
    1.10k
            if !is_empty_subscript {
    1364
    1.10k
                likelihood += likely_chem_subscript(subscript);
    1365
    1.10k
            }3
    1366
    1.74k
        }
    1367
    1368
    2.85k
        let mut empty_superscript = false;
    1369
    2.85k
        if tag_name == "msup" || tag_name == "msubsup"1.45k {
    1370
            // debug!("likely_adorned_chem_formula: mathml\n{}", mml_to_string(mathml));
    1371
    1.59k
            let superscript = as_element(children[if tag_name == "msup" {11.39k} else {2194}]);
    1372
    1.59k
            empty_superscript = name(superscript) == "mtext" && as_text(superscript).trim()13.is_empty13();
    1373
    1.59k
            if !empty_superscript {
    1374
    1.58k
                likelihood += likely_chem_superscript(superscript);
    1375
    1.58k
            }6
    1376
    1.26k
        }
    1377
    2.85k
        if tag_name == "msubsup" && (is_empty_subscript194 || empty_superscript191) {
    1378
    9
            likelihood += 1; // might be trying to vertically align scripts as in done in chemistry
    1379
    2.84k
        }
    1380
    1381
    2.85k
        if tag_name == "mmultiscripts" {
    1382
            // prescripts are normally positive integers, chem 2.5.1 allows for a superscript for a Lewis dot
    1383
            // postscript should be a charge
    1384
    1385
            let prescripts;
    1386
            let postscripts;
    1387
    351
            if children.len() == 4 && name138(as_element138(children[1]))=="mprescripts" { // just prescripts
    1388
    138
                prescripts = &children[2..4];
    1389
    138
                postscripts = &children[0..0]; // empty
    1390
    213
            } else if children.len() == 6 && name57(as_element57(children[3]))=="mprescripts" {  // pre and postscripts
    1391
    55
                prescripts = &children[4..6];
    1392
    55
                postscripts = &children[1..3]; // empty
    1393
    158
            } else if children.len() == 3 || children.len() == 568 {   // just postscripts (simultaneous or offset)
    1394
    118
                prescripts = &children[0..0]; // empty
    1395
    118
                postscripts = &children[1..];
    1396
    118
            } else {
    1397
    40
                return NOT_CHEMISTRY;
    1398
            };
    1399
    1400
    311
            if !prescripts.is_empty() {
    1401
    193
                let pre_subscript = as_element(prescripts[0]);
    1402
    193
                let pre_subscript_name = name(pre_subscript);
    1403
    1404
    193
                let pre_superscript = as_element(prescripts[1]);
    1405
    193
                let pre_superscript_name = name(pre_superscript);
    1406
    1407
                // deal with special case of 'e' with prescripts of -1 and 0
    1408
    193
                if is_adorned_electron(children[0], prescripts) {
    1409
    31
                    return 100;     // very likely chemistry
    1410
    162
                }
    1411
    162
                let base = as_element(children[0]);
    1412
    162
                let base_name = name(base);
    1413
    162
                let atomic_number127 = if matches!154(base_name, "mi" | "mtext"41) &&
    1414
    154
                                            let Some(atomic_number127) = CHEMICAL_ELEMENT_ATOMIC_NUMBER.get(as_text(base)) {
    1415
    127
                            *atomic_number
    1416
                        } else {
    1417
    35
                            return NOT_CHEMISTRY;
    1418
                        };
    1419
    127
                if pre_superscript_name == "mo" {
    1420
                    // Lewis dot prescript case
    1421
    3
                    if pre_subscript_name != "none" {
    1422
    0
                        return NOT_CHEMISTRY;
    1423
    3
                    }
    1424
    3
                    likelihood += likely_chem_superscript(pre_superscript);
    1425
    124
                } else if pre_superscript_name == "mn" { // must have a pre-superscript (neutrons + protons)
    1426
    75
                    if let Ok(mass) = as_text(pre_superscript).parse::<u32>() {
    1427
                        // "drip line" is 1.5 * mass < 3.5 * mass -- it is possible to outside of this range, but VERY unlikely
    1428
                        // to avoid floating point, we multiply by 2 and compare to 3 and 7
    1429
    75
                        if 3*atomic_number < 2*mass && 2*mass < 7*atomic_number74 {
    1430
    74
                            likelihood += 3;
    1431
    74
                        }1
    1432
    0
                    }
    1433
    75
                    if pre_subscript_name == "mn"  && as_text(pre_subscript)71 == atomic_number.to_string() {
    1434
    69
                            likelihood = CHEMISTRY_THRESHOLD;
    1435
    69
                    }6
    1436
                } else {
    1437
    49
                    return NOT_CHEMISTRY;
    1438
                }
    1439
    118
            }
    1440
    1441
    196
            if !postscripts.is_empty() {
    1442
    119
                let mut i = 0;
    1443
    266
                while i < postscripts.len() {
    1444
    147
                    let sub = as_element(postscripts[i]);
    1445
                    // debug!("sub: {}", mml_to_string(sub));
    1446
    147
                    if name(sub) != "none" {
    1447
    91
                        likelihood += likely_chem_subscript(sub);
    1448
    91
                    }56
    1449
    147
                    let sup = as_element(postscripts[i+1]);
    1450
    147
                    if name(sup) != "none" {
    1451
    65
                        // debug!("sup: {}", mml_to_string(sub));
    1452
    65
                        likelihood += likely_chem_superscript(sup);
    1453
    82
                    }
    1454
    147
                    i += 2;
    1455
                }
    1456
    77
            }
    1457
    2.50k
        }
    1458
    1459
    2.69k
        let base = as_element(children[0]);
    1460
    2.69k
        let base_name = name(base);
    1461
    2.69k
        if base_name == "mi" || base_name == "mtext"822 {
    1462
    2.05k
            likelihood += likely_chem_element(base);
    1463
    2.05k
        } else if base_name == "mrow"641 {
    1464
            // debug!("mrow addition:\n{}", mml_to_string(base));
    1465
            // a safe minor canonicalization that allows "short_form" calculations if appropriate
    1466
    187
            if (IsBracketed::is_bracketed(base, "(", ")", false, false) ||
    1467
    89
                IsBracketed::is_bracketed(base, "[", "]", false, false)) &&
    1468
    148
               base.children().len() > 3 {
    1469
    77
                let inner_mrow = create_mathml_element(&base.document(), "mrow");
    1470
    77
                inner_mrow.set_attribute_value(CHANGED_ATTR, ADDED_ATTR_VALUE);
    1471
    77
                let mut children = base.children();
    1472
    77
                let inside_of_parens = children.drain(1..children.len()-1);
    1473
    77
                inner_mrow.append_children(inside_of_parens);
    1474
    77
                base.replace_children(vec![children[0], ChildOfElement::Element(inner_mrow), children[children.len()-1]]);
    1475
    110
            }
    1476
    187
            likelihood += likely_chem_formula(base);
    1477
    454
        } else {
    1478
    454
            likelihood += likely_chem_formula(base);
    1479
    454
        }
    1480
    1481
        // debug!("returning from likely_adorned_chem_formula: likelihood={}, mathml\n{}", likelihood, mml_to_string(mathml));
    1482
    2.69k
        return likelihood;
    1483
    1484
    1485
    193
        fn is_adorned_electron(base: ChildOfElement, prescripts: &[ChildOfElement]) -> bool {
    1486
            // looking for 'e' with prescripts of -1 and 0
    1487
    193
            let base = as_element(base);
    1488
    193
            let pre_lower = as_element(prescripts[0]);
    1489
    193
            let pre_upper = as_element(prescripts[1]);
    1490
    193
            if (name(base) == "mi" || name(base) == "mtext"57) && as_text(base) == "e"185 &&
    1491
    31
               name(pre_upper) == "mn" && as_text(pre_upper) == "0" &&
    1492
    31
               name(pre_lower) == "mrow" && pre_lower.children().len() == 2 {
    1493
                // looking '-' and '1'
    1494
    31
                let lower_children = pre_lower.children();
    1495
    31
                let minus = as_element(lower_children[0]);
    1496
    31
                let one = as_element(lower_children[1]);
    1497
                // not yet normalized, so we need to compare against ASCII minus and u+2212
    1498
    31
                return name(minus) == "mo" && (as_text(minus) == "-" || as_text(minus) == "−") &&
    1499
    31
                       name(one) == "mn"   && as_text(one) == "1";
    1500
            } else {
    1501
    162
                return false;
    1502
            }
    1503
    193
        }
    1504
    2.85k
    }
    1505
    1506
    /// useful function to see if the str is a single char matching the predicate
    1507
    29.6k
    fn is_single_char_matching(leaf_text: &str, pred: impl Fn(char) -> bool) -> bool {
    1508
    29.6k
        let mut chars = leaf_text.chars();
    1509
    29.6k
        if let Some(ch) = chars.next() && chars.next().is_none() {
    1510
    29.5k
            return pred(ch);
    1511
    87
        }
    1512
    87
        return false;
    1513
    29.6k
    }
    1514
    1515
    17.2k
    fn likely_chem_formula_operator(mathml: Element) -> i32 {
    1516
        // mostly from chenzhijin.com/en/article/Useful%20Unicode%20for%20Chemists (Arrows and Other)
    1517
        // also en.wikipedia.org/wiki/Chemical_formula#Condensed_formula
    1518
        #[derive(PartialEq, Eq)]
    1519
        enum BondType {DoubleBond, TripleBond}      // options for is_legal_bond()
    1520
        // "⋅" is used in GTM 16.2 and en.wikipedia.org/wiki/Cement_chemist_notation -- may want to add some similar chars
    1521
        static CHEM_FORMULA_OPERATORS: phf::Set<&str> = phf_set! {
    1522
            "-", "\u{2212}", "⋅", ":", "=", "∷", "≡", ":::", "≣", "::::", // bond symbols (need both 2212 and minus because maybe not canonicalized)
    1523
            "⋮", // lewis dots, part of "⋮⋮" - triple bond (see Nemeth chem guide 2.5.4)
    1524
        };
    1525
    16.1k
        fn is_chem_formula_ok(ch: char) -> bool {
    1526
    16.1k
            matches!9.64k(ch, '(' | ')' | '[' | ']' | '\u{2062}' | '\u{2063}')
    1527
    16.1k
        }
    1528
    1529
    17.2k
        assert_eq!(name(mathml), "mo");
    1530
    17.2k
        let leaf_text = as_text(mathml);
    1531
    17.2k
        if CHEM_FORMULA_OPERATORS.contains(leaf_text) &&
    1532
    1.85k
           (has_inherited_property(mathml, "chemical-formula") ||
    1533
    1.85k
            ( !(leaf_text == "=" || leaf_text == "∷"1.02k ) || is_legal_bond848(mathml848, BondType::DoubleBond848) )  &&
    1534
    1.05k
            ( !(leaf_text == "≡" || leaf_text == ":::"1.03k ) || is_legal_bond26(mathml26, BondType::TripleBond26) )
    1535
           )  {
    1536
    1.04k
            mathml.set_attribute_value(MAYBE_CHEMISTRY, "1");
    1537
    1.04k
            mathml.set_attribute_value(CHEM_FORMULA_OPERATOR, "1");
    1538
    1.04k
            return 1;
    1539
    16.1k
        } else if is_single_char_matching(leaf_text, is_chem_formula_ok) {
    1540
    6.49k
            return 0;  // not much info
    1541
        } else {
    1542
    9.67k
            return -3; // still a small chance;
    1543
        }
    1544
    1545
    874
        fn is_legal_bond(mathml: Element, bond_type: BondType) -> bool {
    1546
    874
            let preceding = mathml.preceding_siblings();
    1547
    874
            let following = mathml.following_siblings();
    1548
    874
            if preceding.is_empty() || following783.is_empty783() {
    1549
    115
                return false;
    1550
    759
            }
    1551
    1552
    759
            let mut preceding_element = as_element(preceding[preceding.len()-1]);
    1553
            // special check for CH_2 -- double bond is really with C
    1554
    759
            if bond_type == BondType::DoubleBond && name(preceding_element) == "msub"734 &&
    1555
    31
               preceding.len() > 1 &&  &11convert_to_short_form11(preceding_element).unwrap_or_default() == "H_2" {
    1556
    2
                preceding_element = as_element(preceding[preceding.len()-2]);
    1557
    2
                if !is_leaf(preceding_element) || as_text(preceding_element) != "C" {
    1558
    0
                    return false;
    1559
    2
                }
    1560
    757
            } else if name(preceding_element) != "mi" && name(preceding_element) != "mtext"353 {
    1561
    320
                return false;
    1562
    437
            }
    1563
    439
            let following_element = get_possible_embellished_node(as_element(following[0]));
    1564
    439
            if name(following_element) != "mi" && name(following_element) != "mtext"315 {
    1565
    313
                return false;
    1566
    126
            }
    1567
    126
            let preceding_text = as_text(preceding_element);
    1568
    126
            let following_text = as_text(following_element);
    1569
    126
            return match bond_type {
    1570
    105
                BondType::DoubleBond => is_legal_double_bond(preceding_text, following_text),
    1571
    21
                BondType::TripleBond => is_legal_triple_bond(preceding_text, following_text),
    1572
            };
    1573
    1574
    105
            fn is_legal_double_bond(left: &str, right: &str) -> bool {
    1575
                // this is based on table in en.wikipedia.org/wiki/Double_bond#Types_of_double_bonds_between_atoms
    1576
                static DOUBLE_BOND_TO_SELF: phf::Set<&str> = phf_set! {
    1577
                    "C", "O", "N", "S", "Si", "Ge", "Sn", "Pb"
    1578
                };
    1579
                    // "C" => &["O", "N", "S"],
    1580
                    // "O" => &["N", "S"],
    1581
    105
                if left == right && DOUBLE_BOND_TO_SELF50.contains50(left50) {
    1582
    44
                    return true;
    1583
    61
                }
    1584
    61
                return match left {
    1585
    61
                    "C" => right=="O"3 || right=="N"2 || right=="S"2,
    1586
    58
                    "O" => right=="N"1 || right=="S"1,
    1587
    57
                    "Si" => right=="C"0,
    1588
    57
                    _ => false,
    1589
                }
    1590
    105
            }
    1591
    1592
    21
            fn is_legal_triple_bond(left: &str, right: &str) -> bool {
    1593
                // According to https://tinyurl.com/rkynhwj3 (from physics.org)
    1594
                // triple bonds can be formed between any of B, C, N, and O
    1595
                // Apparently they can also be forced in other cases, but they are rare.
    1596
                // 'B' is from studiousguy.com/triple-bond-examples/
    1597
    21
                return  (left == "B"  || left == "C"  || left == "N"5  || left == "O"5) &&
    1598
    18
                        (right == "B" || right == "C" || right == "N"5 || right == "O"5);
    1599
    21
            }
    1600
    874
        }
    1601
    17.2k
    }
    1602
    1603
    /// This assumes canonicalization of characters has happened
    1604
    6.85k
    fn likely_chem_equation_operator(mathml: Element) -> i32 {
    1605
    1606
    6.73k
        fn is_chem_equation_operator(ch: char) -> bool {
    1607
    6.73k
            matches!4.90k(ch, '+' | '=' | '-' | '·' | '℃' | '°' | '‡' | '∆' | '×' | '\u{2062}')
    1608
    6.73k
        }
    1609
    1610
    6.85k
        let elem_name = name(mathml);
    1611
    6.85k
        if elem_name == "munder" || elem_name == "mover"6.80k || elem_name == "munderover"6.78k {
    1612
    86
            let base = as_element(mathml.children()[0]);
    1613
    86
            if name(base) == "mo" && is_single_char_matching64(as_text(base)64, is_chem_equation_arrow) {
    1614
    1
                base.set_attribute_value(MAYBE_CHEMISTRY, "1");
    1615
    1
                base.set_attribute_value(CHEM_EQUATION_OPERATOR, "1");
    1616
    1
                return 1;
    1617
    85
            } else if elem_name == "mover" && is_hack_for_missing_arrows20(mathml20) {
    1618
    9
                return 2;
    1619
            } else {
    1620
    76
                return NOT_CHEMISTRY;
    1621
            }
    1622
    6.76k
        }
    1623
    1624
    6.76k
        if name(mathml) == "mo" {
    1625
    6.76k
            let text = as_text(mathml);
    1626
    6.76k
            if is_single_char_matching(text, is_chem_equation_operator) || is_single_char_matching4.93k(text4.93k, is_chem_equation_arrow) {
    1627
    1.96k
                mathml.set_attribute_value(MAYBE_CHEMISTRY, "1");
    1628
    1.96k
                mathml.set_attribute_value(CHEM_EQUATION_OPERATOR, "1");
    1629
    1.96k
                return 1;
    1630
    4.79k
            } else if text == "\u{2062}" || text == "\u{2063}" {
    1631
                // FIX: the invisible operator between elements should be well-defined, but this likely needs work, so both accepted for now
    1632
    0
                return 0;
    1633
    4.79k
            }
    1634
    0
        }
    1635
    4.79k
        return -3;  // there is still a chance
    1636
    1637
        /// Detects output of mhchem for some equilibrium arrows that currently (11/22) don't have Unicode points
    1638
        /// See github.com/NSoiffer/MathCAT/issues/60 for the patterns being matched
    1639
    20
        fn is_hack_for_missing_arrows(mover: Element) -> bool {
    1640
    20
            assert_eq!(name(mover), "mover");
    1641
    20
            let children = mover.children();
    1642
    20
            let base = as_element(children[0]);
    1643
    20
            let mo_base = if name(base) == "mrow" && base.children().len() == 212 {
    1644
    9
                as_element(base.children()[0])
    1645
            } else {
    1646
    11
                base
    1647
            };
    1648
    20
            let upper = as_element(children[1]);
    1649
    20
            let mo_upper = if name(upper) == "mrow" && upper.children().len() == 29 {
    1650
    9
                as_element(upper.children()[1])
    1651
            } else {
    1652
    11
                upper
    1653
            };
    1654
            // slightly sloppy match, but almost certainly good enough
    1655
    20
            return name(mo_base) == "mo" && name(mo_upper) == "mo"9 &&
    1656
    9
                    as_text(mo_base) == "↽" && as_text(mo_upper) == "⇀";
    1657
    20
            }
    1658
    6.85k
    }
    1659
    1660
    38
    fn is_equilibrium_constant(mut mathml: Element) -> bool {
    1661
    38
        if name(mathml) == "msub" {
    1662
    27
            mathml = as_element(mathml.children()[0]);
    1663
    27
        }11
    1664
    1665
    38
        return name(mathml) == "mi" && as_text(mathml) == "K"25;
    1666
    38
    }
    1667
    1668
    // Oxidation states range from -4 to 9 and are written with (a subset of) roman numerals.
    1669
    // All instances seem to be upper case that I've seen.
    1670
    3
    static SMALL_UPPER_ROMAN_NUMERAL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*^(IX|IV|V?I{0,3})\s*$").unwrap());
    1671
    1672
    /// look for "(s), "(l)", "(g)", "(aq)" (could also use [...])
    1673
    /// this might be called before canonicalization, but in clean_chemistry_mrow, we made sure "( xxx )" is grouped properly
    1674
    3.68k
    pub fn likely_chem_state(mathml: Element) -> i32 {
    1675
    1676
    3.68k
        if IsBracketed::is_bracketed(mathml, "(", ")", false, false) ||
    1677
    3.30k
           IsBracketed::is_bracketed(mathml, "[", "]", false, false) {
    1678
    438
            let contents = as_element(mathml.children()[1]);
    1679
    438
            let contents_name = name(contents);
    1680
    438
            if contents_name == "mi" || contents_name == "mtext"331 {
    1681
    109
                let text = as_text(contents);
    1682
    109
                if text == "s" || text == "l"102 ||text == "g"102 ||text == "aq"68 {
    1683
    67
                    return text.len() as i32 + 1;       // hack to count chars -- works because all are ASCII
    1684
    42
                };
    1685
    329
            }
    1686
    3.24k
         }
    1687
    3.61k
         return NOT_CHEMISTRY;
    1688
    3.68k
    }
    1689
    1690
    /// Returns the likelihood that the arg is an element
    1691
    16.4k
    pub fn likely_chem_element(mathml: Element) -> i32 {
    1692
        static NUCLEAR_SYMBOLS: [&str; 6] = ["e", "p", "n", "α", "β","γ"];
    1693
    1694
    16.4k
        assert!(name(mathml) == "mi" || name(mathml) == "mtext"1.11k, "{} is not 'mi' or 'mtext'", name0(mathml0));
    1695
    16.4k
        let text = as_text(mathml);
    1696
    16.4k
        if as_text(mathml).trim().is_empty() {
    1697
    782
            return 0;   // whitespace
    1698
    15.6k
        } else if is_chemical_element(mathml) {
    1699
            // single letter = 1; single letter with mathvariant="normal" = 2; double = 3 -- all elements are ASCII
    1700
    2.21k
            return if text.len() == 1 {
    1701
    1.90k
                if mathml.attribute_value("mathvariant").unwrap_or_default() == "normal" {2491} else {11.41k}
    1702
            } else {
    1703
    311
                3
    1704
            };
    1705
    13.4k
        } else if NUCLEAR_SYMBOLS.contains(&text) {
    1706
    659
            return 0;
    1707
            // not much special about them;
    1708
        } else {
    1709
    12.7k
            return NOT_CHEMISTRY;
    1710
        }
    1711
    16.4k
    }
    1712
    1713
    static SHORT_SINGLE_LETTER_ELEMENT_FORMULAE: phf::Set<&str> = phf_set! {
    1714
        // from en.wikipedia.org/wiki/Glossary_of_chemical_formulae (via chem_formula_from_wikipedia.py)
    1715
        "BF_3", "BI_3", "BN", "BP", "B_2F_4", "B_2H_6", "B_2O_3", "B_2S_3", "B_4C",
    1716
        "CB_4", "CF_4", "CH_2", "CH_4", "CO", "CO_2", "CO_3", "CS_2", "CW", "C_2F_4",
    1717
        "C_2H_4", "C_2H_6", "C_2U", "C_2Y", "C_3H_4", "C_3H_6", "C_3H_8", "C_4H_2",
    1718
        "C_4H_8", "C_4I_2", "C_6H_6", "C_6N_4", "C_7H_8", "C_8H_8", "DI", "D_2O",
    1719
        "FI", "FI_2", "FK", "FN", "FO", "FO_2", "FP", "FS", "FW", "FY", "F_2",
    1720
        "F_2N", "F_2O", "F_2O_2", "F_2P", "F_2S", "F_2S_2", "F_2W", "F_2Y", "F_3B",
    1721
        "F_3P", "F_3S", "F_3W", "F_3Y", "F_4B_2", "F_4C", "F_4C_2", "F_4N_2",
    1722
        "F_4S", "F_4U", "F_4W", "F_5I", "F_5P", "F_5S", "F_5U", "F_5W", "F_6S",
    1723
        "F_6W", "F_7I", "HF", "HI", "HK", "HN_3", "H_2", "H_2C", "H_2C_2", "H_2C_4",
    1724
        "H_2O", "H_2O_2", "H_2S", "H_3N", "H_3P", "H_4C", "H_4C_2", "H_4C_3",
    1725
        "H_4N_2", "H_4N_4", "H_6B_2", "H_6C_2", "H_6C_3", "H_6C_6", "H_8C_3",
    1726
        "H_8C_7", "H_8C_8", "ID", "IF", "IF_5", "IF_7", "IH", "IK", "IO_3", "I_2",
    1727
        "I_2F", "I_2O_5", "I_2W", "I_3B", "I_3N", "I_3U", "I_3V", "I_4P_2", "I_4W",
    1728
        "KH", "KI", "K_2F_2", "K_2O", "K_2O_2", "K_2S", "NB", "NF", "NF_2", "NF_3",
    1729
        "NI_3", "NO", "NO_2", "NU", "NV", "N_2", "N_2F_4", "N_2H_2", "N_2H_4",
    1730
        "N_2O_3", "N_2O_4", "N_2O_5", "N_3H", "N_4C_6", "N_4H_4", "N_5P_3", "O",
    1731
        "OD_2", "OF", "OF_2", "OH_2", "OK_2", "ON", "ON_2", "OT_2", "O_2", "O_2C",
    1732
        "O_2F_2", "O_2H_2", "O_2K_2", "O_2N", "O_2S", "O_2U", "O_2W", "O_3",
    1733
        "O_3C", "O_3I", "O_3N_2", "O_3S", "O_3U", "O_3V_2", "O_3W", "O_3Y_2",
    1734
        "O_5I_2", "O_5N_2", "O_5P_2", "O_5V_2", "O_8U_3", "PB", "PF", "PF_2", "PF_3",
    1735
        "PH_3", "PY", "P_2F_4", "P_2I_4", "P_2O_5", "P_2S_3", "P_3N_5", "SF", "SF_2",
    1736
        "SF_4", "SF_5", "SF_6", "SH_2", "SK_2", "SO_2", "SO_3", "S_2C", "S_2F_2",
    1737
        "S_2W", "S_3B_2", "S_3P_2", "S_3W", "S_3Y_2", "T_2O", "UC_2", "UF_4", "UF_5",
    1738
        "UI_3", "UN", "UO_2", "UO_3", "US_2", "U_3O_8", "VI_3", "VN", "V_2O_3",
    1739
        "WC", "WF", "WF_2", "WF_3", "WF_4", "WF_5", "WF_6", "WI_2", "WI_4", "WO_2",
    1740
        "WS_2", "WS_3", "YB_6", "YC_2", "YF", "YF_2", "YF_3", "YP", "Y_2O_3",
    1741
    1742
        // from en.wikipedia.org/wiki/Ion#Common_ions (via chem_formula_from_wikipedia.py)
    1743
        "CH_3COO^−", "CN^−", "CO_3^2−", "C^−", "C_2O_4^2−", "F^−", "HCOO^−",
    1744
        "HPO_4^2−", "HSO_3^−", "HSO_4^−", "H^+", "H^−", "H_2PO_4^−", "H_3O^+", "I^−",
    1745
        "NH_4^+", "NO_2^−", "NO_3^−", "N^3−", "N_3^−", "OH^−", "O^2−", "O_2^2−",
    1746
        "PO_4^3−", "P^3−", "SO_3^2−", "SO_4^2−", "S^2−", "S_2O_3^2−",
    1747
    1748
        // from gchem.cm.utexas.edu/canvas.php?target=bonding/ionic/polyatomic-ions.html
    1749
        "PO_3^3−", "IO_3^−",
    1750
    1751
        // others
    1752
        "CH_3", /* methyl */
    1753
        "NH_3",  // ammonium
    1754
    };
    1755
    1756
    /// Returns true if the formula is composed of 1 or 2 single letter elements and it matches a known compound/ion
    1757
    /// This might be called (via likely_adorned_chem_formula) unparsed
    1758
    387
    fn is_short_formula(mrow: Element) -> bool {
    1759
    387
        assert_eq!(name(mrow), "mrow");
    1760
    387
        let children = mrow.children();
    1761
    387
        let n_children = children.len();
    1762
    387
        if n_children == 0 || n_children > 3 || (n_children == 3378 && name317(as_element317(children[1])) != "mo") {
    1763
    12
            return false;
    1764
    375
        }
    1765
    1766
    375
        let first_element = convert_to_short_form( as_element(children[0]) );
    1767
    375
        if n_children == 1 {
    1768
    2
            return first_element.is_ok();
    1769
    373
        }
    1770
    373
        let second_element = convert_to_short_form( as_element(children[if n_children == 2 {159} else {2314}]) );
    1771
    373
        return match (first_element, second_element) {
    1772
    365
            (Ok(first), Ok(second)) => {
    1773
    365
                let short_form = first + second.as_str();
    1774
                // debug!("short_form: {}", short_form);
    1775
    365
                return SHORT_SINGLE_LETTER_ELEMENT_FORMULAE.contains(&short_form);
    1776
            },
    1777
    8
            _ => false,
    1778
        }
    1779
    387
    }
    1780
    1781
    931
    fn convert_to_short_form(mathml: Element) -> Result<String> {
    1782
    931
        let mathml_name = name(mathml);
    1783
    931
        return match mathml_name {
    1784
    931
            "mi" | "mtext"441 | "mn"393 | "mo"104 => Ok( as_text(mathml).to_string() )836,
    1785
    95
            "none" => Ok( "".to_string() )0,
    1786
    95
            "msub" | "msup"16 | "msubsup"13 | "mmultiscripts"13=> {
    1787
    86
                let is_mmultiscripts = mathml_name == "mmultiscripts";
    1788
    86
                let children = mathml.children();
    1789
    86
                let mut result = convert_to_short_form(as_element(children[0]))?0;
    1790
    86
                if is_mmultiscripts && children.len() != 34 {
    1791
    0
                    bail!("mmultiscripts found with {} children -- not part of chemical formula", children.len());
    1792
    86
                }
    1793
    86
                if mathml_name == "msub" || mathml_name == "msubsup"7 || (is_mmultiscripts7 && name4(as_element4(children[1])) != "none") {
    1794
    83
                    result += "_";
    1795
    83
                    result += &convert_to_short_form(as_element(children[1]))?1;
    1796
    3
                }
    1797
    85
                if mathml_name == "msup" || mathml_name == "msubsup"82 || (is_mmultiscripts82 && name4(as_element4(children[2])) != "none") {
    1798
    3
                    result += "^";
    1799
    3
                    result += &convert_to_short_form(as_element(children[if mathml_name=="msup" {1} else {20}]))?0;
    1800
    82
                }
    1801
    85
                Ok( result )
    1802
            },
    1803
    9
            "mrow" => {
    1804
                // the only time this is valid is if the superscript is something like "+" or "2+", so we do a few checks and short circuit false now
    1805
    9
                let mrow_children = mathml.children();
    1806
    9
                if mrow_children.len() == 1 || mrow_children.len() == 2 {
    1807
    0
                    let mut result = convert_to_short_form(as_element(mrow_children[0]))?;
    1808
    0
                    if mrow_children.len() == 2 {
    1809
    0
                        result += &convert_to_short_form(as_element(mrow_children[1]))?;
    1810
    0
                    }
    1811
    0
                    return Ok(result)
    1812
                } else {
    1813
    9
                    bail!("mrow found with {} children -- not part of chemical formula", mrow_children.len());
    1814
                }
    1815
            }
    1816
    0
            _ => bail!("{} found -- not part of chemical formula", mathml_name),
    1817
        }
    1818
    931
    }
    1819
    1820
    /// A map of chemical elements and their relative IUPAC electronegativity (https://i.stack.imgur.com/VCSzW.png)
    1821
    /// That list uses a horizontal line for the Lanthanide and Actinide Series.
    1822
    /// Because I had already ordered the elements before realizing that, I opened a gap and started the higher ones again with a '1' in front.
    1823
    /// The list is missing recent (unstable) elements -- I added them with the same value as the element above them in the periodic table.
    1824
    static CHEMICAL_ELEMENT_ELECTRONEGATIVITY: phf::Map<&str, u32> = phf_map! {
    1825
      "Ac" => 40, "Ag" => 155, "Al" => 163, "Am" => 29, "Ar" => 4, "As" => 172, "At" => 181, "Au" => 154,
    1826
        "B" => 164, "Ba" => 14, "Be" => 18, "Bh" => 137, "Bi" => 170, "Bk" => 27, "Br" => 183,
    1827
      "C" => 169, "Ca" => 16, "Cd" => 158, "Ce" => 56, "Cf" => 26, "Cl" => 184, "Cm" => 28, "Cn" => 157, "Co" => 148, "Cr" => 136, "Cs" => 8, "Cu" => 156,
    1828
        "Db" => 129, "Ds" => 149, "Dy" => 48,
    1829
      "Er" => 46, "Es" => 25, "Eu" => 51, "F" => 185, "Fe" => 144, "Fl" => 165, "Fm" => 24, "Fr" => 7, "Ga" => 162, "Gd" => 50, "Ge" => 167,
    1830
      "H" => 175, "He" => 6, "Hf" => 126, "Hg" => 157, "Ho" => 47, "Hs" => 141, "I" => 182, "In" => 161, "Ir" => 146, "K" => 10, "Kr" => 3,
    1831
      "La" => 62, "Li" => 12, "Lr" => 19, "Lu" => 41, "Lv" => 176, "Mc" => 170, "Md" => 23, "Mg" => 17, "Mn" => 140, "Mo" => 135, "Mt" => 145,
    1832
      "N" => 174, "Na" => 11, "Nb" => 131, "Nd" => 54, "Ne" => 5, "Nh" => 160, "Ni" => 152, "No" => 22, "Np" => 31, "O" => 180, "Og" => 1, "Os" => 142,
    1833
      "P" => 173, "Pa" => 33, "Pb" => 165, "Pd" => 151, "Pm" => 53, "Po" => 176, "Pr" => 55, "Pt" => 150, "Pu" => 30,
    1834
      "Ra" => 13, "Rb" => 9, "Re" => 138, "Rf" => 125, "Rg" => 153, "Rh" => 147, "Rn" => 1, "Ru" => 143,
    1835
      "S" => 179, "Sb" => 171, "Sc" => 124, "Se" => 178, "Sg" => 133, "Si" => 168, "Sm" => 52, "Sn" => 166, "Sr" => 15,
    1836
      "Ta" => 130, "Tb" => 49, "Tc" => 139, "Te" => 177, "Th" => 34, "Ti" => 128, "Tl" => 160, "Tm" => 45, "Ts" => 181,
    1837
      "U" => 32, "V" => 132, "W" => 134, "Xe" => 2, "Y" => 123, "Yb" => 44, "Zn" => 159, "Zr" => 127,
    1838
        // The following come from E.A. Moore who said to treat them like chemicals
    1839
        // These stand for methyl, ethyl, alkyl, acetyl and phenyl and apparently are quite commonly used ("Ac" is already a chemical)
    1840
        // A full(er?) list is at en.wikipedia.org/wiki/Skeletal_formula#Alkyl_groups and in following sections
    1841
        "Me" => 0, "Et" => 0, "R" => 0, /* "Ac" => 0, */ "Ph" => 0,
    1842
        "X" => 0, /* treated as an unknown */
    1843
    };
    1844
    1845
    // A map of the chemical elements and their atomic numbers
    1846
    static CHEMICAL_ELEMENT_ATOMIC_NUMBER: phf::Map<&str, u32> = phf_map! {
    1847
        "H" => 1, "He" => 2, "Li" => 3, "Be" => 4, "B" => 5, "C" => 6, "N" => 7, "O" => 8, "F" => 9, "Ne" => 10,
    1848
        "Na" => 11, "Mg" => 12, "Al" => 13, "Si" => 14, "P" => 15, "S" => 16, "Cl" => 17, "Ar" => 18, "K" => 19, "Ca" => 20,
    1849
        "Sc" => 21, "Ti" => 22, "V" => 23, "Cr" => 24, "Mn" => 25, "Fe" => 26, "Co" => 27, "Ni" => 28, "Cu" => 29, "Zn" => 30,
    1850
        "Ga" => 31, "Ge" => 32, "As" => 33, "Se" => 34, "Br" => 35, "Kr" => 36, "Rb" => 37, "Sr" => 38, "Y" => 39, "Zr" => 40,
    1851
        "Nb" => 41, "Mo" => 42, "Tc" => 43, "Ru" => 44, "Rh" => 45, "Pd" => 46, "Ag" => 47, "Cd" => 48, "In" => 49, "Sn" => 50,
    1852
        "Sb" => 51, "Te" => 52, "I" => 53, "Xe" => 54, "Cs" => 55, "Ba" => 56, "La" => 57, "Ce" => 58, "Pr" => 59, "Nd" => 60,
    1853
        "Pm" => 61, "Sm" => 62, "Eu" => 63, "Gd" => 64, "Tb" => 65, "Dy" => 66, "Ho" => 67, "Er" => 68, "Tm" => 69, "Yb" => 70,
    1854
        "Lu" => 71, "Hf" => 72, "Ta" => 73, "W" => 74, "Re" => 75, "Os" => 76, "Ir" => 77, "Pt" => 78, "Au" => 79, "Hg" => 80,
    1855
        "Tl" => 81, "Pb" => 82, "Bi" => 83, "Po" => 84, "At" => 85, "Rn" => 86, "Fr" => 87, "Ra" => 88, "Ac" => 89, "Th" => 90,
    1856
        "Pa" => 91, "U" => 92, "Np" => 93, "Pu" => 94, "Am" => 95, "Cm" => 96, "Bk" => 97, "Cf" => 98, "Es" => 99, "Fm" => 100,
    1857
        "Md" => 101, "No" => 102, "Lr" => 103, "Rf" => 104, "Db" => 105, "Sg" => 106, "Bh" => 107, "Hs" => 108, "Mt" => 109, "Ds" => 110,
    1858
        "Rg" => 111, "Cn" => 112, "Nh" => 113, "Fl" => 114, "Mc" => 115, "Lv" => 116, "Ts" => 117, "Og" => 118,
    1859
    };
    1860
    1861
    26.9k
    pub fn is_chemical_element(node: Element) -> bool {
    1862
      // FIX: allow name to be in an mrow (e.g., <mi>N</mi><mi>a</mi>
    1863
    26.9k
      let name = name(node);
    1864
    26.9k
      if name != "mi" && name != "mtext"701 {
    1865
    71
        return false;
    1866
    26.9k
      }
    1867
    1868
    26.9k
      let text = as_text(node);
    1869
    26.9k
      return CHEMICAL_ELEMENT_ELECTRONEGATIVITY.contains_key(text) ||
    1870
    23.0k
               has_chem_intent(node, "chemical-element") ||
    1871
    23.0k
               has_inherited_property(node, "chemical-formula");
    1872
    26.9k
    }
    1873
    1874
    1875
    #[cfg(test)]
    1876
    mod chem_tests {
    1877
    1878
    1879
    #[allow(unused_imports)]
    1880
      use super::super::init_logger;
    1881
      use super::super::are_strs_canonically_equal;
    1882
        use super::*;
    1883
    1884
    40
        fn parse_mathml_string<F>(test: &str, test_mathml: F) -> bool
    1885
    40
                where F: Fn(Element) -> bool {
    1886
            use sxd_document::parser;
    1887
            use crate::interface::{get_element, trim_element};
    1888
    1889
    1890
    40
            let test = if test.starts_with("<math") {test0} else {&format!("<math>{}</math>", test)};
    1891
    40
            let new_package = parser::parse(test);
    1892
    40
            if let Err(e0) = new_package {
    1893
    0
                panic!("Invalid MathML input:\n{}\nError is: {}", &test, &e.to_string());
    1894
    40
            }
    1895
    1896
    40
            let new_package = new_package.unwrap();
    1897
    40
            let mut mathml = get_element(&new_package);
    1898
    40
            trim_element(mathml, false);
    1899
    40
            mathml = as_element(mathml.children()[0]);
    1900
    40
            return test_mathml(mathml);
    1901
    40
        }
    1902
    1903
        #[test]
    1904
    1
        fn test_noble_element() {
    1905
            // mathml test strings need to be canonical MathML since we aren't testing canonicalize()
    1906
    1
            let test = "<mrow> <mi>Na</mi> <mo>&#x2063;</mo> <mi>Cl</mi> </mrow>"; //
    1907
    1
            assert!( !parse_mathml_string(test, |mathml| has_noble_element( &collect_elements(mathml).unwrap() )) );
    1908
    1
            let test = "<mrow> <mi>Ar</mi> <mo>&#x2063;</mo> <mi>Cl</mi> </mrow>"; //
    1909
    1
            assert!( parse_mathml_string(test, |mathml| has_noble_element( &collect_elements(mathml).unwrap() )) );
    1910
    1
            let test = "<mrow> <mi>Ne</mi> </mrow>"; //
    1911
    1
            assert!( parse_mathml_string(test, |mathml| has_noble_element( &collect_elements(mathml).unwrap() )) );
    1912
    1
        }
    1913
    1914
        #[test]
    1915
    1
        fn test_alphabetical_order() {
    1916
            // mathml test strings need to be canonical MathML since we aren't testing canonicalize()
    1917
    1
            let test = r#"<mrow>
    1918
    1
                <msub><mi>C</mi><mn>6</mn></msub><mo>&#x2063;</mo>
    1919
    1
                <msub><mi>H</mi><mn>14</mn></msub>
    1920
    1
                 </mrow>"#;
    1921
    1
            assert!( parse_mathml_string(test, |mathml| is_alphabetical( &collect_elements(mathml).unwrap() )) );
    1922
    1
            let test = r#"<mrow>
    1923
    1
                 <msub><mi>C</mi><mn>6</mn></msub><mo>&#x2063;</mo>
    1924
    1
                 <msub><mi>H</mi><mn>12</mn></msub><mo>&#x2063;</mo>
    1925
    1
                 <msub><mi>O</mi><mn>6</mn></msub>
    1926
    1
                  </mrow>"#;
    1927
    1
            assert!( parse_mathml_string(test, |mathml| is_alphabetical( &collect_elements(mathml).unwrap() )) );
    1928
    1
            let test = "<mrow> <mi>B</mi> <mo>&#x2063;</mo> <mi>C</mi> <mo>&#x2063;</mo> <mi>O</mi></mrow>"; // "C" should be first
    1929
    1
            assert!( !parse_mathml_string(test, |mathml| is_alphabetical( &collect_elements(mathml).unwrap() )) );
    1930
    1
            let test = "<mrow> <mi>P</mi> <mo>&#x2063;</mo> <mi>B</mi> <mo>&#x2063;</mo> <mi>O</mi></mrow>"; // not alphabetical
    1931
    1
            assert!( !parse_mathml_string(test, |mathml| is_alphabetical( &collect_elements(mathml).unwrap() )) );
    1932
    1
        }
    1933
    1934
        #[test]
    1935
    1
        fn test_is_structural() {
    1936
            // mathml test strings need to be canonical MathML since we aren't testing canonicalize()
    1937
    1
            let test = r#"<mrow>
    1938
    1
                <msub><mi>C</mi><mn>6</mn></msub><mo>&#x2063;</mo>
    1939
    1
                <msub><mi>H</mi><mn>14</mn></msub>
    1940
    1
                 </mrow>"#;
    1941
    1
            assert!( !parse_mathml_string(test, |mathml| is_structural( &collect_elements(mathml).unwrap() )) );
    1942
    1
            let test = "<mrow> <mi>B</mi> <mo>&#x2063;</mo> <mi>C</mi> <mo>&#x2063;</mo> <mi>O</mi></mrow>";
    1943
    1
            assert!( !parse_mathml_string(test, |mathml| is_structural( &collect_elements(mathml).unwrap() )) );
    1944
    1
            let test = "<mrow> <mi>H</mi> <mo>&#x2063;</mo> <mi>O</mi> <mo>&#x2063;</mo> <mi>H</mi></mrow>";
    1945
    1
            assert!( parse_mathml_string(test, |mathml| is_structural( &collect_elements(mathml).unwrap() )) );
    1946
    1
            let test = "<mrow data-chem-formula='9'>
    1947
    1
                    <mmultiscripts data-chem-formula='1'>
    1948
    1
                    <mi mathvariant='normal' data-chem-element='1'>H</mi>
    1949
    1
                    <mn>2</mn>
    1950
    1
                    <none></none>
    1951
    1
                    </mmultiscripts>
    1952
    1
                    <mo data-changed='added'>&#x2063;</mo>
    1953
    1
                    <mi mathvariant='normal' data-chem-element='1'>C</mi>
    1954
    1
                    <mo data-chemical-bond='true' data-chem-formula-op='1'>=</mo>
    1955
    1
                    <mi mathvariant='normal' data-chem-element='1'>C</mi>
    1956
    1
                    <mo data-changed='added'>&#x2063;</mo>
    1957
    1
                    <mmultiscripts data-chem-formula='1'>
    1958
    1
                    <mi mathvariant='normal' data-chem-element='1'>H</mi>
    1959
    1
                    <mn>2</mn>
    1960
    1
                    <none></none>
    1961
    1
                    </mmultiscripts>
    1962
    1
                </mrow>";
    1963
    1
            assert!( parse_mathml_string(test, |mathml| is_structural( &collect_elements(mathml).unwrap() )) );
    1964
    1
        }
    1965
    1966
    1967
        #[test]
    1968
    1
        fn test_electronegativity_order() {
    1969
            // mathml test strings need to be canonical MathML since we aren't testing canonicalize()
    1970
    1
            let test = r#"<mrow>
    1971
    1
                <mi>N</mi><mo>&#x2063;</mo>
    1972
    1
                <msub><mi>H</mi><mn>3</mn></msub>
    1973
    1
                 </mrow>"#;
    1974
    1
            assert!( parse_mathml_string(test, |mathml| is_ordered_by_electronegativity( &collect_elements(mathml).unwrap() )) );
    1975
    1
            let test = r#"<mrow>
    1976
    1
                <mi>O</mi><mo>&#x2063;</mo>
    1977
    1
                <msub><mi>F</mi><mn>2</mn></msub>
    1978
    1
                 </mrow>"#;
    1979
    1
            assert!( parse_mathml_string(test, |mathml| is_ordered_by_electronegativity( &collect_elements(mathml).unwrap() )) );
    1980
    1
            let test = r#"<mrow>
    1981
    1
                <msub><mi>Rb</mi><mn>15</mn></msub><mo>&#x2063;</mo>
    1982
    1
                <msub><mi>Hg</mi><mn>16</mn></msub>
    1983
    1
                 </mrow>"#;
    1984
    1
            assert!( parse_mathml_string(test, |mathml| is_ordered_by_electronegativity( &collect_elements(mathml).unwrap() )) );
    1985
    1
            let test = r#"
    1986
    1
                <mrow><msup>
    1987
    1
                    <mo>[</mo>
    1988
    1
                        <mi>Si</mi><mo>&#x2063;</mo>
    1989
    1
                        <msub><mi>As</mi><mn>4</mn></msub>
    1990
    1
                    <mo>]</mo>
    1991
    1
                    <mrow><mn>8</mn><mo>-</mo></mrow>
    1992
    1
                </msup></mrow>"#;
    1993
    1
            assert!( parse_mathml_string(test, |mathml| is_ordered_by_electronegativity( &collect_elements(as_element(mathml.children()[0])).unwrap() )) );
    1994
    1
            let test = r#"<mrow>
    1995
    1
                    <mi>Si</mi><mo>&#x2063;</mo>
    1996
    1
                    <msub><mi>H</mi><mn>2</mn></msub>
    1997
    1
                    <mi>Br</mi><mo>&#x2063;</mo>
    1998
    1
                    <mi>Cl</mi>
    1999
    1
                    </mrow>"#;
    2000
    1
            assert!( parse_mathml_string(test, |mathml| is_ordered_by_electronegativity( &collect_elements(mathml).unwrap() )) );
    2001
    1
        }
    2002
    2003
        #[test]
    2004
    1
        fn test_order() {
    2005
    1
            let test = r#"<mrow>
    2006
    1
                <msub><mi>C</mi><mn>2</mn></msub><mo>&#x2063;</mo>
    2007
    1
                <msub><mi>H</mi><mn>4</mn></msub><mo>&#x2063;</mo>
    2008
    1
                <msub><mrow> <mo>(</mo><mi>N</mi> <mo>&#x2063;</mo> <msub> <mi>H</mi> <mn>2</mn> </msub><mo>)</mo> </mrow><mn>2</mn></msub>
    2009
    1
                 </mrow>"#;
    2010
    1
            assert!( parse_mathml_string(test, is_order_ok) );
    2011
    1
            let test = r#"<mrow>
    2012
    1
                <mi>Fe</mi><mo>&#x2063;</mo>
    2013
    1
                <mi>O</mi><mo>&#x2063;</mo>
    2014
    1
                <mrow> <mo>(</mo><mrow><mi>O</mi> <mo>&#x2063;</mo><mi>H</mi> </mrow><mo>)</mo> </mrow>
    2015
    1
                 </mrow>"#;
    2016
    1
            assert!( parse_mathml_string(test, is_order_ok) );
    2017
    1
            let test = r#"<mrow>  // R-4.4.3.3 -- Chain compound doesn't fit rules but should be accepted
    2018
    1
                    <mi>Br</mi><mo>&#x2063;</mo>
    2019
    1
                    <mi>S</mi><mo>&#x2063;</mo>
    2020
    1
                    <mi>C</mi><mo>&#x2063;</mo>
    2021
    1
                    <mi>N</mi>
    2022
    1
                    </mrow>"#;
    2023
    1
            assert!( parse_mathml_string(test, |mathml| likely_chem_formula(mathml)==5) );
    2024
    1
        }
    2025
    2026
        #[test]
    2027
    1
        fn test_simple_double_bond() {
    2028
    1
            let test1 = r#"<mrow><mi>C</mi><mo>=</mo><mi>C</mi></mrow>"#;
    2029
    1
            assert!( parse_mathml_string(test1, |mathml| likely_chem_formula(mathml) < CHEMISTRY_THRESHOLD) ); // just under threshold
    2030
    1
            let test2 = r#"<mrow><mi>C</mi><mo>∷</mo><mi>O</mi></mrow>"#;
    2031
    1
            assert!( parse_mathml_string(test2, |mathml| likely_chem_formula(mathml)==CHEMISTRY_THRESHOLD) );
    2032
    1
            let test3 = r#"<mrow><mi>N</mi><mo>=</mo><mi>N</mi></mrow>"#;
    2033
    1
            assert!( parse_mathml_string(test3, |mathml| likely_chem_formula(mathml) < CHEMISTRY_THRESHOLD) ); // just under threshold
    2034
    1
            let test4 = r#"<mrow><mi>Sn</mi><mo>=</mo><mi>Sn</mi></mrow>"#;
    2035
    1
            assert!( parse_mathml_string(test4, |mathml| likely_chem_formula(mathml) == 8) );
    2036
    1
            let test5 = r#"<mrow><mi>O</mi><mo>=</mo><mi>S</mi></mrow>"#;
    2037
    1
            assert!( parse_mathml_string(test5, |mathml| likely_chem_formula(mathml) < CHEMISTRY_THRESHOLD) );  // just under threshold
    2038
    1
            let test10 = r#"<mrow><mi>K</mi><mo>=</mo><mi>K</mi></mrow>"#;
    2039
    1
            assert!( parse_mathml_string(test10, |mathml| likely_chem_formula(mathml) == NOT_CHEMISTRY) );
    2040
    1
            let test11 = r#"<mrow><mi>C</mi><mo>=</mo><mi>K</mi></mrow>"#;
    2041
    1
            assert!( parse_mathml_string(test11, |mathml| likely_chem_formula(mathml) == NOT_CHEMISTRY) );
    2042
    1
        }
    2043
    2044
        #[test]
    2045
    1
        fn test_double_bond() {
    2046
    1
            let test1 = r#"<mrow><mi mathvariant='normal'>C</mi><msub><mi mathvariant='normal'>H</mi><mn>2</mn></msub><mo>=</mo><mi>C</mi></mrow>"#;
    2047
    1
            assert!( parse_mathml_string(test1, |mathml| likely_chem_formula(mathml)==8) );
    2048
    1
            let test2 = r#"<mrow><mi mathvariant='normal'>C</mi><msub><mi mathvariant='normal'>H</mi><mn>2</mn></msub><mo>=</mo>
    2049
    1
            <mi>C</mi><mi>H</mi><mi>R</mi></mrow>"#;
    2050
    1
            assert!( parse_mathml_string(test2, |mathml| likely_chem_formula(mathml)==12) );
    2051
    1
            let test3 = r#"<mrow><msub><mi mathvariant='normal'>H</mi><mn>2</mn></msub><mi mathvariant='normal'>C</mi><mo>=</mo>
    2052
    1
                    <mi>C</mi><msub><mi mathvariant='normal'>H</mi><mn>2</mn></msub></mrow>"#;
    2053
    1
            assert!( parse_mathml_string(test3, |mathml| likely_chem_formula(mathml)==11) );
    2054
    1
            let test4 = r#"<mrow><mi>H</mi><mo>-</mo><mi>N</mi><mo>=</mo><mi>N</mi><mo>-</mo><mi>H</mi></mrow>"#;
    2055
    1
            assert!( parse_mathml_string(test4, |mathml| likely_chem_formula(mathml)==10) );
    2056
    1
            let test10 = r#"<mrow><mi mathvariant='normal'>C</mi><msub><mi mathvariant='normal'>H</mi><mn>3</mn></msub><mo>=</mo><mi>C</mi></mrow>"#;
    2057
    1
            assert!( parse_mathml_string(test10, |mathml| likely_chem_formula(mathml)==NOT_CHEMISTRY) );
    2058
    1
        }
    2059
    2060
        #[test]
    2061
        #[ignore]   // It would be good to say "not chemistry" for this, but there aren't rules for that at the moment
    2062
    0
        fn test_water_bond() {
    2063
    0
            let test11 = r#"<mrow><msub><mi mathvariant='normal'>H</mi><mn>2</mn></msub><mi mathvariant='normal'>O</mi><mo>=</mo><mi>O</mi></mrow>"#;
    2064
    0
            assert!( parse_mathml_string(test11, |mathml| {println!("val={}", likely_chem_formula(mathml)); likely_chem_formula(mathml)==8}) );
    2065
            // assert!( parse_mathml_string(test11, |mathml| likely_chem_formula(mathml)==NOT_CHEMISTRY) );
    2066
    0
        }
    2067
    2068
    2069
        #[test]
    2070
    1
        fn test_triple_bond() {
    2071
    1
            let test1 = r#"<mrow><mi>C</mi><mo>≡</mo><mi>C</mi></mrow>"#;
    2072
    1
            assert!( parse_mathml_string(test1, |mathml| likely_chem_formula(mathml) < CHEMISTRY_THRESHOLD) );
    2073
    1
            let test2 = r#"<mrow><mi>C</mi><mo>:::</mo><mi>O</mi></mrow>"#;
    2074
    1
            assert!( parse_mathml_string(test2, |mathml| likely_chem_formula(mathml)==CHEMISTRY_THRESHOLD) );
    2075
    1
            let test3 = r#"<mrow><mi>H</mi><mo>-</mo><mi>C</mi><mo>≡</mo><mi>C</mi><mo>-</mo><mi>H</mi></mrow>"#;
    2076
    1
            assert!( parse_mathml_string(test3, |mathml| likely_chem_formula(mathml)==10) );
    2077
    1
            let test4 = r#"<mrow><mi>H</mi><mo>-</mo><mi>C</mi><mo>≡</mo><mi>C</mi><mo>-</mo><mi>H</mi></mrow>"#;
    2078
    1
            assert!( parse_mathml_string(test4, |mathml| likely_chem_formula(mathml)==10) );
    2079
    1
            let test5 = r#"<mrow><mi>N</mi><mo>-</mo><mi>C</mi><mo>≡</mo><mi>C</mi><mo>-</mo><mi>N</mi></mrow>"#;
    2080
    1
            assert!( parse_mathml_string(test5, |mathml| likely_chem_formula(mathml)==10) );
    2081
    1
            let test6 = r#"<mrow><mi>H</mi><mo>-</mo><mi>C</mi><mo>≡</mo>
    2082
    1
                <mi>C</mi><mo>-</mo><mi mathvariant='normal'>C</mi><msub><mi mathvariant='normal'>H</mi><mn>3</mn></msub></mrow>"#; // 1-Propyne
    2083
    1
            assert!( parse_mathml_string(test6, |mathml| likely_chem_formula(mathml)==14) );
    2084
            // assert!( parse_mathml_string(test6, |mathml| {println!("val={}", likely_chem_formula(mathml)); likely_chem_formula(mathml)==10}) );
    2085
    1
            let test10 = r#"<mrow><mi>O</mi><mo>:::</mo><mi>S</mi></mrow>"#;
    2086
    1
            assert!( parse_mathml_string(test10, |mathml| likely_chem_formula(mathml)==NOT_CHEMISTRY) );
    2087
    1
            let test11 = r#"<mrow><mi>Pb</mi><mo>≡</mo><mi>Pb</mi></mrow>"#;
    2088
    1
            assert!( parse_mathml_string(test11, |mathml| likely_chem_formula(mathml)==NOT_CHEMISTRY) );
    2089
    1
            let test12 = r#"<mrow><mi>C</mi><mo>≡</mo><mi>K</mi></mrow>"#;
    2090
    1
            assert!( parse_mathml_string(test12, |mathml| likely_chem_formula(mathml)==NOT_CHEMISTRY) );
    2091
    1
        }
    2092
    2093
        #[test]
    2094
    1
        fn split_mi() {
    2095
    1
            let test = "<math><mi>LiF</mi></math>";
    2096
    1
            let target = "<math>
    2097
    1
                <mrow data-changed='added' data-chem-formula='5'>
    2098
    1
                    <mi data-chem-element='3'>Li</mi>
    2099
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2100
    1
                    <mi mathvariant='normal' data-split='true' data-chem-element='1'>F</mi>
    2101
    1
                </mrow>
    2102
    1
           </math>";
    2103
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2104
    1
        }
    2105
    2106
        #[test]
    2107
    1
        fn no_split_mi() {
    2108
    1
            let test = "<math><mi>HC</mi></math>";
    2109
    1
            let target = "<math>
    2110
    1
                 <mi>HC</mi>
    2111
    1
            </math>";
    2112
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2113
    1
        }
    2114
    2115
        #[test]
    2116
    1
        fn combine_mi() {
    2117
    1
            let test = "<math><mi>H</mi><mi>C</mi><mi>l</mi></math>";
    2118
    1
            let target = " <math>
    2119
    1
                <mrow data-changed='added' data-chem-formula='5'>
    2120
    1
                <mi data-chem-element='1'>H</mi>
    2121
    1
                <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2122
    1
                <mi data-merged='true' data-chem-element='3'>Cl</mi>
    2123
    1
                </mrow>
    2124
    1
            </math>";
    2125
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2126
    1
        }
    2127
    2128
        #[test]
    2129
    1
        fn no_combine() {
    2130
    1
            let test = "<math><mi>C</mi><mi>l</mi></math>";
    2131
    1
            let target = "<math>
    2132
    1
                <mrow data-changed='added'>
    2133
    1
                    <mi>C</mi>
    2134
    1
                    <mo data-changed='added'>&#x2062;</mo>
    2135
    1
                    <mi>l</mi>
    2136
    1
                </mrow>
    2137
    1
            </math>";
    2138
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2139
    1
        }
    2140
    2141
        #[test]
    2142
    1
        fn add_script() {
    2143
    1
            let test = "<math> <mi>SO</mi>  <msub> <mrow></mrow> <mn>2</mn> </msub> </math>";
    2144
    1
            let target = "<math>
    2145
    1
                <mrow data-changed='added' data-chem-formula='5'>
    2146
    1
                    <mi mathvariant='normal' data-chem-element='1'>S</mi>
    2147
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2148
    1
                    <mmultiscripts data-chem-formula='2'>
    2149
    1
                        <mi mathvariant='normal' data-split='true' data-chem-element='1'>O</mi>
    2150
    1
                        <mn>2</mn>
    2151
    1
                        <none></none>
    2152
    1
                    </mmultiscripts>
    2153
    1
                </mrow>
    2154
    1
           </math>";
    2155
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2156
    1
        }
    2157
    2158
        #[test]
    2159
    1
        fn add_script_bug_287() {
    2160
    1
            let test = r#"<math><mrow>
    2161
    1
                <msubsup>
    2162
    1
                    <mrow><mi mathvariant="normal">SO</mi></mrow>
    2163
    1
                    <mn>4</mn>
    2164
    1
                    <mrow><mn>2</mn><mo>&#x2212;</mo></mrow>
    2165
    1
                </msubsup>
    2166
    1
                </mrow></math>"#;
    2167
    1
            let target = r#"<math>
    2168
    1
                <mrow data-changed='added' data-chem-formula='7'>
    2169
    1
                    <mi mathvariant='normal' data-chem-element='1'>S</mi>
    2170
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2171
    1
                    <msubsup data-chem-formula='5'>
    2172
    1
                        <mi mathvariant='normal' data-split='true' data-chem-element='1'>O</mi>
    2173
    1
                        <mn>4</mn>
    2174
    1
                        <mrow data-chem-formula='3'><mn>2</mn><mo>-</mo></mrow>
    2175
    1
                    </msubsup>
    2176
    1
                </mrow>
    2177
    1
                </math>"#;
    2178
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2179
    1
        }
    2180
    2181
        #[test]
    2182
    1
        fn salt() {
    2183
    1
            let test = "<math><mi>Na</mi><mi>Cl</mi></math>";
    2184
    1
            let target = "<math>
    2185
    1
                <mrow data-changed='added' data-chem-formula='7'>
    2186
    1
                    <mi data-chem-element='3'>Na</mi>
    2187
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2188
    1
                    <mi data-chem-element='3'>Cl</mi>
    2189
    1
                </mrow>
    2190
    1
            </math>";
    2191
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2192
    1
        }
    2193
    2194
        #[test]
    2195
    1
        fn water() {
    2196
    1
            let test = "<math><msub><mi mathvariant='normal'>H</mi><mn>2</mn></msub><mi mathvariant='normal'>O</mi></math>";
    2197
    1
            let target = "<math>
    2198
    1
                <mrow data-changed='added' data-chem-formula='5'>
    2199
    1
                    <msub data-chem-formula='2'>
    2200
    1
                        <mi mathvariant='normal' data-chem-element='2'>H</mi>
    2201
    1
                        <mn>2</mn>
    2202
    1
                    </msub>
    2203
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2204
    1
                    <mi mathvariant='normal' data-chem-element='2'>O</mi>
    2205
    1
                </mrow>
    2206
    1
            </math>";
    2207
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2208
    1
        }
    2209
    2210
        #[test]
    2211
    1
        fn mhchem_water() {
    2212
    1
            let test = "<math>
    2213
    1
                <mrow>
    2214
    1
                <mrow>
    2215
    1
                    <mi mathvariant='normal'>H</mi>
    2216
    1
                </mrow>
    2217
    1
                <msub>
    2218
    1
                    <mrow>
    2219
    1
                    <mrow>
    2220
    1
                        <mpadded width='0'>
    2221
    1
                        <mphantom>
    2222
    1
                            <mi>A</mi>
    2223
    1
                        </mphantom>
    2224
    1
                        </mpadded>
    2225
    1
                    </mrow>
    2226
    1
                    </mrow>
    2227
    1
                    <mrow>
    2228
    1
                    <mrow>
    2229
    1
                        <mpadded height='0'>
    2230
    1
                        <mn>2</mn>
    2231
    1
                        </mpadded>
    2232
    1
                    </mrow>
    2233
    1
                    </mrow>
    2234
    1
                </msub>
    2235
    1
                <mrow>
    2236
    1
                    <mi mathvariant='normal'>O</mi>
    2237
    1
                </mrow>
    2238
    1
                </mrow>
    2239
    1
            </math>";
    2240
    1
            let target = "<math>
    2241
    1
                <mrow data-chem-formula='5'>
    2242
    1
                    <mmultiscripts data-chem-formula='2'>
    2243
    1
                        <mi mathvariant='normal' data-chem-element='2'>H</mi>
    2244
    1
                        <mn>2</mn>
    2245
    1
                        <none></none>
    2246
    1
                    </mmultiscripts>
    2247
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2248
    1
                    <mi mathvariant='normal' data-chem-element='2'>O</mi>
    2249
    1
                </mrow>
    2250
    1
           </math>";
    2251
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2252
    1
        }
    2253
    2254
        #[test]
    2255
    1
        fn carbon() {
    2256
    1
            let test = "<math><mi>C</mi></math>";     // not enough to trigger recognition
    2257
    1
            let target = " <math>
    2258
    1
                <mi>C</mi>
    2259
    1
            </math>";
    2260
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2261
    1
        }
    2262
    2263
        #[test]
    2264
    1
        fn sulfate() {
    2265
    1
            let test = "<math><mrow><msup>
    2266
    1
                    <mrow><mo>[</mo><mi>S</mi><msub><mi>O</mi><mn>4</mn></msub><mo>]</mo></mrow>
    2267
    1
                    <mrow><mn>2</mn><mo>&#x2212;</mo></mrow>
    2268
    1
                </msup></mrow></math>";
    2269
    1
            let target = "<math>
    2270
    1
            <msup data-chem-formula='9'>
    2271
    1
              <mrow data-chem-formula='6'>
    2272
    1
                <mo>[</mo>
    2273
    1
                <mrow data-changed='added' data-chem-formula='3'>
    2274
    1
                  <mi data-chem-element='1'>S</mi>
    2275
    1
                  <mo data-changed='added'>&#x2063;</mo>
    2276
    1
                  <msub data-chem-formula='1'>
    2277
    1
                    <mi data-chem-element='1'>O</mi>
    2278
    1
                    <mn>4</mn>
    2279
    1
                  </msub>
    2280
    1
                </mrow>
    2281
    1
                <mo>]</mo>
    2282
    1
              </mrow>
    2283
    1
              <mrow data-chem-formula='3'>
    2284
    1
                <mn>2</mn>
    2285
    1
                <mo>-</mo>
    2286
    1
              </mrow>
    2287
    1
            </msup>
    2288
    1
           </math>";
    2289
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2290
    1
        }
    2291
    2292
        #[test]
    2293
    1
        fn aluminum_sulfate() {
    2294
    1
            let test = "<math><mrow><msub><mi>Al</mi><mn>2</mn></msub>
    2295
    1
                    <msub><mrow><mo>(</mo><mi>S</mi><msub><mi>O</mi><mn>4</mn></msub><mo>)</mo></mrow><mn>3</mn></msub></mrow></math>";
    2296
    1
            let target = " <math>
    2297
    1
                    <mrow data-chem-formula='10'>
    2298
    1
                        <msub data-chem-formula='3'>
    2299
    1
                            <mi data-chem-element='3'>Al</mi>
    2300
    1
                            <mn>2</mn>
    2301
    1
                        </msub>
    2302
    1
                        <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2303
    1
                        <msub data-chem-formula='6'>
    2304
    1
                            <mrow data-chem-formula='6'>
    2305
    1
                            <mo>(</mo>
    2306
    1
                            <mrow data-changed='added' data-chem-formula='3'>
    2307
    1
                                <mi data-chem-element='1'>S</mi>
    2308
    1
                                <mo data-changed='added'>&#x2063;</mo>
    2309
    1
                                <msub data-chem-formula='1'>
    2310
    1
                                <mi data-chem-element='1'>O</mi>
    2311
    1
                                <mn>4</mn>
    2312
    1
                                </msub>
    2313
    1
                            </mrow>
    2314
    1
                            <mo>)</mo>
    2315
    1
                            </mrow>
    2316
    1
                            <mn>3</mn>
    2317
    1
                        </msub>
    2318
    1
                    </mrow>
    2319
    1
                </math>";
    2320
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2321
    1
        }
    2322
    2323
        #[test]
    2324
    1
        fn ethanol_bonds() {
    2325
    1
            let test = "<math>
    2326
    1
                    <mrow>
    2327
    1
                        <mi>C</mi>
    2328
    1
                        <msub>  <mi>H</mi> <mn>3</mn> </msub>
    2329
    1
                        <mo>&#x2212;</mo>
    2330
    1
                        <mi>C</mi>
    2331
    1
                        <msub>  <mi>H</mi> <mn>2</mn> </msub>
    2332
    1
                        <mo>&#x2212;</mo>
    2333
    1
                        <mi>O</mi>
    2334
    1
                        <mi>H</mi>
    2335
    1
                    </mrow>
    2336
    1
                </math>";
    2337
    1
            let target = "<math>
    2338
    1
            <mrow data-chem-formula='13'>
    2339
    1
              <mi data-chem-element='1'>C</mi>
    2340
    1
              <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2341
    1
              <msub data-chem-formula='1'>
    2342
    1
                <mi data-chem-element='1'>H</mi>
    2343
    1
                <mn>3</mn>
    2344
    1
              </msub>
    2345
    1
              <mo data-chemical-bond='true' data-chem-formula-op='1'>-</mo>
    2346
    1
              <mi data-chem-element='1'>C</mi>
    2347
    1
              <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2348
    1
              <msub data-chem-formula='1'>
    2349
    1
                <mi data-chem-element='1'>H</mi>
    2350
    1
                <mn>2</mn>
    2351
    1
              </msub>
    2352
    1
              <mo data-chemical-bond='true' data-chem-formula-op='1'>-</mo>
    2353
    1
              <mi data-chem-element='1'>O</mi>
    2354
    1
              <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2355
    1
              <mi data-chem-element='1'>H</mi>
    2356
    1
            </mrow>
    2357
    1
           </math>";
    2358
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2359
    1
        }
    2360
    2361
        #[test]
    2362
    1
        fn dichlorine_hexoxide() {
    2363
            // init_logger();
    2364
    1
            let test = "<math><mrow>
    2365
    1
                <msup>
    2366
    1
                <mrow><mo>[</mo><mi>Cl</mi><msub><mi>O</mi><mn>2</mn></msub><mo>]</mo></mrow>
    2367
    1
                <mo>+</mo>
    2368
    1
                </msup>
    2369
    1
                <msup>
    2370
    1
                <mrow><mo>[</mo><mi>Cl</mi><msub><mi>O</mi><mn>4</mn></msub><mo>]</mo></mrow>
    2371
    1
                <mo>-</mo>
    2372
    1
                </msup>
    2373
    1
            </mrow></math>";
    2374
    1
            let target = "<math>
    2375
    1
                <mrow data-chem-formula='19'>
    2376
    1
                    <msup data-chem-formula='9'>
    2377
    1
                        <mrow data-chem-formula='8'>
    2378
    1
                        <mo>[</mo>
    2379
    1
                        <mrow data-changed='added' data-chem-formula='5'>
    2380
    1
                            <mi data-chem-element='3'>Cl</mi>
    2381
    1
                            <mo data-changed='added'>&#x2063;</mo>
    2382
    1
                            <msub data-chem-formula='1'>
    2383
    1
                            <mi data-chem-element='1'>O</mi>
    2384
    1
                            <mn>2</mn>
    2385
    1
                            </msub>
    2386
    1
                        </mrow>
    2387
    1
                        <mo>]</mo>
    2388
    1
                        </mrow>
    2389
    1
                        <mo>+</mo>
    2390
    1
                    </msup>
    2391
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2392
    1
                    <msup data-chem-formula='9'>
    2393
    1
                        <mrow data-chem-formula='8'>
    2394
    1
                        <mo>[</mo>
    2395
    1
                        <mrow data-changed='added' data-chem-formula='5'>
    2396
    1
                            <mi data-chem-element='3'>Cl</mi>
    2397
    1
                            <mo data-changed='added'>&#x2063;</mo>
    2398
    1
                            <msub data-chem-formula='1'>
    2399
    1
                            <mi data-chem-element='1'>O</mi>
    2400
    1
                            <mn>4</mn>
    2401
    1
                            </msub>
    2402
    1
                        </mrow>
    2403
    1
                        <mo>]</mo>
    2404
    1
                        </mrow>
    2405
    1
                        <mo>-</mo>
    2406
    1
                    </msup>
    2407
    1
                </mrow>
    2408
    1
           </math>";
    2409
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2410
    1
        }
    2411
    2412
        #[test]
    2413
    1
        fn ethylene_with_bond() {
    2414
    1
            let test = "<math><mrow>
    2415
    1
                    <msub><mi>H</mi><mn>2</mn></msub><mi>C</mi>
    2416
    1
                    <mo>=</mo>
    2417
    1
                    <mi>C</mi><msub><mi>H</mi><mn>2</mn></msub>
    2418
    1
                </mrow></math>";
    2419
    1
            let target = "<math>
    2420
    1
                <mrow data-chem-formula='8'>
    2421
    1
                    <msub data-chem-formula='1'>
    2422
    1
                        <mi data-chem-element='1'>H</mi>
    2423
    1
                        <mn>2</mn>
    2424
    1
                    </msub>
    2425
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2426
    1
                    <mi data-chem-element='1'>C</mi>
    2427
    1
                    <mo data-chemical-bond='true' data-chem-formula-op='1'>=</mo>
    2428
    1
                    <mi data-chem-element='1'>C</mi>
    2429
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2430
    1
                    <msub data-chem-formula='1'>
    2431
    1
                        <mi data-chem-element='1'>H</mi>
    2432
    1
                        <mn>2</mn>
    2433
    1
                    </msub>
    2434
    1
                </mrow>
    2435
    1
            </math>";
    2436
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2437
    1
        }
    2438
    2439
        #[test]
    2440
    1
        fn ferric_chloride_aq() {
    2441
    1
            let test = "<math><mrow>
    2442
    1
                <mi>Fe</mi>
    2443
    1
                <msub><mi>Cl</mi><mn>3</mn></msub>
    2444
    1
                <mrow><mo>(</mo><mrow><mi>aq</mi></mrow><mo>)</mo></mrow>
    2445
    1
            </mrow></math>";
    2446
    1
            let target = "<math>
    2447
    1
                <mrow data-chem-formula='11'>
    2448
    1
                    <mi data-chem-element='3'>Fe</mi>
    2449
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2450
    1
                    <msub data-chem-formula='3'>
    2451
    1
                        <mi data-chem-element='3'>Cl</mi>
    2452
    1
                        <mn>3</mn>
    2453
    1
                    </msub>
    2454
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2455
    1
                    <mrow data-chem-formula='3'>
    2456
    1
                        <mo>(</mo>
    2457
    1
                        <mi>aq</mi>
    2458
    1
                        <mo>)</mo>
    2459
    1
                    </mrow>
    2460
    1
                </mrow>
    2461
    1
           </math>";
    2462
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2463
    1
        }
    2464
    2465
        #[test]
    2466
    1
        fn ferric_chloride_aq_as_mi() {
    2467
    1
            let test = "<math><mrow>
    2468
    1
                <mi>Fe</mi>
    2469
    1
                <msub><mi>Cl</mi><mn>3</mn></msub>
    2470
    1
                <mi>(aq)</mi>
    2471
    1
            </mrow></math>";
    2472
    1
            let target = "<math>
    2473
    1
                <mrow data-chem-formula='11'>
    2474
    1
                    <mi data-chem-element='3'>Fe</mi>
    2475
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2476
    1
                    <msub data-chem-formula='3'>
    2477
    1
                        <mi data-chem-element='3'>Cl</mi>
    2478
    1
                        <mn>3</mn>
    2479
    1
                    </msub>
    2480
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2481
    1
                    <mrow data-chem-formula='3'>
    2482
    1
                        <mo>(</mo>
    2483
    1
                        <mi>aq</mi>
    2484
    1
                        <mo>)</mo>
    2485
    1
                    </mrow>
    2486
    1
                </mrow>
    2487
    1
            </math>";
    2488
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2489
    1
        }
    2490
    2491
        #[test]
    2492
    1
        fn chemtype_ammonia() {
    2493
    1
            let test = r#"<math><msub><mi>NH</mi><mn>3</mn></msub></math>"#;
    2494
    1
            let target = " <math>
    2495
    1
                <mrow data-changed='added' data-chem-formula='5'>
    2496
    1
                <mi mathvariant='normal' data-chem-element='1'>N</mi>
    2497
    1
                <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2498
    1
                <msub data-chem-formula='2'>
    2499
    1
                    <mi mathvariant='normal' data-chem-element='1' data-split='true'>H</mi>
    2500
    1
                    <mn>3</mn>
    2501
    1
                </msub>
    2502
    1
                </mrow>
    2503
    1
            </math>";
    2504
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2505
    1
        }
    2506
    2507
        #[test]
    2508
    1
        fn mhchem_ammonia() {
    2509
    1
            let test = r#"<math>
    2510
    1
                <mrow>
    2511
    1
                    <mi data-mjx-auto-op="false">NH</mi>
    2512
    1
                    <msub>
    2513
    1
                        <mpadded width="0">
    2514
    1
                        <mphantom>
    2515
    1
                            <mi>A</mi>
    2516
    1
                        </mphantom>
    2517
    1
                        </mpadded>
    2518
    1
                        <mpadded height="0">
    2519
    1
                        <mn>3</mn>
    2520
    1
                        </mpadded>
    2521
    1
                    </msub>
    2522
    1
                </mrow>
    2523
    1
            </math>"#;
    2524
    1
            let target = "<math>
    2525
    1
                <mrow data-chem-formula='5'>
    2526
    1
                    <mi mathvariant='normal' data-chem-element='1'>N</mi>
    2527
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2528
    1
                    <mmultiscripts data-mjx-auto-op='false' data-chem-formula='2'>
    2529
    1
                    <mi mathvariant='normal' data-mjx-auto-op='false' data-split='true' data-chem-element='1'>H</mi>
    2530
    1
                    <mn>3</mn>
    2531
    1
                    <none></none>
    2532
    1
                    </mmultiscripts>
    2533
    1
                </mrow>
    2534
    1
                </math>";
    2535
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2536
    1
        }
    2537
    2538
        #[test]
    2539
    1
        fn mhchem_so4() {
    2540
    1
            let test = "<math>
    2541
    1
                <mrow>
    2542
    1
                <mi>SO</mi>
    2543
    1
                <msub>
    2544
    1
                    <mpadded width='0'>
    2545
    1
                    <mphantom>
    2546
    1
                        <mi>A</mi>
    2547
    1
                    </mphantom>
    2548
    1
                    </mpadded>
    2549
    1
                    <mpadded height='0'>
    2550
    1
                    <mn>4</mn>
    2551
    1
                    </mpadded>
    2552
    1
                </msub>
    2553
    1
                <msup>
    2554
    1
                    <mpadded width='0'>
    2555
    1
                    <mphantom>
    2556
    1
                        <mi>A</mi>
    2557
    1
                    </mphantom>
    2558
    1
                    </mpadded>
    2559
    1
                    <mrow>
    2560
    1
                    <mn>2</mn>
    2561
    1
                    <mo>&#x2212;</mo>
    2562
    1
                    </mrow>
    2563
    1
                </msup>
    2564
    1
                </mrow>
    2565
    1
            </math>";
    2566
    1
            let target = "<math>
    2567
    1
                <mrow data-chem-formula='7'>
    2568
    1
                    <mi mathvariant='normal' data-chem-element='1'>S</mi>
    2569
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2570
    1
                    <mmultiscripts data-chem-formula='5'>
    2571
    1
                        <mi mathvariant='normal' data-split='true' data-chem-element='1'>O</mi>
    2572
    1
                        <mn>4</mn>
    2573
    1
                        <none/>
    2574
    1
                        <none/>
    2575
    1
                        <mrow data-chem-formula='3'>
    2576
    1
                        <mn>2</mn>
    2577
    1
                        <mo>-</mo>
    2578
    1
                        </mrow>
    2579
    1
                    </mmultiscripts>
    2580
    1
                </mrow>
    2581
    1
           </math>";
    2582
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2583
    1
        }
    2584
    2585
        #[test]
    2586
    1
        fn mhchem_short_ion() {
    2587
    1
            let test = "  <math>
    2588
    1
                    <mrow>
    2589
    1
                    <mi mathvariant='normal'>H</mi>
    2590
    1
                    <msub>
    2591
    1
                        <mpadded width='0'> <mphantom> <mi>A</mi> </mphantom>  </mpadded>
    2592
    1
                        <mpadded height='0'> <mn>3</mn></mpadded>
    2593
    1
                    </msub>
    2594
    1
                    <mi mathvariant='normal'>O</mi>
    2595
    1
                    <msup>
    2596
    1
                        <mpadded width='0'> <mphantom> <mi>A</mi> </mphantom>  </mpadded>
    2597
    1
                        <mo>+</mo>
    2598
    1
                    </msup>
    2599
    1
                    </mrow>
    2600
    1
                </math>";
    2601
    1
            let target = "<math>
    2602
    1
                <mrow data-chem-formula='6'>
    2603
    1
                    <mmultiscripts data-chem-formula='2'>
    2604
    1
                        <mi mathvariant='normal' data-chem-element='2'>H</mi>
    2605
    1
                        <mn>3</mn>
    2606
    1
                        <none></none>
    2607
    1
                    </mmultiscripts>
    2608
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2609
    1
                    <mmultiscripts data-chem-formula='3'>
    2610
    1
                        <mi mathvariant='normal' data-chem-element='2'>O</mi>
    2611
    1
                        <none></none>
    2612
    1
                        <mo>+</mo>
    2613
    1
                    </mmultiscripts>
    2614
    1
                </mrow>
    2615
    1
           </math>";
    2616
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2617
    1
        }
    2618
    2619
        #[test]
    2620
    1
        fn mhchem_ions_and_state() {
    2621
    1
            let test = "<math>
    2622
    1
                <mrow>
    2623
    1
                <mrow>
    2624
    1
                    <mi>Na</mi>
    2625
    1
                </mrow>
    2626
    1
                <msup>
    2627
    1
                    <mrow>
    2628
    1
                    <mrow>
    2629
    1
                        <mpadded width='0'>
    2630
    1
                        <mphantom>
    2631
    1
                            <mi>A</mi>
    2632
    1
                        </mphantom>
    2633
    1
                        </mpadded>
    2634
    1
                    </mrow>
    2635
    1
                    </mrow>
    2636
    1
                    <mrow>
    2637
    1
                    <mo>+</mo>
    2638
    1
                    </mrow>
    2639
    1
                </msup>
    2640
    1
                <mo stretchy='false'>(</mo>
    2641
    1
                <mrow>
    2642
    1
                    <mi>aq</mi>
    2643
    1
                </mrow>
    2644
    1
                <mo stretchy='false'>)</mo>
    2645
    1
                <mrow>
    2646
    1
                    <mi>Cl</mi>
    2647
    1
                </mrow>
    2648
    1
                <msup>
    2649
    1
                    <mrow>
    2650
    1
                    <mrow>
    2651
    1
                        <mpadded width='0'>
    2652
    1
                        <mphantom>
    2653
    1
                            <mi>A</mi>
    2654
    1
                        </mphantom>
    2655
    1
                        </mpadded>
    2656
    1
                    </mrow>
    2657
    1
                    </mrow>
    2658
    1
                    <mrow>
    2659
    1
                    <mo>&#x2212;</mo>
    2660
    1
                    </mrow>
    2661
    1
                </msup>
    2662
    1
                <mspace width='0.111em'></mspace>
    2663
    1
                <mo stretchy='false'>(</mo>
    2664
    1
                <mrow>
    2665
    1
                    <mi>aq</mi>
    2666
    1
                </mrow>
    2667
    1
                <mo stretchy='false'>)</mo>
    2668
    1
                </mrow>
    2669
    1
                </math>";
    2670
    1
            let target = "<math>
    2671
    1
                <mrow data-chem-formula='18'>
    2672
    1
                    <mmultiscripts data-chem-formula='4'>
    2673
    1
                        <mi data-chem-element='3'>Na</mi>
    2674
    1
                        <none></none>
    2675
    1
                        <mo>+</mo>
    2676
    1
                    </mmultiscripts>
    2677
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2678
    1
                    <mrow data-changed='added' data-chem-formula='3'>
    2679
    1
                        <mo stretchy='false'>(</mo>
    2680
    1
                        <mi>aq</mi>
    2681
    1
                        <mo stretchy='false'>)</mo>
    2682
    1
                    </mrow>
    2683
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2684
    1
                    <mmultiscripts data-chem-formula='5'>
    2685
    1
                        <mi data-chem-element='3'>Cl</mi>
    2686
    1
                        <none></none>
    2687
    1
                        <mo>-</mo>
    2688
    1
                    </mmultiscripts>
    2689
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2690
    1
                    <mrow data-changed='added' data-chem-formula='3'>
    2691
    1
                        <mo stretchy='false' data-previous-space-width='0.111'>(</mo>
    2692
    1
                        <mi>aq</mi>
    2693
    1
                        <mo stretchy='false'>)</mo>
    2694
    1
                    </mrow>
    2695
    1
                </mrow>
    2696
    1
            </math>";
    2697
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2698
    1
        }
    2699
    2700
        #[test]
    2701
    1
        fn ethylene_with_colon_bond() {
    2702
    1
            let test = "<math><mrow>
    2703
    1
                    <msub><mi>H</mi><mn>2</mn></msub><mi>C</mi>
    2704
    1
                    <mo>::</mo>
    2705
    1
                    <mi>C</mi><msub><mi>H</mi><mn>2</mn></msub>
    2706
    1
                </mrow></math>";
    2707
    1
            let target = "<math>
    2708
    1
                <mrow data-chem-formula='8'>
    2709
    1
                    <msub data-chem-formula='1'>
    2710
    1
                        <mi data-chem-element='1'>H</mi>
    2711
    1
                        <mn>2</mn>
    2712
    1
                    </msub>
    2713
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2714
    1
                    <mi data-chem-element='1'>C</mi>
    2715
    1
                    <mo data-chemical-bond='true' data-chem-formula-op='1'>∷</mo>
    2716
    1
                    <mi data-chem-element='1'>C</mi>
    2717
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2718
    1
                    <msub data-chem-formula='1'>
    2719
    1
                        <mi data-chem-element='1'>H</mi>
    2720
    1
                        <mn>2</mn>
    2721
    1
                    </msub>
    2722
    1
                </mrow>
    2723
    1
            </math>";
    2724
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2725
    1
        }
    2726
    2727
        #[test]
    2728
    1
        fn mhchem_u238() {
    2729
    1
            let test = "<math>
    2730
    1
            <mrow>
    2731
    1
              <msubsup>
    2732
    1
                <mrow>
    2733
    1
                  <mrow>
    2734
    1
                    <mpadded width='0'>
    2735
    1
                      <mphantom>
    2736
    1
                        <mi>A</mi>
    2737
    1
                      </mphantom>
    2738
    1
                    </mpadded>
    2739
    1
                  </mrow>
    2740
    1
                </mrow>
    2741
    1
                <mrow>
    2742
    1
                  <mrow>
    2743
    1
                    <mpadded height='0' depth='0'>
    2744
    1
                      <mphantom></mphantom>
    2745
    1
                    </mpadded>
    2746
    1
                  </mrow>
    2747
    1
                </mrow>
    2748
    1
                <mrow>
    2749
    1
                  <mrow>
    2750
    1
                    <mpadded height='0' depth='0'>
    2751
    1
                      <mphantom>
    2752
    1
                        <mn>238</mn>
    2753
    1
                      </mphantom>
    2754
    1
                    </mpadded>
    2755
    1
                  </mrow>
    2756
    1
                </mrow>
    2757
    1
              </msubsup>
    2758
    1
              <mspace width='-0.083em' linebreak='nobreak'></mspace>
    2759
    1
              <msubsup>
    2760
    1
                <mrow>
    2761
    1
                  <mrow>
    2762
    1
                    <mpadded width='0'>
    2763
    1
                      <mphantom>
    2764
    1
                        <mi>A</mi>
    2765
    1
                      </mphantom>
    2766
    1
                    </mpadded>
    2767
    1
                  </mrow>
    2768
    1
                </mrow>
    2769
    1
                <mrow>
    2770
    1
                  <mrow>
    2771
    1
                    <mpadded width='0'>
    2772
    1
                      <mphantom>
    2773
    1
                        <mn>2</mn>
    2774
    1
                      </mphantom>
    2775
    1
                    </mpadded>
    2776
    1
                  </mrow>
    2777
    1
                  <mrow>
    2778
    1
                    <mpadded width='0' lspace='-1width'>
    2779
    1
                      <mrow>
    2780
    1
                        <mpadded height='0'></mpadded>
    2781
    1
                      </mrow>
    2782
    1
                    </mpadded>
    2783
    1
                  </mrow>
    2784
    1
                </mrow>
    2785
    1
                <mrow>
    2786
    1
                  <mrow>
    2787
    1
                    <mpadded height='0'>
    2788
    1
                      <mrow>
    2789
    1
                        <mpadded width='0'>
    2790
    1
                          <mphantom>
    2791
    1
                            <mn>2</mn>
    2792
    1
                          </mphantom>
    2793
    1
                        </mpadded>
    2794
    1
                      </mrow>
    2795
    1
                    </mpadded>
    2796
    1
                  </mrow>
    2797
    1
                  <mrow>
    2798
    1
                    <mpadded width='0' lspace='-1width'>
    2799
    1
                      <mn>238</mn>
    2800
    1
                    </mpadded>
    2801
    1
                  </mrow>
    2802
    1
                </mrow>
    2803
    1
              </msubsup>
    2804
    1
              <mrow>
    2805
    1
                <mi mathvariant='normal'>U</mi>
    2806
    1
              </mrow>
    2807
    1
            </mrow>
    2808
    1
          </math>";
    2809
    1
            let target = " <math>
    2810
    1
                <mmultiscripts data-previous-space-width='-0.083' data-chem-formula='5'>
    2811
    1
                    <mi mathvariant='normal' data-chem-element='2'>U</mi>
    2812
    1
                    <mprescripts></mprescripts>
    2813
    1
                    <none></none>
    2814
    1
                    <mn>238</mn>
    2815
    1
                </mmultiscripts>
    2816
    1
             </math>";
    2817
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2818
    1
        }
    2819
    2820
        #[test]
    2821
    1
        fn mhchem_hcl_aq() {
    2822
    1
            let test = "<math>
    2823
    1
            <mrow>
    2824
    1
              <mn>2</mn>
    2825
    1
              <mstyle scriptlevel='0'>
    2826
    1
                <mspace width='0.167em'></mspace>
    2827
    1
              </mstyle>
    2828
    1
              <mrow>
    2829
    1
                <mi>HCl</mi>
    2830
    1
              </mrow>
    2831
    1
              <mspace width='0.111em'></mspace>
    2832
    1
              <mo stretchy='false'>(</mo>
    2833
    1
              <mrow>
    2834
    1
                <mi>aq</mi>
    2835
    1
              </mrow>
    2836
    1
              <mo stretchy='false'>)</mo>
    2837
    1
            </mrow>
    2838
    1
          </math>";
    2839
    1
            let target = "<math>
    2840
    1
                <mrow data-chem-formula='9'>
    2841
    1
                    <mn>2</mn>
    2842
    1
                    <mo data-changed='added' data-chem-formula-op='0'>&#x2062;</mo>
    2843
    1
                    <mrow data-changed='added' data-chem-formula='9'>
    2844
    1
                        <mi mathvariant='normal' data-previous-space-width='0.167' data-chem-element='1'>H</mi>
    2845
    1
                        <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2846
    1
                        <mi data-split='true' data-chem-element='3'>Cl</mi>
    2847
    1
                        <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    2848
    1
                        <mrow data-changed='added' data-chem-formula='3'>
    2849
    1
                        <mo stretchy='false' data-previous-space-width='0.111'>(</mo>
    2850
    1
                        <mi>aq</mi>
    2851
    1
                        <mo stretchy='false'>)</mo>
    2852
    1
                        </mrow>
    2853
    1
                    </mrow>
    2854
    1
                </mrow>
    2855
    1
            </math>";
    2856
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    2857
    1
        }
    2858
    2859
        #[test]
    2860
    1
        fn mhchem_nested_sub() {
    2861
            // from \ce{(CH3)3}
    2862
    1
            let test = "<math>
    2863
    1
            <mrow>
    2864
    1
              <mo stretchy='false'>(</mo>
    2865
    1
              <mrow>
    2866
    1
                <mi>CH</mi>
    2867
    1
              </mrow>
    2868
    1
              <msub>
    2869
    1
                <mrow>
    2870
    1
                  <mrow>
    2871
    1
                    <mpadded width='0'>
    2872
    1
                      <mphantom>
    2873
    1
                        <mi>A</mi>
    2874
    1
                      </mphantom>
    2875
    1
                    </mpadded>
    2876
    1
                  </mrow>
    2877
    1
                </mrow>
    2878
    1
                <mrow>
    2879
    1
                  <mrow>
    2880
    1
                    <mpadded height='0'>
    2881
    1
                      <mn>3</mn>
    2882
    1
                    </mpadded>
    2883
    1
                  </mrow>
    2884
    1
                </mrow>
    2885
    1
              </msub>
    2886
    1
              <mo stretchy='false'>)</mo>
    2887
    1
              <msub>
    2888
    1
                <mrow>
    2889
    1
                  <mrow>
    2890
    1
                    <mpadded width='0'>
    2891
    1
                      <mphantom>
    2892
    1
                        <mi>A</mi>
    2893
    1
                      </mphantom>
    2894
    1
                    </mpadded>
    2895
    1
                  </mrow>
    2896
    1
                </mrow>
    2897
    1
                <mrow>
    2898
    1
                  <mrow>
    2899
    1
                    <mpadded height='0'>
    2900
    1
                      <mn>3</mn>
    2901
    1
                    </mpadded>
    2902
    1
                  </mrow>
    2903
    1
                </mrow>
    2904
    1
              </msub>
    2905
    1
            </mrow>
    2906
    1
          </math>";
    2907
    1
        let target = "<math>
    2908
    1
            <mmultiscripts data-chem-formula='8'>
    2909
    1
                <mrow data-changed='added' data-chem-formula='8'>
    2910
    1
                    <mo stretchy='false'>(</mo>
    2911
    1
                    <mrow data-changed='added' data-chem-formula='5'>
    2912
    1
                    <mi mathvariant='normal' data-chem-element='1'>C</mi>
    2913
    1
                    <mo data-changed='added'>&#x2063;</mo>
    2914
    1
                    <mmultiscripts data-chem-formula='2'>
    2915
    1
                        <mi mathvariant='normal' data-split='true' data-chem-element='1'>H</mi>
    2916
    1
                        <mn>3</mn>
    2917
    1
                        <none></none>
    2918
    1
                    </mmultiscripts>
    2919
    1
                    </mrow>
    2920
    1
                    <mo stretchy='false'>)</mo>
    2921
    1
                </mrow>
    2922
    1
                <mn>3</mn>
    2923
    1
                <none></none>
    2924
    1
            </mmultiscripts>
    2925
    1
        </math>";
    2926
    1
        assert!(are_strs_canonically_equal(test, target, &[]));
    2927
    1
        }
    2928
    2929
        #[test]
    2930
    1
        fn mhchem_isotopes() {
    2931
            // from \ce{^{18}O{}^{16}O}
    2932
    1
            let test = "<math>
    2933
    1
            <mrow>
    2934
    1
              <msubsup>
    2935
    1
                <mpadded width='0'>
    2936
    1
                  <mphantom>
    2937
    1
                    <mi>A</mi>
    2938
    1
                  </mphantom>
    2939
    1
                </mpadded>
    2940
    1
                <mpadded height='0' depth='0'>
    2941
    1
                  <mphantom></mphantom>
    2942
    1
                </mpadded>
    2943
    1
                <mpadded height='0' depth='0'>
    2944
    1
                  <mphantom>
    2945
    1
                    <mn>18</mn>
    2946
    1
                  </mphantom>
    2947
    1
                </mpadded>
    2948
    1
              </msubsup>
    2949
    1
              <mspace width='-0.083em'></mspace>
    2950
    1
              <msubsup>
    2951
    1
                <mpadded width='0'>
    2952
    1
                  <mphantom>
    2953
    1
                    <mi>A</mi>
    2954
    1
                  </mphantom>
    2955
    1
                </mpadded>
    2956
    1
                <mrow>
    2957
    1
                  <mpadded width='0'>
    2958
    1
                    <mphantom>
    2959
    1
                      <mn>2</mn>
    2960
    1
                    </mphantom>
    2961
    1
                  </mpadded>
    2962
    1
                  <mpadded width='0' lspace='-1width'>
    2963
    1
                    <mpadded height='0'></mpadded>
    2964
    1
                  </mpadded>
    2965
    1
                </mrow>
    2966
    1
                <mrow>
    2967
    1
                  <mpadded height='0'>
    2968
    1
                    <mpadded width='0'>
    2969
    1
                      <mphantom>
    2970
    1
                        <mn>2</mn>
    2971
    1
                      </mphantom>
    2972
    1
                    </mpadded>
    2973
    1
                  </mpadded>
    2974
    1
                  <mpadded width='0' lspace='-1width'>
    2975
    1
                    <mn>18</mn>
    2976
    1
                  </mpadded>
    2977
    1
                </mrow>
    2978
    1
              </msubsup>
    2979
    1
              <mi mathvariant='normal'>O</mi>
    2980
    1
              <mspace width='0.111em'></mspace>
    2981
    1
              <msubsup>
    2982
    1
                <mpadded width='0'>
    2983
    1
                  <mphantom>
    2984
    1
                    <mi>A</mi>
    2985
    1
                  </mphantom>
    2986
    1
                </mpadded>
    2987
    1
                <mpadded height='0' depth='0'>
    2988
    1
                  <mphantom></mphantom>
    2989
    1
                </mpadded>
    2990
    1
                <mpadded height='0' depth='0'>
    2991
    1
                  <mphantom>
    2992
    1
                    <mn>16</mn>
    2993
    1
                  </mphantom>
    2994
    1
                </mpadded>
    2995
    1
              </msubsup>
    2996
    1
              <mspace width='-0.083em'></mspace>
    2997
    1
              <msubsup>
    2998
    1
                <mpadded width='0'>
    2999
    1
                  <mphantom>
    3000
    1
                    <mi>A</mi>
    3001
    1
                  </mphantom>
    3002
    1
                </mpadded>
    3003
    1
                <mrow>
    3004
    1
                  <mpadded width='0'>
    3005
    1
                    <mphantom>
    3006
    1
                      <mn>2</mn>
    3007
    1
                    </mphantom>
    3008
    1
                  </mpadded>
    3009
    1
                  <mpadded width='0' lspace='-1width'>
    3010
    1
                    <mpadded height='0'></mpadded>
    3011
    1
                  </mpadded>
    3012
    1
                </mrow>
    3013
    1
                <mrow>
    3014
    1
                  <mpadded height='0'>
    3015
    1
                    <mpadded width='0'>
    3016
    1
                      <mphantom>
    3017
    1
                        <mn>2</mn>
    3018
    1
                      </mphantom>
    3019
    1
                    </mpadded>
    3020
    1
                  </mpadded>
    3021
    1
                  <mpadded width='0' lspace='-1width'>
    3022
    1
                    <mn>16</mn>
    3023
    1
                  </mpadded>
    3024
    1
                </mrow>
    3025
    1
              </msubsup>
    3026
    1
              <mi mathvariant='normal'>O</mi>
    3027
    1
            </mrow>
    3028
    1
          </math>";
    3029
    1
        let target = "<math>
    3030
    1
            <mrow data-chem-formula='11'>
    3031
    1
                <mmultiscripts data-previous-space-width='-0.083' data-chem-formula='5'>
    3032
    1
                    <mi mathvariant='normal' data-chem-element='2'>O</mi>
    3033
    1
                    <mprescripts></mprescripts>
    3034
    1
                    <none></none>
    3035
    1
                    <mn>18</mn>
    3036
    1
                </mmultiscripts>
    3037
    1
                <mo data-changed='added' data-chem-formula-op='0'>&#x2063;</mo>
    3038
    1
                <mmultiscripts data-previous-space-width='0.027999999999999997' data-chem-formula='5'>
    3039
    1
                    <mi mathvariant='normal' data-chem-element='2'>O</mi>
    3040
    1
                    <mprescripts></mprescripts>
    3041
    1
                    <none></none>
    3042
    1
                    <mn>16</mn>
    3043
    1
                </mmultiscripts>
    3044
    1
            </mrow>
    3045
    1
        </math>";
    3046
    1
        assert!(are_strs_canonically_equal(test, target, &[]));
    3047
    1
        }
    3048
    3049
    3050
        #[test]
    3051
    1
        fn merge_bug_274() {
    3052
    1
            let test = r#"
    3053
    1
            <math>
    3054
    1
                <mrow>
    3055
    1
                    <mtable>
    3056
    1
                        <mtr>
    3057
    1
                            <mtd>
    3058
    1
                                <mrow>
    3059
    1
                                    <msub><mtext>H</mtext><mn>2</mn></msub>
    3060
    1
                                    <mtext>g</mtext>
    3061
    1
                                    <mtext/>
    3062
    1
                                    <mtext>+</mtext>
    3063
    1
                                    <mtext/>
    3064
    1
                                    <msub><mrow><mtext>Cl</mtext></mrow><mn>2</mn></msub>
    3065
    1
                                    <mo stretchy="false">(</mo>
    3066
    1
                                    <mtext>g</mtext>
    3067
    1
                                    <mo stretchy="false">)</mo>
    3068
    1
                                    <mo>&#x2192;</mo>
    3069
    1
                                    <mn>2</mn>
    3070
    1
                                    <mtext>HCl(g)</mtext>
    3071
    1
                                </mrow>
    3072
    1
                            </mtd>
    3073
    1
                        </mtr>
    3074
    1
                        <mtr>
    3075
    1
                            <mtd>
    3076
    1
                                <mrow>
    3077
    1
                                    <mn>1</mn>
    3078
    1
                                    <mo>:</mo>
    3079
    1
                                    <mn>1</mn>
    3080
    1
                                    <mo>:</mo>
    3081
    1
                                    <mn>2</mn>
    3082
    1
                                </mrow>
    3083
    1
                            </mtd>
    3084
    1
                        </mtr>
    3085
    1
                        <mtr>
    3086
    1
                            <mtd>
    3087
    1
                                <mrow>
    3088
    1
                                    <mn>1</mn>
    3089
    1
                                    <mtext/>
    3090
    1
                                    <msub><mtext>H</mtext><mn>2</mn></msub>
    3091
    1
                                    <mtext/>
    3092
    1
                                    <mtext>to</mtext>
    3093
    1
                                    <mtext/>
    3094
    1
                                    <mn>1</mn>
    3095
    1
                                    <mtext/>
    3096
    1
                                    <msub><mrow><mtext>Cl</mtext></mrow><mn>2</mn></msub>
    3097
    1
                                    <mtext/>
    3098
    1
                                    <mtext>to</mtext>
    3099
    1
                                    <mtext/>
    3100
    1
                                    <mtext>2</mtext>
    3101
    1
                                    <mtext/>
    3102
    1
                                    <mtext>HCl</mtext>
    3103
    1
                                </mrow>
    3104
    1
                            </mtd>
    3105
    1
                        </mtr>
    3106
    1
                    </mtable>
    3107
    1
                </mrow>
    3108
    1
            </math>
    3109
    1
            "#;
    3110
    1
            let target = "
    3111
    1
                <math>
    3112
    1
                <mtable>
    3113
    1
                    <mtr>
    3114
    1
                    <mtd data-maybe-chemistry='9'>
    3115
    1
                        <mrow data-maybe-chemistry='9'>
    3116
    1
                        <mrow data-changed='added' data-maybe-chemistry='8'>
    3117
    1
                            <mrow data-changed='added' data-maybe-chemistry='1'>
    3118
    1
                            <msub data-maybe-chemistry='1'>
    3119
    1
                                <mtext data-maybe-chemistry='1'>H</mtext>
    3120
    1
                                <mn>2</mn>
    3121
    1
                            </msub>
    3122
    1
                            <mo data-changed='added' data-maybe-chemistry='0'>&#x2062;</mo>
    3123
    1
                            <mtext data-maybe-chemistry='0'>g</mtext>
    3124
    1
                            </mrow>
    3125
    1
                            <mo data-chem-equation-op='1' data-maybe-chemistry='1'>+</mo>
    3126
    1
                            <mrow data-changed='added' data-maybe-chemistry='6'>
    3127
    1
                            <msub data-maybe-chemistry='3'>
    3128
    1
                                <mtext data-maybe-chemistry='3'>Cl</mtext>
    3129
    1
                                <mn>2</mn>
    3130
    1
                            </msub>
    3131
    1
                            <mo data-changed='added' data-maybe-chemistry='0'>&#x2063;</mo>
    3132
    1
                            <mrow data-changed='added' data-maybe-chemistry='2'>
    3133
    1
                                <mo stretchy='false'>(</mo>
    3134
    1
                                <mtext>g</mtext>
    3135
    1
                                <mo stretchy='false'>)</mo>
    3136
    1
                            </mrow>
    3137
    1
                            </mrow>
    3138
    1
                        </mrow>
    3139
    1
                        <mo data-chem-equation-op='1' data-maybe-chemistry='1'>→</mo>
    3140
    1
                        <mrow data-changed='added' data-maybe-chemistry='0'>
    3141
    1
                            <mn data-maybe-chemistry='0'>2</mn>
    3142
    1
                            <mo data-changed='added' data-maybe-chemistry='0'>&#x2062;</mo>
    3143
    1
                            <mtext data-maybe-chemistry='0'>HCl(g)</mtext>
    3144
    1
                        </mrow>
    3145
    1
                        </mrow>
    3146
    1
                    </mtd>
    3147
    1
                    </mtr>
    3148
    1
                    <mtr>
    3149
    1
                    <mtd>
    3150
    1
                        <mrow>
    3151
    1
                        <mn>1</mn>
    3152
    1
                        <mo>:</mo>
    3153
    1
                        <mn>1</mn>
    3154
    1
                        <mo>:</mo>
    3155
    1
                        <mn>2</mn>
    3156
    1
                        </mrow>
    3157
    1
                    </mtd>
    3158
    1
                    </mtr>
    3159
    1
                    <mtr>
    3160
    1
                    <mtd data-maybe-chemistry='7'>
    3161
    1
                        <mrow data-maybe-chemistry='7'>
    3162
    1
                        <mn data-maybe-chemistry='0'>1</mn>
    3163
    1
                        <mo data-changed='added' data-maybe-chemistry='0'>&#x2062;</mo>
    3164
    1
                        <msub data-maybe-chemistry='1'>
    3165
    1
                            <mtext data-maybe-chemistry='1'>H</mtext>
    3166
    1
                            <mn>2</mn>
    3167
    1
                        </msub>
    3168
    1
                        <mo data-changed='added' data-maybe-chemistry='0'>&#x2062;</mo>
    3169
    1
                        <mtext data-maybe-chemistry='0'>to</mtext>
    3170
    1
                        <mo data-changed='added' data-maybe-chemistry='0'>&#x2062;</mo>
    3171
    1
                        <mn data-maybe-chemistry='0'>1</mn>
    3172
    1
                        <mo data-changed='added' data-maybe-chemistry='0'>&#x2062;</mo>
    3173
    1
                        <msub data-maybe-chemistry='3'>
    3174
    1
                            <mtext data-maybe-chemistry='3'>Cl</mtext>
    3175
    1
                            <mn>2</mn>
    3176
    1
                        </msub>
    3177
    1
                        <mo data-changed='added' data-maybe-chemistry='0'>&#x2062;</mo>
    3178
    1
                        <mtext data-maybe-chemistry='0'>to</mtext>
    3179
    1
                        <mo data-changed='added' data-maybe-chemistry='0'>&#x2062;</mo>
    3180
    1
                        <mn data-maybe-chemistry='0'>2</mn>
    3181
    1
                        <mo data-changed='added' data-maybe-chemistry='0'>&#x2062;</mo>
    3182
    1
                        <mi data-maybe-chemistry='1' mathvariant='normal'>H</mi>
    3183
    1
                        <mo data-changed='added' data-maybe-chemistry='0'>&#x2062;</mo>
    3184
    1
                        <mi data-maybe-chemistry='3' data-split='true'>Cl</mi>
    3185
    1
                        </mrow>
    3186
    1
                    </mtd>
    3187
    1
                    </mtr>
    3188
    1
                </mtable>
    3189
    1
                </math>
    3190
    1
            ";
    3191
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    3192
    1
        }
    3193
    3194
        #[test]
    3195
    1
        fn merge_bug_303() {
    3196
    1
            let test = r#"
    3197
    1
                <math>
    3198
    1
                    <mn>2</mn>
    3199
    1
                    <msup><mtext>OH</mtext><mo>−</mo></msup>
    3200
    1
                    <mo stretchy="false">(</mo>
    3201
    1
                    <mtext>aq</mtext>
    3202
    1
                    <mo stretchy="false">)</mo>
    3203
    1
                    <mo>+</mo>
    3204
    1
                    <mtext>C</mtext>
    3205
    1
                    <msup><mtext>u</mtext><mrow><mn>2</mn><mo>+</mo></mrow></msup>
    3206
    1
                </math>
    3207
    1
            "#;
    3208
    1
            let target = "
    3209
    1
                <math>
    3210
    1
                    <mrow data-changed='added'>
    3211
    1
                    <mrow data-changed='added'>
    3212
    1
                        <mn>2</mn>
    3213
    1
                        <mo data-changed='added'>&#x2062;</mo>
    3214
    1
                        <mrow data-changed='added'>
    3215
    1
                            <msup><mi>OH</mi><mo>-</mo></msup>
    3216
    1
                            <mo data-changed='added'>&#x2061;</mo>
    3217
    1
                            <mrow data-changed='added'>
    3218
    1
                                <mo stretchy='false'>(</mo>
    3219
    1
                                <mtext>aq</mtext>
    3220
    1
                                <mo stretchy='false'>)</mo>
    3221
    1
                            </mrow>
    3222
    1
                        </mrow>
    3223
    1
                    </mrow>
    3224
    1
                    <mo>+</mo>
    3225
    1
                    <mrow data-changed='added'>
    3226
    1
                        <mtext>C</mtext>
    3227
    1
                        <mo data-changed='added'>&#x2062;</mo>
    3228
    1
                        <msup> <mtext>u</mtext> <mrow><mn>2</mn><mo>+</mo></mrow> </msup>
    3229
    1
                    </mrow>
    3230
    1
                    </mrow>
    3231
    1
                </math>
    3232
    1
               ";
    3233
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    3234
    1
        }
    3235
    3236
        #[test]
    3237
    1
        fn mtd_assert_bug_393() {
    3238
    1
            let test = r#"
    3239
    1
            <math display="block">
    3240
    1
                <mtable>
    3241
    1
                    <mtr>
    3242
    1
                    <mtd>
    3243
    1
                        <mrow>
    3244
    1
                        <mi>A</mi>
    3245
    1
                        <mi>c</mi>
    3246
    1
                        </mrow>
    3247
    1
                    </mtd>
    3248
    1
                    <mtd>
    3249
    1
                        <mi>A</mi>
    3250
    1
                        <mfenced>
    3251
    1
                        <mtable>
    3252
    1
                            <mtr>
    3253
    1
                            <mtd>
    3254
    1
                                <mrow>
    3255
    1
                                <mi>c</mi>
    3256
    1
                                <mi>n</mi>
    3257
    1
                                </mrow>
    3258
    1
                            </mtd>
    3259
    1
                            </mtr>
    3260
    1
                        </mtable>
    3261
    1
                        </mfenced>
    3262
    1
                    </mtd>
    3263
    1
                    </mtr>
    3264
    1
                </mtable>
    3265
    1
            </math>"#;
    3266
    1
            let target = "
    3267
    1
            <math display='block'>
    3268
    1
                <mtable>
    3269
    1
                <mtr>
    3270
    1
                    <mtd>
    3271
    1
                    <mi>A</mi>
    3272
    1
                    <mi>c</mi>
    3273
    1
                    </mtd>
    3274
    1
                    <mtd>
    3275
    1
                    <mrow data-changed='added'>
    3276
    1
                        <mi>A</mi>
    3277
    1
                        <mrow>
    3278
    1
                        <mo data-changed='from_mfenced'>(</mo>
    3279
    1
                        <mtable>
    3280
    1
                            <mtr>
    3281
    1
                            <mtd>
    3282
    1
                                <mrow>
    3283
    1
                                <mi>c</mi>
    3284
    1
                                <mi>n</mi>
    3285
    1
                                </mrow>
    3286
    1
                            </mtd>
    3287
    1
                            </mtr>
    3288
    1
                        </mtable>
    3289
    1
                        <mo data-changed='from_mfenced'>)</mo>
    3290
    1
                        </mrow>
    3291
    1
                    </mrow>
    3292
    1
                    </mtd>
    3293
    1
                </mtr>
    3294
    1
                </mtable>
    3295
    1
            </math>";
    3296
    1
            assert!(are_strs_canonically_equal(test, target, &[]));
    3297
    1
        }
    3298
    3299
    }