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
=>
{852
mathml852
.
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 &&  
&11
convert_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
}