1#![allow(non_snake_case)]
4#![allow(clippy::needless_return)]
5use std::cell::RefCell;
6
7use crate::canonicalize::{as_text, create_mathml_element};
8use crate::errors::*;
9use phf::phf_map;
10use regex::{Captures, Regex};
11use sxd_document::dom::*;
12use sxd_document::parser;
13use sxd_document::Package;
14
15use crate::canonicalize::{as_element, name};
16use crate::shim_filesystem::{find_all_dirs_shim, find_files_in_dir_that_ends_with_shim};
17
18use crate::navigate::*;
19use crate::pretty_print::mml_to_string;
20use crate::xpath_functions::{is_leaf, IsNode};
21
22#[cfg(feature = "enable-logs")]
23use std::sync::Once;
24#[cfg(feature = "enable-logs")]
25static INIT: Once = Once::new();
26
27fn enable_logs() {
28 #[cfg(feature = "enable-logs")]
29 INIT.call_once(||{
30 #[cfg(target_os = "android")]
31 {
32 extern crate log;
33 extern crate android_logger;
34
35 use log::*;
36 use android_logger::*;
37
38 android_logger::init_once(
39 Config::default()
40 .with_max_level(LevelFilter::Trace)
41 .with_tag("MathCat")
42 );
43 trace!("Activated Android logger!");
44 }
45 });
46}
47
48fn cleanup_mathml(mathml: Element) -> Result<Element> {
50 trim_element(mathml, false);
51 let mathml = crate::canonicalize::canonicalize(mathml)?;
52 let mathml = add_ids(mathml);
53 return Ok(mathml);
54}
55
56thread_local! {
57 pub static MATHML_INSTANCE: RefCell<Package> = init_mathml_instance();
59}
60
61fn init_mathml_instance() -> RefCell<Package> {
62 let package = parser::parse("<math></math>")
63 .expect("Internal error in 'init_mathml_instance;: didn't parse initializer string");
64 return RefCell::new(package);
65}
66
67pub fn set_rules_dir(dir: String) -> Result<()> {
70 enable_logs();
71 use std::path::PathBuf;
72 let dir = if dir.is_empty() {
73 std::env::var_os("MathCATRulesDir")
74 .unwrap_or_default()
75 .to_str()
76 .unwrap()
77 .to_string()
78 } else {
79 dir
80 };
81 let pref_manager = crate::prefs::PreferenceManager::get();
82 return pref_manager.borrow_mut().initialize(PathBuf::from(dir));
83}
84
85pub fn get_version() -> String {
87 enable_logs();
88 const VERSION: &str = env!("CARGO_PKG_VERSION");
89 return VERSION.to_string();
90}
91
92pub fn set_mathml(mathml_str: String) -> Result<String> {
96 enable_logs();
97 lazy_static! {
98 static ref MATHJAX_V2: Regex = Regex::new(r#"class *= *['"]MJX-.*?['"]"#).unwrap();
100 static ref MATHJAX_V3: Regex = Regex::new(r#"class *= *['"]data-mjx-.*?['"]"#).unwrap();
101 static ref NAMESPACE_DECL: Regex = Regex::new(r#"xmlns:[[:alpha:]]+"#).unwrap(); static ref PREFIX: Regex = Regex::new(r#"(</?)[[:alpha:]]+:"#).unwrap(); static ref HTML_ENTITIES: Regex = Regex::new(r#"&([a-zA-Z]+?);"#).unwrap();
104 }
105
106 NAVIGATION_STATE.with(|nav_stack| {
107 nav_stack.borrow_mut().reset();
108 });
109
110 crate::speech::SPEECH_RULES.with(|rules| rules.borrow_mut().read_files())?;
113
114 return MATHML_INSTANCE.with(|old_package| {
115 static HTML_ENTITIES_MAPPING: phf::Map<&str, &str> = include!("entities.in");
116
117 let mut error_message = "".to_string(); let mathml_str =
120 HTML_ENTITIES.replace_all(&mathml_str, |cap: &Captures| match HTML_ENTITIES_MAPPING.get(&cap[1]) {
121 None => {
122 error_message = format!("No entity named '{}'", &cap[0]);
123 cap[0].to_string()
124 }
125 Some(&ch) => ch.to_string(),
126 });
127
128 if !error_message.is_empty() {
129 bail!(error_message);
130 }
131 let mathml_str = MATHJAX_V2.replace_all(&mathml_str, "");
132 let mathml_str = MATHJAX_V3.replace_all(&mathml_str, "");
133
134 let mathml_str = NAMESPACE_DECL.replace(&mathml_str, "xmlns"); let mathml_str = PREFIX.replace_all(&mathml_str, "$1");
139
140 let new_package = parser::parse(&mathml_str);
141 if let Err(e) = new_package {
142 bail!("Invalid MathML input:\n{}\nError is: {}", &mathml_str, &e.to_string());
143 }
144
145 let new_package = new_package.unwrap();
146 let mathml = get_element(&new_package);
147 let mathml = cleanup_mathml(mathml)?;
148 let mathml_string = mml_to_string(mathml);
149 old_package.replace(new_package);
150
151 return Ok(mathml_string);
152 });
153}
154
155pub fn get_spoken_text() -> Result<String> {
158 enable_logs();
159 return MATHML_INSTANCE.with(|package_instance| {
162 let package_instance = package_instance.borrow();
163 let mathml = get_element(&package_instance);
164 let new_package = Package::new();
165 let intent = crate::speech::intent_from_mathml(mathml, new_package.as_document())?;
166 debug!("Intent tree:\n{}", mml_to_string(intent));
167 let speech = crate::speech::speak_mathml(intent, "")?;
168 return Ok(speech);
170 });
171}
172
173pub fn get_overview_text() -> Result<String> {
177 enable_logs();
178 return MATHML_INSTANCE.with(|package_instance| {
181 let package_instance = package_instance.borrow();
182 let mathml = get_element(&package_instance);
183 let speech = crate::speech::overview_mathml(mathml, "")?;
184 return Ok(speech);
186 });
187}
188
189pub fn get_preference(name: String) -> Result<String> {
192 enable_logs();
193 use crate::prefs::NO_PREFERENCE;
194 return crate::speech::SPEECH_RULES.with(|rules| {
195 let rules = rules.borrow();
196 let pref_manager = rules.pref_manager.borrow();
197 let mut value = pref_manager.pref_to_string(&name);
198 if value == NO_PREFERENCE {
199 value = pref_manager.pref_to_string(&name);
200 }
201 if value == NO_PREFERENCE {
202 bail!("No preference named '{}'", &name);
203 } else {
204 return Ok(value);
205 }
206 });
207}
208
209pub fn set_preference(name: String, value: String) -> Result<()> {
230 enable_logs();
231 let mut value = value;
233 if name == "Language" || name == "LanguageAuto" {
234 if value != "Auto" {
236 let mut lang_country_split = value.split('-');
238 let language = lang_country_split.next().unwrap_or("");
239 let country = lang_country_split.next().unwrap_or("");
240 if language.len() != 2 {
241 bail!(
242 "Improper format for 'Language' preference '{}'. Should be of form 'en' or 'en-gb'",
243 value
244 );
245 }
246 let mut new_lang_country = language.to_string(); if !country.is_empty() {
248 new_lang_country.push('-');
249 new_lang_country.push_str(country);
250 }
251 value = new_lang_country;
252 }
253 if name == "LanguageAuto" && value == "Auto" {
254 bail!("'LanguageAuto' can not have the value 'Auto'");
255 }
256 }
257
258 crate::speech::SPEECH_RULES.with(|rules| {
259 let rules = rules.borrow_mut();
260 if let Some(error_string) = rules.get_error() {
261 bail!("{}", error_string);
262 }
263
264 let mut pref_manager = rules.pref_manager.borrow_mut();
266 if name == "LanguageAuto" {
267 let language_pref = pref_manager.pref_to_string("Language");
268 if language_pref != "Auto" {
269 bail!(
270 "'LanguageAuto' can only be used when 'Language' has the value 'Auto'; Language={}",
271 language_pref
272 );
273 }
274 }
275 let lower_case_value = value.to_lowercase();
276 if lower_case_value == "true" || lower_case_value == "false" {
277 pref_manager.set_api_boolean_pref(&name, value.to_lowercase() == "true");
278 } else {
279 match name.as_str() {
280 "Pitch" | "Rate" | "Volume" | "CapitalLetters_Pitch" | "MathRate" | "PauseFactor" => {
281 pref_manager.set_api_float_pref(&name, to_float(&name, &value)?)
282 }
283 _ => {
284 pref_manager.set_string_pref(&name, &value)?;
285 }
286 }
287 };
288 return Ok::<(), Error>(());
289 })?;
290
291 return Ok(());
292
293 fn to_float(name: &str, value: &str) -> Result<f64> {
294 return match value.parse::<f64>() {
295 Ok(val) => Ok(val),
296 Err(_) => bail!("SetPreference: preference'{}'s value '{}' must be a float", name, value),
297 };
298 }
299}
300
301pub fn get_braille(nav_node_id: String) -> Result<String> {
305 enable_logs();
306 return MATHML_INSTANCE.with(|package_instance| {
309 let package_instance = package_instance.borrow();
310 let mathml = get_element(&package_instance);
311 let braille = crate::braille::braille_mathml(mathml, &nav_node_id)?.0;
312 return Ok(braille);
314 });
315}
316
317pub fn get_navigation_braille() -> Result<String> {
321 enable_logs();
322 return MATHML_INSTANCE.with(|package_instance| {
323 let package_instance = package_instance.borrow();
324 let mathml = get_element(&package_instance);
325 let new_package = Package::new(); let new_doc = new_package.as_document();
327 let nav_mathml = NAVIGATION_STATE.with(|nav_stack| {
328 return match nav_stack.borrow_mut().get_navigation_mathml(mathml) {
329 Err(e) => Err(e),
330 Ok((found, offset)) => {
331 if offset == 0 {
334 if name(found) == "math" {
335 Ok(found)
336 } else {
337 let new_mathml = create_mathml_element(&new_doc, "math");
338 new_mathml.append_child(copy_mathml(found));
339 new_doc.root().append_child(new_mathml);
340 Ok(new_mathml)
341 }
342 } else if !is_leaf(found) {
343 bail!(
344 "Internal error: non-zero offset '{}' on a non-leaf element '{}'",
345 offset,
346 name(found)
347 );
348 } else if let Some(ch) = as_text(found).chars().nth(offset) {
349 let internal_mathml = create_mathml_element(&new_doc, name(found));
350 internal_mathml.set_text(&ch.to_string());
351 let new_mathml = create_mathml_element(&new_doc, "math");
352 new_mathml.append_child(internal_mathml);
353 new_doc.root().append_child(new_mathml);
354 Ok(new_mathml)
355 } else {
356 bail!(
357 "Internal error: offset '{}' on leaf element '{}' doesn't exist",
358 offset,
359 mml_to_string(found)
360 );
361 }
362 }
363 };
364 })?;
365
366 let braille = crate::braille::braille_mathml(nav_mathml, "")?.0;
367 return Ok(braille);
368 });
369}
370
371pub fn do_navigate_keypress(
375 key: usize,
376 shift_key: bool,
377 control_key: bool,
378 alt_key: bool,
379 meta_key: bool,
380) -> Result<String> {
381 return MATHML_INSTANCE.with(|package_instance| {
382 let package_instance = package_instance.borrow();
383 let mathml = get_element(&package_instance);
384 return do_mathml_navigate_key_press(mathml, key, shift_key, control_key, alt_key, meta_key);
385 });
386}
387
388pub fn do_navigate_command(command: String) -> Result<String> {
422 enable_logs();
423 let command = NAV_COMMANDS.get_key(&command); if command.is_none() {
425 bail!("Unknown command in call to DoNavigateCommand()");
426 };
427 let command = *command.unwrap();
428 return MATHML_INSTANCE.with(|package_instance| {
429 let package_instance = package_instance.borrow();
430 let mathml = get_element(&package_instance);
431 return do_navigate_command_string(mathml, command);
432 });
433}
434
435pub fn set_navigation_node(id: String, offset: usize) -> Result<()> {
438 enable_logs();
439 return MATHML_INSTANCE.with(|package_instance| {
440 let package_instance = package_instance.borrow();
441 let mathml = get_element(&package_instance);
442 return set_navigation_node_from_id(mathml, id, offset);
443 });
444}
445
446pub fn get_navigation_mathml() -> Result<(String, usize)> {
449 return MATHML_INSTANCE.with(|package_instance| {
450 let package_instance = package_instance.borrow();
451 let mathml = get_element(&package_instance);
452 return NAVIGATION_STATE.with(|nav_stack| {
453 return match nav_stack.borrow_mut().get_navigation_mathml(mathml) {
454 Err(e) => Err(e),
455 Ok((found, offset)) => Ok((mml_to_string(found), offset)),
456 };
457 });
458 });
459}
460
461pub fn get_navigation_mathml_id() -> Result<(String, usize)> {
465 enable_logs();
466 return MATHML_INSTANCE.with(|package_instance| {
467 let package_instance = package_instance.borrow();
468 let mathml = get_element(&package_instance);
469 return Ok(NAVIGATION_STATE.with(|nav_stack| {
470 return nav_stack.borrow().get_navigation_mathml_id(mathml);
471 }));
472 });
473}
474
475pub fn get_braille_position() -> Result<(usize, usize)> {
477 enable_logs();
478 return MATHML_INSTANCE.with(|package_instance| {
479 let package_instance = package_instance.borrow();
480 let mathml = get_element(&package_instance);
481 let nav_node = get_navigation_mathml_id()?;
482 let (_, start, end) = crate::braille::braille_mathml(mathml, &nav_node.0)?;
483 return Ok((start, end));
484 });
485}
486
487pub fn get_navigation_node_from_braille_position(position: usize) -> Result<(String, usize)> {
490 enable_logs();
491 return MATHML_INSTANCE.with(|package_instance| {
492 let package_instance = package_instance.borrow();
493 let mathml = get_element(&package_instance);
494 return crate::braille::get_navigation_node_from_braille_position(mathml, position);
495 });
496}
497
498pub fn get_supported_braille_codes() -> Vec<String> {
499 enable_logs();
500 let rules_dir = crate::prefs::PreferenceManager::get().borrow().get_rules_dir();
501 let braille_dir = rules_dir.join("Braille");
502 let mut braille_code_paths = Vec::new();
503
504 find_all_dirs_shim(&braille_dir, &mut braille_code_paths);
505 let mut braille_code_paths = braille_code_paths.iter()
506 .map(|path| path.strip_prefix(&braille_dir).unwrap().to_string_lossy().to_string())
507 .filter(|string_path| !string_path.is_empty() )
508 .collect::<Vec<String>>();
509 braille_code_paths.sort();
510
511 return braille_code_paths;
512 }
513
514pub fn get_supported_languages() -> Vec<String> {
515 enable_logs();
516 let rules_dir = crate::prefs::PreferenceManager::get().borrow().get_rules_dir();
517 let lang_dir = rules_dir.join("Languages");
518 let mut lang_paths = Vec::new();
519
520 find_all_dirs_shim(&lang_dir, &mut lang_paths);
521 let mut language_paths = lang_paths.iter()
522 .map(|path| path.strip_prefix(&lang_dir).unwrap()
523 .to_string_lossy()
524 .replace(std::path::MAIN_SEPARATOR, "-")
525 .to_string())
526 .filter(|string_path| !string_path.is_empty() )
527 .collect::<Vec<String>>();
528
529 language_paths.sort();
530 return language_paths;
531 }
532
533 pub fn get_supported_speech_styles(lang: String) -> Vec<String> {
534 enable_logs();
535 let rules_dir = crate::prefs::PreferenceManager::get().borrow().get_rules_dir();
536 let lang_dir = rules_dir.join("Languages").join(lang);
537 let mut speech_styles = find_files_in_dir_that_ends_with_shim(&lang_dir, "_Rules.yaml");
538 for file_name in &mut speech_styles {
539 file_name.truncate(file_name.len() - "_Rules.yaml".len())
540 }
541 speech_styles.sort();
542 let mut i = 1;
543 while i < speech_styles.len() {
544 if speech_styles[i-1] == speech_styles[i] {
545 speech_styles.remove(i);
546 } else {
547 i += 1;
548 }
549 }
550 return speech_styles;
551 }
552
553pub fn copy_mathml(mathml: Element) -> Element {
559 let children = mathml.children();
561 let new_mathml = create_mathml_element(&mathml.document(), name(mathml));
562 mathml.attributes().iter().for_each(|attr| {
563 new_mathml.set_attribute_value(attr.name(), attr.value());
564 });
565
566 if children.len() == 1 {
568 if let Some(text) = children[0].text() {
569 new_mathml.set_text(text.text());
570 return new_mathml;
571 }
572 }
573
574 let mut new_children = Vec::with_capacity(children.len());
575 for child in children {
576 let child = as_element(child);
577 let new_child = copy_mathml(child);
578 new_children.push(new_child);
579 }
580 new_mathml.append_children(new_children);
581 return new_mathml;
582}
583
584pub fn errors_to_string(e: &Error) -> String {
585 enable_logs();
586 let mut result = String::default();
587 let mut first_time = true;
588 for e in e.iter() {
589 if first_time {
590 result = format!("{e}\n");
591 first_time = false;
592 } else {
593 result += &format!("caused by: {e}\n");
594 }
595 }
596 return result;
597}
598
599fn add_ids(mathml: Element) -> Element {
600 use std::time::SystemTime;
601 let time = if cfg!(target_family = "wasm") {
602 fastrand::usize(..)
603 } else {
604 SystemTime::now()
605 .duration_since(SystemTime::UNIX_EPOCH)
606 .unwrap()
607 .as_millis() as usize
608 };
609 let mut time_part = radix_fmt::radix(time, 36).to_string();
610 if time_part.len() < 3 {
611 time_part.push_str("a2c"); }
613 let mut random_part = radix_fmt::radix(fastrand::u32(..), 36).to_string();
614 if random_part.len() < 4 {
615 random_part.push_str("a1b2"); }
617 let prefix = "M".to_string() + &time_part[time_part.len() - 3..] + &random_part[random_part.len() - 4..] + "-"; add_ids_to_all(mathml, &prefix, 0);
619 return mathml;
620
621 fn add_ids_to_all(mathml: Element, id_prefix: &str, count: usize) -> usize {
622 let mut count = count;
623 if mathml.attribute("id").is_none() {
624 mathml.set_attribute_value("id", (id_prefix.to_string() + &count.to_string()).as_str());
625 mathml.set_attribute_value("data-id-added", "true");
626 count += 1;
627 };
628
629 if crate::xpath_functions::is_leaf(mathml) {
630 return count;
631 }
632
633 for child in mathml.children() {
634 let child = as_element(child);
635 count = add_ids_to_all(child, id_prefix, count);
636 }
637 return count;
638 }
639}
640
641pub fn get_element(package: &Package) -> Element<'_> {
642 enable_logs();
643 let doc = package.as_document();
644 let mut result = None;
645 for root_child in doc.root().children() {
646 if let ChildOfRoot::Element(e) = root_child {
647 assert!(result.is_none());
648 result = Some(e);
649 }
650 }
651 return result.unwrap();
652}
653
654#[allow(dead_code)]
657pub fn get_intent<'a>(mathml: Element<'a>, doc: Document<'a>) -> Result<Element<'a>> {
658 crate::speech::SPEECH_RULES.with(|rules| rules.borrow_mut().read_files().unwrap());
659 let mathml = cleanup_mathml(mathml)?;
660 return crate::speech::intent_from_mathml(mathml, doc);
661}
662
663#[allow(dead_code)]
664fn trim_doc(doc: &Document) {
665 for root_child in doc.root().children() {
666 if let ChildOfRoot::Element(e) = root_child {
667 trim_element(e, false);
668 } else {
669 doc.root().remove_child(root_child); }
671 }
672}
673
674pub fn trim_element(e: Element, allow_structure_in_leaves: bool) {
676 const WHITESPACE: &[char] = &[' ', '\u{0009}', '\u{000A}', '\u{000D}'];
681 lazy_static! {
682 static ref WHITESPACE_MATCH: Regex = Regex::new(r#"[ \u{0009}\u{000A}\u{000D}]+"#).unwrap();
683 }
684
685 if is_leaf(e) && (!allow_structure_in_leaves || IsNode::is_mathml(e)) {
686 make_leaf_element(e);
688 return;
689 }
690
691 let mut single_text = "".to_string();
692 for child in e.children() {
693 match child {
694 ChildOfElement::Element(c) => {
695 trim_element(c, allow_structure_in_leaves);
696 }
697 ChildOfElement::Text(t) => {
698 single_text += t.text();
699 e.remove_child(child);
700 }
701 _ => {
702 e.remove_child(child);
703 }
704 }
705 }
706
707 if !(is_leaf(e) || name(e) == "intent-literal" || single_text.is_empty()) {
709 if !single_text.trim_matches(WHITESPACE).is_empty() {
713 error!(
714 "trim_element: both element and textual children which shouldn't happen -- ignoring text '{single_text}'"
715 );
716 }
717 return;
718 }
719 if e.children().is_empty() && !single_text.is_empty() {
720 e.set_text(&WHITESPACE_MATCH.replace_all(&single_text, " "));
722 }
723
724 fn make_leaf_element(mathml_leaf: Element) {
725 let children = mathml_leaf.children();
730 if children.is_empty() {
731 return;
732 }
733
734 let mut text = "".to_string();
736 for child in children {
737 let child_text = match child {
738 ChildOfElement::Element(child) => {
739 if name(child) == "mglyph" {
740 child.attribute_value("alt").unwrap_or("").to_string()
741 } else {
742 gather_text(child)
743 }
744 }
745 ChildOfElement::Text(t) => {
746 t.text().to_string()
748 }
749 _ => "".to_string(),
750 };
751 if !child_text.is_empty() {
752 text += &child_text;
753 }
754 }
755
756 mathml_leaf.clear_children();
758 mathml_leaf.set_text(WHITESPACE_MATCH.replace_all(&text, " ").trim_matches(WHITESPACE));
759 fn gather_text(html: Element) -> String {
763 let mut text = "".to_string(); for child in html.children() {
765 match child {
766 ChildOfElement::Element(child) => {
767 text += &gather_text(child);
768 }
769 ChildOfElement::Text(t) => text += t.text(),
770 _ => (),
771 }
772 }
773 return text;
775 }
776 }
777}
778
779#[allow(dead_code)]
782fn is_same_doc(doc1: &Document, doc2: &Document) -> Result<()> {
783 if doc1.root().children().len() != doc2.root().children().len() {
786 bail!(
787 "Children of docs have {} != {} children",
788 doc1.root().children().len(),
789 doc2.root().children().len()
790 );
791 }
792
793 for (i, (c1, c2)) in doc1
794 .root()
795 .children()
796 .iter()
797 .zip(doc2.root().children().iter())
798 .enumerate()
799 {
800 match c1 {
801 ChildOfRoot::Element(e1) => {
802 if let ChildOfRoot::Element(e2) = c2 {
803 is_same_element(*e1, *e2)?;
804 } else {
805 bail!("child #{}, first is element, second is something else", i);
806 }
807 }
808 ChildOfRoot::Comment(com1) => {
809 if let ChildOfRoot::Comment(com2) = c2 {
810 if com1.text() != com2.text() {
811 bail!("child #{} -- comment text differs", i);
812 }
813 } else {
814 bail!("child #{}, first is comment, second is something else", i);
815 }
816 }
817 ChildOfRoot::ProcessingInstruction(p1) => {
818 if let ChildOfRoot::ProcessingInstruction(p2) = c2 {
819 if p1.target() != p2.target() || p1.value() != p2.value() {
820 bail!("child #{} -- processing instruction differs", i);
821 }
822 } else {
823 bail!(
824 "child #{}, first is processing instruction, second is something else",
825 i
826 );
827 }
828 }
829 }
830 }
831 return Ok(());
832}
833
834#[allow(dead_code)]
837pub fn is_same_element(e1: Element, e2: Element) -> Result<()> {
838 enable_logs();
839 if name(e1) != name(e2) {
840 bail!("Names not the same: {}, {}", name(e1), name(e2));
841 }
842
843 if e1.children().len() != e2.children().len() {
846 bail!(
847 "Children of {} have {} != {} children",
848 name(e1),
849 e1.children().len(),
850 e2.children().len()
851 );
852 }
853
854 if let Err(e) = attrs_are_same(e1.attributes(), e2.attributes()) {
855 bail!("In element {}, {}", name(e1), e);
856 }
857
858 for (i, (c1, c2)) in e1.children().iter().zip(e2.children().iter()).enumerate() {
859 match c1 {
860 ChildOfElement::Element(child1) => {
861 if let ChildOfElement::Element(child2) = c2 {
862 is_same_element(*child1, *child2)?;
863 } else {
864 bail!("{} child #{}, first is element, second is something else", name(e1), i);
865 }
866 }
867 ChildOfElement::Comment(com1) => {
868 if let ChildOfElement::Comment(com2) = c2 {
869 if com1.text() != com2.text() {
870 bail!("{} child #{} -- comment text differs", name(e1), i);
871 }
872 } else {
873 bail!("{} child #{}, first is comment, second is something else", name(e1), i);
874 }
875 }
876 ChildOfElement::ProcessingInstruction(p1) => {
877 if let ChildOfElement::ProcessingInstruction(p2) = c2 {
878 if p1.target() != p2.target() || p1.value() != p2.value() {
879 bail!("{} child #{} -- processing instruction differs", name(e1), i);
880 }
881 } else {
882 bail!(
883 "{} child #{}, first is processing instruction, second is something else",
884 name(e1),
885 i
886 );
887 }
888 }
889 ChildOfElement::Text(t1) => {
890 if let ChildOfElement::Text(t2) = c2 {
891 if t1.text() != t2.text() {
892 bail!("{} child #{} -- text differs", name(e1), i);
893 }
894 } else {
895 bail!("{} child #{}, first is text, second is something else", name(e1), i);
896 }
897 }
898 }
899 }
900 return Ok(());
901
902 fn attrs_are_same(attrs1: Vec<Attribute>, attrs2: Vec<Attribute>) -> Result<()> {
904 if attrs1.len() != attrs2.len() {
905 bail!("Attributes have different length: {:?} != {:?}", attrs1, attrs2);
906 }
907 for attr1 in attrs1 {
909 if let Some(found_attr2) = attrs2
910 .iter()
911 .find(|&attr2| attr1.name().local_part() == attr2.name().local_part())
912 {
913 if attr1.value() == found_attr2.value() {
914 continue;
915 } else {
916 bail!(
917 "Attribute named {} has differing values:\n '{}'\n '{}'",
918 attr1.name().local_part(),
919 attr1.value(),
920 found_attr2.value()
921 );
922 }
923 } else {
924 bail!(
925 "Attribute name {} not in [{}]",
926 print_attr(&attr1),
927 print_attrs(&attrs2)
928 );
929 }
930 }
931 return Ok(());
932
933 fn print_attr(attr: &Attribute) -> String {
934 return format!("@{}='{}'", attr.name().local_part(), attr.value());
935 }
936 fn print_attrs(attrs: &[Attribute]) -> String {
937 return attrs.iter().map(print_attr).collect::<Vec<String>>().join(", ");
938 }
939 }
940}
941
942#[cfg(test)]
943mod tests {
944 #[allow(unused_imports)]
945 use super::super::init_logger;
946 use super::*;
947
948 fn are_parsed_strs_equal(test: &str, target: &str) -> bool {
949 let target_package = &parser::parse(target).expect("Failed to parse input");
950 let target_doc = target_package.as_document();
951 trim_doc(&target_doc);
952 debug!("target:\n{}", mml_to_string(get_element(&target_package)));
953
954 let test_package = &parser::parse(test).expect("Failed to parse input");
955 let test_doc = test_package.as_document();
956 trim_doc(&test_doc);
957 debug!("test:\n{}", mml_to_string(get_element(&test_package)));
958
959 match is_same_doc(&test_doc, &target_doc) {
960 Ok(_) => return true,
961 Err(e) => panic!("{}", e),
962 }
963 }
964
965 #[test]
966 fn trim_same() {
967 let trimmed_str = "<math><mrow><mo>-</mo><mi>a</mi></mrow></math>";
968 assert!(are_parsed_strs_equal(trimmed_str, trimmed_str));
969 }
970
971 #[test]
972 fn trim_whitespace() {
973 let trimmed_str = "<math><mrow><mo>-</mo><mi> a </mi></mrow></math>";
974 let whitespace_str = "<math> <mrow ><mo>-</mo><mi> a </mi></mrow ></math>";
975 assert!(are_parsed_strs_equal(trimmed_str, whitespace_str));
976 }
977
978 #[test]
979 fn no_trim_whitespace_nbsp() {
980 let trimmed_str = "<math><mrow><mo>-</mo><mtext>  a </mtext></mrow></math>";
981 let whitespace_str = "<math> <mrow ><mo>-</mo><mtext>  a </mtext></mrow ></math>";
982 assert!(are_parsed_strs_equal(trimmed_str, whitespace_str));
983 }
984
985 #[test]
986 fn trim_comment() {
987 let whitespace_str = "<math> <mrow ><mo>-</mo><mi> a </mi></mrow ></math>";
988 let comment_str = "<math><mrow><mo>-</mo><!--a comment --><mi> a </mi></mrow></math>";
989 assert!(are_parsed_strs_equal(comment_str, whitespace_str));
990 }
991
992 #[test]
993 fn replace_mglyph() {
994 let mglyph_str = "<math>
995 <mrow>
996 <mi>X<mglyph fontfamily='my-braid-font' index='2' alt='23braid' /></mi>
997 <mo>+</mo>
998 <mi>
999 <mglyph fontfamily='my-braid-font' index='5' alt='132braid' />Y
1000 </mi>
1001 <mo>=</mo>
1002 <mi>
1003 <mglyph fontfamily='my-braid-font' index='3' alt='13braid' />
1004 </mi>
1005 </mrow>
1006 </math>";
1007 let result_str = "<math>
1008 <mrow>
1009 <mi>X23braid</mi>
1010 <mo>+</mo>
1011 <mi>132braidY</mi>
1012 <mo>=</mo>
1013 <mi>13braid</mi>
1014 </mrow>
1015 </math>";
1016 assert!(are_parsed_strs_equal(mglyph_str, result_str));
1017 }
1018
1019 #[test]
1020 fn trim_differs() {
1021 let whitespace_str = "<math> <mrow ><mo>-</mo><mi> a </mi></mrow ></math>";
1022 let different_str = "<math> <mrow ><mo>-</mo><mi> b </mi></mrow ></math>";
1023
1024 let package1 = &parser::parse(whitespace_str).expect("Failed to parse input");
1026 let doc1 = package1.as_document();
1027 trim_doc(&doc1);
1028 debug!("doc1:\n{}", mml_to_string(get_element(&package1)));
1029
1030 let package2 = parser::parse(different_str).expect("Failed to parse input");
1031 let doc2 = package2.as_document();
1032 trim_doc(&doc2);
1033 debug!("doc2:\n{}", mml_to_string(get_element(&package2)));
1034
1035 assert!(is_same_doc(&doc1, &doc2).is_err());
1036 }
1037
1038 #[test]
1039 fn test_entities() {
1040 set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
1042
1043 let entity_str = set_mathml("<math><mrow><mo>−</mo><mi>𝕞</mi></mrow></math>".to_string()).unwrap();
1044 let converted_str =
1045 set_mathml("<math><mrow><mo>−</mo><mi>𝕞</mi></mrow></math>".to_string()).unwrap();
1046
1047 lazy_static! {
1049 static ref ID_MATCH: Regex = Regex::new(r#"id='.+?' "#).unwrap();
1050 }
1051 let entity_str = ID_MATCH.replace_all(&entity_str, "");
1052 let converted_str = ID_MATCH.replace_all(&converted_str, "");
1053 assert_eq!(entity_str, converted_str, "normal entity test failed");
1054
1055 let entity_str = set_mathml(
1056 "<math data-quot=\""value"\" data-apos=''value''><mi>XXX</mi></math>".to_string(),
1057 )
1058 .unwrap();
1059 let converted_str =
1060 set_mathml("<math data-quot='\"value\"' data-apos=\"'value'\"><mi>XXX</mi></math>".to_string()).unwrap();
1061 let entity_str = ID_MATCH.replace_all(&entity_str, "");
1062 let converted_str = ID_MATCH.replace_all(&converted_str, "");
1063 assert_eq!(entity_str, converted_str, "special entities quote test failed");
1064
1065 let entity_str =
1066 set_mathml("<math><mo><</mo><mo>></mo><mtext>&lt;</mtext></math>".to_string()).unwrap();
1067 let converted_str =
1068 set_mathml("<math><mo><</mo><mo>></mo><mtext>&lt;</mtext></math>".to_string())
1069 .unwrap();
1070 let entity_str = ID_MATCH.replace_all(&entity_str, "");
1071 let converted_str = ID_MATCH.replace_all(&converted_str, "");
1072 assert_eq!(entity_str, converted_str, "special entities <,>,& test failed");
1073 }
1074
1075 #[test]
1076 fn can_recover_from_invalid_set_rules_dir() {
1077 use std::env;
1078 env::set_var("MathCATRulesDir", "MathCATRulesDir");
1080 assert!(set_rules_dir("someInvalidRulesDir".to_string()).is_err());
1081 assert!(
1082 set_rules_dir(super::super::abs_rules_dir_path()).is_ok(),
1083 "\nset_rules_dir to '{}' failed",
1084 super::super::abs_rules_dir_path()
1085 );
1086 assert!(set_mathml("<math><mn>1</mn></math>".to_string()).is_ok());
1087 }
1088
1089 #[test]
1090 fn single_html_in_mtext() {
1091 let test = "<math><mn>1</mn> <mtext>a<p> para 1</p>bc</mtext> <mi>y</mi></math>";
1092 let target = "<math><mn>1</mn> <mtext>a para 1bc</mtext> <mi>y</mi></math>";
1093 assert!(are_parsed_strs_equal(test, target));
1094 }
1095
1096 #[test]
1097 fn multiple_html_in_mtext() {
1098 let test = "<math><mn>1</mn> <mtext>a<p>para 1</p> <p>para 2</p>bc </mtext> <mi>y</mi></math>";
1099 let target = "<math><mn>1</mn> <mtext>apara 1 para 2bc</mtext> <mi>y</mi></math>";
1100 assert!(are_parsed_strs_equal(test, target));
1101 }
1102
1103 #[test]
1104 fn nested_html_in_mtext() {
1105 let test = "<math><mn>1</mn> <mtext>a <ol><li>first</li><li>second</li></ol> bc</mtext> <mi>y</mi></math>";
1106 let target = "<math><mn>1</mn> <mtext>a firstsecond bc</mtext> <mi>y</mi></math>";
1107 assert!(are_parsed_strs_equal(test, target));
1108 }
1109
1110 #[test]
1111 fn empty_html_in_mtext() {
1112 let test = "<math><mn>1</mn> <mtext>a<br/>bc</mtext> <mi>y</mi></math>";
1113 let target = "<math><mn>1</mn> <mtext>abc</mtext> <mi>y</mi></math>";
1114 assert!(are_parsed_strs_equal(test, target));
1115 }
1116}