Coverage Report

Created: 2026-05-25 08:50

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