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