/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(¤t_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(¤t_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(¤t_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}' => "⁡", |
4559 | 0 | '\u{2062}' => "⁢", |
4560 | 0 | '\u{2063}' => "⁣", |
4561 | 0 | '\u{2064}' => "⁤", |
4562 | 0 | '\u{E000}' => "", |
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=𝟗</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'>⁡</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'>⁡</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'>⁡</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'>⁡</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><mi/></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><mi/></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>⁡</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'>⁢</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'>⁢</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'>⁡</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'>⁡</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'>⁡</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'>⁢</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'>⁡</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'>⁡</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'>⁢</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'>⁡</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'>⁡</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'>⁢</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'>⁢</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'>⁡</mo> |
5291 | 1 | <mrow data-changed='added'> <mo>(</mo> <mi>x</mi> <mo>)</mo> </mrow> |
5292 | 1 | </mrow> |
5293 | 1 | <mo data-changed='added'>⁢</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'>⁤</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'>⁤</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'>⁤</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'>⁣</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'>⁢</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'>⁢</mo> |
5413 | 1 | <mi>y</mi> |
5414 | 1 | <mo data-changed='added'>⁢</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'>⁢</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'>⁢</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>∈</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'>⁡</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'>⁡</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'>⁡</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'>⁡</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'>⁡</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'>⁡</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'>⁡</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'>⁢</mo> |
5579 | 1 | <mi>π</mi> |
5580 | 1 | <mo data-changed='added'>⁢</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'>⁡</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'>⁢</mo> |
5603 | 1 | <mi>π</mi> |
5604 | 1 | <mo data-changed='added'>⁢</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'>⁡</mo> |
5618 | 1 | <mrow data-changed='added'> |
5619 | 1 | <mn>2</mn> |
5620 | 1 | <mo data-changed='added'>⁢</mo> |
5621 | 1 | <mi>π</mi> |
5622 | 1 | <mo data-changed='added'>⁢</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'>⁡</mo> |
5639 | 1 | <mn>2</mn> |
5640 | 1 | </mrow> |
5641 | 1 | <mo data-changed='added'>⁢</mo> |
5642 | 1 | <mi>π</mi> |
5643 | 1 | <mo data-changed='added'>⁢</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'>⁡</mo> |
5658 | 1 | <mi>x</mi> |
5659 | 1 | </mrow> |
5660 | 1 | <mo data-changed='added'>⁢</mo> |
5661 | 1 | <mrow data-changed='added'> |
5662 | 1 | <mi>cos</mi> |
5663 | 1 | <mo data-changed='added'>⁡</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'>⁢</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'>⁢</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'>⁢</mo> |
5712 | 1 | <mn>188,23</mn> |
5713 | 1 | <mo data-changed='added'>⁢</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'>⁢</mo> |
5727 | 1 | <mi>€</mi> |
5728 | 1 | <mo data-changed='added'>⁢</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>  </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>  </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>  </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'>⁢</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>  </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'>⁡</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> </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'>⁢</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> </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> </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'>⁡</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> </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'> </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> </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=' <annotation-xml encoding='MathML-Content'> |
5915 | 1 | <ci>𝑧</ci> |
5916 | 1 | </annotation-xml> |
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'>⁣</mo> |
5931 | 1 | <mi>B</mi> |
5932 | 1 | <mo data-changed='added'>⁣</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 </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'>⁡</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'>⁡</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'>⁢</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>…</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'>⁢</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'>⁢</mo> |
6336 | 1 | <msup> |
6337 | 1 | <mn>5</mn> |
6338 | 1 | <mo>″</mo> |
6339 | 1 | </msup> |
6340 | 1 | <mo data-changed='added'>⁢</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'>⁢</mo> |
6362 | 1 | <msup> |
6363 | 1 | <mn>5</mn> |
6364 | 1 | <mo>⁗</mo> |
6365 | 1 | </msup> |
6366 | 1 | <mo data-changed='added'>⁢</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'>⁢</mo> |
6395 | 1 | <mi>x</mi> |
6396 | 1 | <mo data-changed='added'>⁢</mo> |
6397 | 1 | <mi>y</mi> |
6398 | 1 | <mo data-changed='added'>⁢</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'>⁢</mo> |
6405 | 1 | <mi>a</mi> |
6406 | 1 | <mo data-changed='added'>⁢</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'>⁢</mo> |
6413 | 1 | <mi>ω</mi> |
6414 | 1 | <mo data-changed='added'>⁢</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'>⁡</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'>⁡</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'>⁢</mo> |
6458 | 1 | <mi>a</mi> |
6459 | 1 | <mo data-changed='added'>⁢</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'>⁤</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'>⁡</mo> |
6661 | 1 | <msup data-changed='added'><mn>30</mn><mo>°</mo></msup> |
6662 | 1 | </mrow> |
6663 | 1 | <mo data-changed='added'>⁢</mo> |
6664 | 1 | <mrow data-changed='added'> |
6665 | 1 | <mi>sin</mi> |
6666 | 1 | <mo data-changed='added'>⁡</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'>⁡</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>⁡</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'>⁢</mo> |
6843 | 1 | <mrow> |
6844 | 1 | <mi>sin</mi> |
6845 | 1 | <mo>⁡</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>𝒙</mi> |
6861 | 1 | <mo>=</mo> |
6862 | 1 | <mrow> |
6863 | 1 | <mrow> |
6864 | 1 | <mi>𝒄</mi> |
6865 | 1 | <mi>𝒐</mi> |
6866 | 1 | <mi>𝒔</mi> |
6867 | 1 | </mrow> |
6868 | 1 | <mo>⁡</mo> |
6869 | 1 | <mrow> |
6870 | 1 | <mi>𝒕</mi> |
6871 | 1 | </mrow> |
6872 | 1 | </mrow> |
6873 | 1 | <mo>+</mo> |
6874 | 1 | <mn>𝟐</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>⁡</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 | | } |