1#![allow(non_snake_case)]
4#![allow(clippy::needless_return)]
5use std::cell::RefCell;
6use std::sync::LazyLock;
7
8use crate::canonicalize::{as_text, create_mathml_element};
9use crate::errors::*;
10use phf::phf_map;
11use regex::{Captures, Regex};
12use sxd_document_no_unsafe::dom::{Element, Document, ChildOfRoot, ChildOfElement, Attribute};
13use sxd_document_no_unsafe::parser;
14use sxd_document_no_unsafe::Package;
15use sxd_document_no_unsafe::{as_str, as_qname};
16
17use crate::canonicalize::{as_element, name};
18use crate::shim_filesystem::{find_all_dirs_shim, find_files_in_dir_that_ends_with_shim};
19use log::{debug, error};
20
21use crate::navigate::*;
22use crate::pretty_print::mml_to_string;
23use crate::xpath_functions::{is_leaf, IsNode};
24use std::panic::{catch_unwind, AssertUnwindSafe};
25
26pub const MAX_DEPTH: usize = 512;
28
29#[cfg(feature = "enable-logs")]
30use std::sync::Once;
31#[cfg(feature = "enable-logs")]
32static INIT: Once = Once::new();
33
34fn enable_logs() {
35 #[cfg(feature = "enable-logs")]
36 INIT.call_once(||{
37 #[cfg(target_os = "android")]
38 {
39 use log::*;
40 use android_logger::*;
41
42 android_logger::init_once(
43 Config::default()
44 .with_max_level(LevelFilter::Trace)
45 .with_tag("MathCat")
46 );
47 trace!("Activated Android logger!");
48 }
49 });
50}
51
52thread_local! {
54 static PANIC_INFO: RefCell<Option<(String, String, u32)>> = const { RefCell::new(None) };
56}
57
58pub fn init_panic_handler() {
63 use std::panic;
64 use std::sync::Once;
65
66 static INSTALL: Once = Once::new();
67 INSTALL.call_once(|| {
68 let previous = panic::take_hook();
69 panic::set_hook(Box::new(move |info| {
70 let location = info.location()
71 .map(|l| format!("{}:{}", l.file(), l.line()))
72 .unwrap_or_else(|| "unknown".to_string());
73
74 let payload = info.payload();
75 let msg = if let Some(s) = payload.downcast_ref::<&'static str>() {
76 s.to_string()
77 } else if let Some(s) = payload.downcast_ref::<String>() {
78 s.clone()
79 } else {
80 "Unknown panic payload".to_string()
81 };
82
83 let _ = PANIC_INFO.try_with(|cell| {
85 if let Ok(mut slot) = cell.try_borrow_mut() {
86 *slot = Some((msg, location, 0));
87 }
88 });
89
90 if cfg!(test) {
92 previous(info);
93 }
94 }));
95 });
96}
97
98pub fn report_any_panic<T>(result: Result<Result<T, Error>, Box<dyn std::any::Any + Send>>) -> Result<T, Error> {
99 match result {
100 Ok(val) => val,
101 Err(payload) => {
102 if let Some((msg, file, line)) = PANIC_INFO.with(|cell| cell.borrow_mut().take()) {
104 return Err(anyhow::anyhow!(
105 "MathCAT crash! Please report the following information: '{}' at {}:{}",
106 msg, file, line
107 ));
108 }
109 let msg = if let Some(s) = payload.downcast_ref::<&'static str>() {
111 (*s).to_string()
112 } else if let Some(s) = payload.downcast_ref::<String>() {
113 s.clone()
114 } else {
115 return Err(anyhow::anyhow!("MathCAT crash! -- please report"));
116 };
117 Err(anyhow::anyhow!(
118 "MathCAT crash! Please report the following information: '{}'",
119 msg
120 ))
121 }
122 }
123}
124
125fn cleanup_mathml(mathml: Element) -> Result<Element> {
127 trim_element(mathml, false);
128 let mathml = crate::canonicalize::canonicalize(mathml)?;
129 let mathml = add_ids(mathml);
130 return Ok(mathml);
131}
132
133thread_local! {
134 pub static MATHML_INSTANCE: RefCell<Package> = init_mathml_instance();
136}
137
138fn init_mathml_instance() -> RefCell<Package> {
139 let package = parser::parse("<math></math>")
140 .expect("Internal error in 'init_mathml_instance;: didn't parse initializer string");
141 return RefCell::new(package);
142}
143
144pub fn set_rules_dir(dir: impl AsRef<str>) -> Result<()> {
147 enable_logs();
148 init_panic_handler();
149 let dir = dir.as_ref().to_string();
150 let result = catch_unwind(AssertUnwindSafe(|| {
151 use std::path::PathBuf;
152 let dir_os = if dir.is_empty() {
153 std::env::var_os("MathCATRulesDir").unwrap_or_default()
154 } else {
155 std::ffi::OsString::from(&dir)
156 };
157 let pref_manager = crate::prefs::PreferenceManager::get();
158 pref_manager.borrow_mut().initialize(PathBuf::from(dir_os))
159 }));
160 return report_any_panic(result);
161}
162
163pub fn get_version() -> String {
165 enable_logs();
166 const VERSION: &str = env!("CARGO_PKG_VERSION");
167 return VERSION.to_string();
168}
169
170pub fn set_mathml(mathml_str: impl AsRef<str>) -> Result<String> {
174 enable_logs();
175 static MATHJAX_V2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"class *= *['"]MJX-.*?['"]"#).unwrap());
177 static MATHJAX_V3: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"class *= *['"]data-mjx-.*?['"]"#).unwrap());
178
179 static PROCESSING_INSTRUCTION: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"<\?[\s\S]{1,2048}\?>"#).unwrap());
181 static XML_COMMENT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"(?s)"#).unwrap());
182
183 static NAMESPACE_DECL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"xmlns:[[:alpha:]]{1,32}"#).unwrap());
185 static PREFIX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"(</?)[[:alpha:]]{1,32}:"#).unwrap());
186 static HTML_ENTITIES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"&([a-zA-Z]{2,10});"#).unwrap());
187 let result = catch_unwind(AssertUnwindSafe(|| {
188 NAVIGATION_STATE.with(|nav_stack| {
189 nav_stack.borrow_mut().reset();
190 });
191
192 crate::speech::SPEECH_RULES.with(|rules| rules.borrow_mut().read_files())?;
195
196 let mathml_str = mathml_str.as_ref();
197 if mathml_str.len() > 1024 * 1024 {
199 bail!("MathML string of size {} bytes exceeds length limit of 1MB", mathml_str.len());
200 }
201
202 return MATHML_INSTANCE.with(|old_package| {
203 static HTML_ENTITIES_MAPPING: phf::Map<&str, &str> = include!("entities.in");
204
205 let mut error_message = "".to_string(); let mathml_str = XML_COMMENT.replace_all(mathml_str, "");
208 let mathml_str = PROCESSING_INSTRUCTION.replace_all(&mathml_str, "");
209 let mathml_str = HTML_ENTITIES.replace_all(&mathml_str, |cap: &Captures| match HTML_ENTITIES_MAPPING.get(&cap[1]) {
211 None => {
212 error_message = format!("No entity named '{}'", &cap[0]);
213 cap[0].to_string()
214 }
215 Some(&ch) => ch.to_string(),
216 });
217
218 if !error_message.is_empty() {
219 old_package.replace(parser::parse("<math></math>").unwrap());
221 bail!(error_message);
222 }
223 let mathml_str = MATHJAX_V2.replace_all(&mathml_str, "");
224 let mathml_str = MATHJAX_V3.replace_all(&mathml_str, "");
225
226 let mathml_str = NAMESPACE_DECL.replace(&mathml_str, "xmlns"); let mathml_str = PREFIX.replace_all(&mathml_str, "$1");
231
232 let new_package = parser::parse(&mathml_str);
233 if let Err(e) = new_package {
234 old_package.replace(parser::parse("<math></math>").unwrap());
236 bail!("Invalid MathML input:\n{}\nError is: {}", mathml_str, e);
237 }
238
239 let new_package = new_package.unwrap();
240 let mathml = get_element(&new_package);
241 let mathml = cleanup_mathml(mathml)?;
242 let mathml_string = mml_to_string(mathml);
243 old_package.replace(new_package);
244
245 return Ok(mathml_string);
246 });
247 }));
248
249 return report_any_panic(result);
250}
251
252pub fn get_spoken_text() -> Result<String> {
255 enable_logs();
256 let result = catch_unwind(AssertUnwindSafe(|| {
257 MATHML_INSTANCE.with(|package_instance| {
258 let package_instance = package_instance.borrow();
259 let mathml = get_element(&package_instance);
260 let new_package = Package::new();
261 let intent = crate::speech::intent_from_mathml(mathml, new_package.as_document())?;
262 debug!("Intent tree:\n{}", mml_to_string(intent));
263 let speech = crate::speech::speak_mathml(intent, "", 0)?;
264 return Ok(speech);
265 })
266 }));
267 return report_any_panic(result);
268}
269
270pub fn get_overview_text() -> Result<String> {
274 enable_logs();
275 let result = catch_unwind(AssertUnwindSafe(|| {
276 MATHML_INSTANCE.with(|package_instance| {
277 let package_instance = package_instance.borrow();
278 let mathml = get_element(&package_instance);
279 let speech = crate::speech::overview_mathml(mathml, "", 0)?;
280 return Ok(speech);
281 })
282 }));
283 return report_any_panic(result);
284}
285
286pub fn get_preference(name: impl AsRef<str>) -> Result<String> {
289 enable_logs();
290 let name = name.as_ref().to_string();
291 let result = catch_unwind(AssertUnwindSafe(|| {
292 use crate::prefs::NO_PREFERENCE;
293 crate::speech::SPEECH_RULES.with(|rules| {
294 let rules = rules.borrow();
295 let pref_manager = rules.pref_manager.borrow();
296 let mut value = pref_manager.pref_to_string(&name);
297 if value == NO_PREFERENCE {
298 value = pref_manager.pref_to_string(&name);
299 }
300 if value == NO_PREFERENCE {
301 bail!("No preference named '{}'", name);
302 } else {
303 return Ok(value);
304 }
305 })
306 }));
307 return report_any_panic(result);
308}
309
310pub fn set_preference(name: impl AsRef<str>, value: impl AsRef<str>) -> Result<()> {
331 enable_logs();
332 let name = name.as_ref().to_string();
333 let value = value.as_ref().to_string();
334 let result = catch_unwind(AssertUnwindSafe(|| {
335 set_preference_impl(&name, &value)
336 }));
337 return report_any_panic(result);
338}
339
340fn set_preference_impl(name: &str, value: &str) -> Result<()> {
341 let mut value = value.to_string();
342 if name == "Language" || name == "LanguageAuto" {
343 if value != "Auto" {
345 let mut lang_country_split = value.split('-');
347 let language = lang_country_split.next().unwrap_or("");
348 let country = lang_country_split.next().unwrap_or("");
349 if language.len() != 2 {
350 bail!(
351 "Improper format for 'Language' preference '{}'. Should be of form 'en' or 'en-gb'",
352 value
353 );
354 }
355 let mut new_lang_country = language.to_string(); if !country.is_empty() {
357 new_lang_country.push('-');
358 new_lang_country.push_str(country);
359 }
360 value = new_lang_country;
361 }
362 if name == "LanguageAuto" && value == "Auto" {
363 bail!("'LanguageAuto' can not have the value 'Auto'");
364 }
365 }
366
367 crate::speech::SPEECH_RULES.with(|rules| -> Result<()> {
368 if let Some(error_string) = rules.borrow().get_error() {
369 bail!("{}", error_string);
370 }
371 Ok(())
372 })?;
373
374 let pref_manager = crate::prefs::PreferenceManager::get();
376 let mut pref_manager = pref_manager.borrow_mut();
377 if name == "LanguageAuto" {
378 let language_pref = pref_manager.pref_to_string("Language");
379 if language_pref != "Auto" {
380 bail!(
381 "'LanguageAuto' can only be used when 'Language' has the value 'Auto'; Language={}",
382 language_pref
383 );
384 }
385 }
386 let lower_case_value = value.to_lowercase();
387 if lower_case_value == "true" || lower_case_value == "false" {
388 pref_manager.set_api_boolean_pref(name, value.to_lowercase() == "true");
389 } else {
390 match name {
391 "Pitch" | "Rate" | "Volume" | "CapitalLetters_Pitch" | "MathRate" | "PauseFactor" => {
392 pref_manager.set_api_float_pref(name, to_float(name, &value)?)
393 }
394 _ => {
395 pref_manager.set_string_pref(name, &value)?;
396 }
397 }
398 };
399
400 return Ok(());
401}
402
403fn to_float(name: &str, value: &str) -> Result<f64> {
404 return match value.parse::<f64>() {
405 Ok(val) => Ok(val),
406 Err(_) => bail!("SetPreference: preference'{}'s value '{}' must be a float", name, value),
407 };
408}
409
410pub fn get_braille(nav_node_id: impl AsRef<str>) -> Result<String> {
414 enable_logs();
415 let nav_node_id = nav_node_id.as_ref().to_string();
416 let result = catch_unwind(AssertUnwindSafe(|| {
417 MATHML_INSTANCE.with(|package_instance| {
418 let package_instance = package_instance.borrow();
419 let mathml = get_element(&package_instance);
420 let braille = crate::braille::braille_mathml(mathml, &nav_node_id)?.0;
421 return Ok(braille);
422 })
423 }));
424 return report_any_panic(result);
425}
426
427pub fn get_navigation_braille() -> Result<String> {
431 enable_logs();
432 let result = catch_unwind(AssertUnwindSafe(|| {
433 MATHML_INSTANCE.with(|package_instance| {
434 let package_instance = package_instance.borrow();
435 let mathml = get_element(&package_instance);
436 let new_package = Package::new(); let new_doc = new_package.as_document();
438 let nav_mathml = NAVIGATION_STATE.with(|nav_stack| {
439 return match nav_stack.borrow_mut().get_navigation_mathml(mathml) {
440 Err(e) => Err(e),
441 Ok((found, offset)) => {
442 if offset == 0 {
445 if name(found) == "math" {
446 Ok(found)
447 } else {
448 let new_mathml = create_mathml_element(&new_doc, "math");
449 new_mathml.append_child(copy_mathml(found));
450 new_doc.root().append_child(new_mathml);
451 Ok(new_mathml)
452 }
453 } else if !is_leaf(found) {
454 bail!(
455 "Internal error: non-zero offset '{}' on a non-leaf element '{}'",
456 offset,
457 name(found)
458 );
459 } else if let Some(ch) = as_text(found).chars().nth(offset) {
460 let internal_mathml = create_mathml_element(&new_doc, as_str!(name(found)));
461 internal_mathml.set_text(&ch.to_string());
462 let new_mathml = create_mathml_element(&new_doc, "math");
463 new_mathml.append_child(internal_mathml);
464 new_doc.root().append_child(new_mathml);
465 Ok(new_mathml)
466 } else {
467 bail!(
468 "Internal error: offset '{}' on leaf element '{}' doesn't exist",
469 offset,
470 mml_to_string(found)
471 );
472 }
473 }
474 };
475 })?;
476
477 let braille = crate::braille::braille_mathml(nav_mathml, "")?.0;
478 return Ok(braille);
479 })
480 }));
481 return report_any_panic(result);
482}
483
484pub fn do_navigate_keypress(
488 key: usize,
489 shift_key: bool,
490 control_key: bool,
491 alt_key: bool,
492 meta_key: bool,
493) -> Result<String> {
494 enable_logs();
495 let result = catch_unwind(AssertUnwindSafe(|| {
496 MATHML_INSTANCE.with(|package_instance| {
497 let package_instance = package_instance.borrow();
498 let mathml = get_element(&package_instance);
499 return do_mathml_navigate_key_press(mathml, key, shift_key, control_key, alt_key, meta_key);
500 })
501 }));
502 return report_any_panic(result);
503}
504
505pub fn do_navigate_command(command: impl AsRef<str>) -> Result<String> {
539 enable_logs();
540 let command = command.as_ref().to_string();
541 let result = catch_unwind(AssertUnwindSafe(|| {
542 let cmd = NAV_COMMANDS.get_key(&command); if cmd.is_none() {
544 bail!("Unknown command in call to DoNavigateCommand()");
545 };
546 let cmd = *cmd.unwrap();
547 MATHML_INSTANCE.with(|package_instance| {
548 let package_instance = package_instance.borrow();
549 let mathml = get_element(&package_instance);
550 return do_navigate_command_string(mathml, cmd);
551 })
552 }));
553 return report_any_panic(result);
554}
555
556pub fn set_navigation_node(id: impl AsRef<str>, offset: usize) -> Result<()> {
559 enable_logs();
560 let id = id.as_ref().to_string();
561 let result = catch_unwind(AssertUnwindSafe(|| {
562 MATHML_INSTANCE.with(|package_instance| {
563 let package_instance = package_instance.borrow();
564 let mathml = get_element(&package_instance);
565 return set_navigation_node_from_id(mathml, &id, offset);
566 })
567 }));
568 return report_any_panic(result);
569}
570
571pub fn get_navigation_mathml() -> Result<(String, usize)> {
574 enable_logs();
575 let result = catch_unwind(AssertUnwindSafe(|| {
576 MATHML_INSTANCE.with(|package_instance| {
577 let package_instance = package_instance.borrow();
578 let mathml = get_element(&package_instance);
579 return NAVIGATION_STATE.with(|nav_stack| {
580 return match nav_stack.borrow_mut().get_navigation_mathml(mathml) {
581 Err(e) => Err(e),
582 Ok((found, offset)) => Ok((mml_to_string(found), offset)),
583 };
584 });
585 })
586 }));
587 return report_any_panic(result);
588}
589
590pub fn get_navigation_mathml_id() -> Result<(String, usize)> {
594 enable_logs();
595 let result = catch_unwind(AssertUnwindSafe(|| {
596 MATHML_INSTANCE.with(|package_instance| {
597 let package_instance = package_instance.borrow();
598 let mathml = get_element(&package_instance);
599 return Ok(NAVIGATION_STATE.with(|nav_stack| {
600 return nav_stack.borrow().get_navigation_mathml_id(mathml);
601 }));
602 })
603 }));
604 return report_any_panic(result);
605}
606
607pub fn get_braille_position() -> Result<(usize, usize)> {
609 enable_logs();
610 let result = catch_unwind(AssertUnwindSafe(|| {
611 MATHML_INSTANCE.with(|package_instance| {
612 let package_instance = package_instance.borrow();
613 let mathml = get_element(&package_instance);
614 let nav_node = get_navigation_mathml_id()?;
615 let (_, start, end) = crate::braille::braille_mathml(mathml, &nav_node.0)?;
616 return Ok((start, end));
617 })
618 }));
619 return report_any_panic(result);
620}
621
622pub fn get_navigation_node_from_braille_position(position: usize) -> Result<(String, usize)> {
625 enable_logs();
626 let result = catch_unwind(AssertUnwindSafe(|| {
627 MATHML_INSTANCE.with(|package_instance| {
628 let package_instance = package_instance.borrow();
629 let mathml = get_element(&package_instance);
630 return crate::braille::get_navigation_node_from_braille_position(mathml, position);
631 })
632 }));
633 return report_any_panic(result);
634}
635
636pub fn get_supported_braille_codes() -> Result<Vec<String>> {
637 enable_logs();
638 let result = catch_unwind(AssertUnwindSafe(|| {
639 let rules_dir = crate::prefs::PreferenceManager::get().borrow().get_rules_dir();
640 let braille_dir = rules_dir.join("Braille");
641 let mut braille_code_paths = Vec::new();
642
643 find_all_dirs_shim(&braille_dir, &mut braille_code_paths);
644 let mut braille_code_paths = braille_code_paths.iter()
645 .map(|path| path.strip_prefix(&braille_dir).unwrap().to_string_lossy().to_string())
646 .filter(|string_path| !string_path.is_empty() )
647 .collect::<Vec<String>>();
648 braille_code_paths.sort();
649
650 Ok(braille_code_paths)
651 }));
652 return report_any_panic(result);
653 }
654
655pub fn get_supported_languages() -> Result<Vec<String>> {
657 enable_logs();
658 let result = catch_unwind(AssertUnwindSafe(|| {
659 let rules_dir = crate::prefs::PreferenceManager::get().borrow().get_rules_dir();
660 let lang_dir = rules_dir.join("Languages");
661 let mut lang_paths = Vec::new();
662
663 find_all_dirs_shim(&lang_dir, &mut lang_paths);
664 let mut language_paths = lang_paths.iter()
665 .map(|path| path.strip_prefix(&lang_dir).unwrap()
666 .to_string_lossy()
667 .replace(std::path::MAIN_SEPARATOR, "-")
668 .to_string())
669 .filter(|string_path| !string_path.is_empty() )
670 .collect::<Vec<String>>();
671
672 language_paths.retain(|s| !s.starts_with("zz"));
674 language_paths.sort();
675 Ok(language_paths)
676 }));
677 return report_any_panic(result);
678 }
679
680 pub fn get_supported_speech_styles(lang: impl AsRef<str>) -> Result<Vec<String>> {
681 enable_logs();
682 let lang = lang.as_ref().to_string();
683 let result = catch_unwind(AssertUnwindSafe(|| {
684 let rules_dir = crate::prefs::PreferenceManager::get().borrow().get_rules_dir();
685 let lang_dir = rules_dir.join("Languages").join(&lang);
686 let mut speech_styles = find_files_in_dir_that_ends_with_shim(&lang_dir, "_Rules.yaml");
687 for file_name in &mut speech_styles {
688 file_name.truncate(file_name.len() - "_Rules.yaml".len())
689 }
690 speech_styles.sort();
691 speech_styles.dedup(); Ok(speech_styles)
693 }));
694 return report_any_panic(result);
695 }
696
697pub fn copy_mathml(mathml: Element) -> Element {
703 return copy_mathml_recursive(mathml, 0);
704}
705
706fn copy_mathml_recursive(mathml: Element, depth: usize) -> Element {
707 if depth > MAX_DEPTH {
709 return create_mathml_element(&mathml.document(), as_str!(name(mathml)));
711 }
712
713 let children = mathml.children();
715 let new_mathml = create_mathml_element(&mathml.document(), as_str!(name(mathml)));
716 mathml.attributes().iter().for_each(|attr| {
717 new_mathml.set_attribute_value(as_qname!(attr.name()), as_str!(attr.value()));
718 });
719
720 if children.len() == 1 &&
722 let Some(text) = children[0].text() {
723 new_mathml.set_text(as_str!(text.text()));
724 return new_mathml;
725 }
726
727 let mut new_children = Vec::with_capacity(children.len());
728 for child in children {
729 let child = as_element(child);
730 let new_child = copy_mathml_recursive(child, depth + 1);
731 new_children.push(new_child);
732 }
733 new_mathml.append_children(new_children);
734 return new_mathml;
735}
736
737pub fn errors_to_string(e: &Error) -> String {
738 enable_logs();
739 let mut result = format!("{e}\n");
740 for cause in e.chain().skip(1) { result += &format!("caused by: {cause}\n");
742 }
743 result
744}
745
746fn add_ids(mathml: Element) -> Element {
747 use std::time::SystemTime;
748 let time = if cfg!(target_family = "wasm") {
749 fastrand::usize(..)
750 } else {
751 SystemTime::now()
752 .duration_since(SystemTime::UNIX_EPOCH)
753 .unwrap()
754 .as_millis() as usize
755 };
756 let mut time_part = radix_fmt::radix(time, 36).to_string();
757 if time_part.len() < 3 {
758 time_part.push_str("a2c"); }
760 let mut random_part = radix_fmt::radix(fastrand::u32(..), 36).to_string();
761 if random_part.len() < 4 {
762 random_part.push_str("a1b2"); }
764 let prefix = "M".to_string() + &time_part[time_part.len() - 3..] + &random_part[random_part.len() - 4..] + "-"; add_ids_to_all(mathml, &prefix, 0, 0);
766 return mathml;
767
768 fn add_ids_to_all(mathml: Element, id_prefix: &str, count: usize, depth: usize) -> usize {
769 if depth > 512 {
771 return count;
773 }
774
775 let mut count = count;
776 if mathml.attribute("id").is_none() {
777 mathml.set_attribute_value("id", (id_prefix.to_string() + &count.to_string()).as_str());
778 mathml.set_attribute_value("data-id-added", "true");
779 count += 1;
780 };
781
782 if crate::xpath_functions::is_leaf(mathml) {
783 return count;
784 }
785
786 for child in mathml.children() {
787 let child = as_element(child);
788 count = add_ids_to_all(child, id_prefix, count, depth + 1);
789 }
790 return count;
791 }
792}
793
794pub fn get_element(package: &Package) -> Element<'_> {
795 enable_logs();
796 let doc = package.as_document();
797 let mut result = None;
798 for root_child in doc.root().children() {
799 if let ChildOfRoot::Element(e) = root_child {
800 assert!(result.is_none());
801 result = Some(e);
802 }
803 }
804 return result.unwrap();
805}
806
807#[allow(dead_code)]
810pub fn get_intent<'a>(mathml: Element<'a>, doc: Document<'a>) -> Result<Element<'a>> {
811 crate::speech::SPEECH_RULES.with(|rules| rules.borrow_mut().read_files().unwrap());
812 let mathml = cleanup_mathml(mathml)?;
813 return crate::speech::intent_from_mathml(mathml, doc);
814}
815
816#[allow(dead_code)]
817fn trim_doc(doc: &Document) {
818 for root_child in doc.root().children() {
819 if let ChildOfRoot::Element(e) = root_child {
820 trim_element(e, false);
821 } else {
822 doc.root().remove_child(root_child); }
824 }
825}
826
827pub fn trim_element(e: Element, allow_structure_in_leaves: bool) {
829 trim_element_recursive(e, allow_structure_in_leaves, 0);
830}
831
832fn trim_element_recursive(e: Element, allow_structure_in_leaves: bool, depth: usize) {
833 if depth > 512 {
835 return;
836 }
837
838 const WHITESPACE: &[char] = &[' ', '\u{0009}', '\u{000A}','\u{000C}', '\u{000D}'];
843 static WHITESPACE_MATCH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"[ \u{0009}\u{000A}\u{00C}\u{000D}]+"#).unwrap());
844
845 if is_leaf(e) && (!allow_structure_in_leaves || IsNode::is_mathml(e)) {
846 make_leaf_element(e);
848 return;
849 }
850
851 let mut single_text = "".to_string();
852 for child in e.children() {
853 match child {
854 ChildOfElement::Element(c) => {
855 trim_element_recursive(c, allow_structure_in_leaves, depth + 1);
856 }
857 ChildOfElement::Text(t) => {
858 single_text += as_str!(t.text());
859 e.remove_child(child);
860 }
861 _ => {
862 e.remove_child(child);
863 }
864 }
865 }
866
867 if !(is_leaf(e) || name(e) == "intent-literal" || single_text.is_empty()) {
869 if !single_text.trim_matches(WHITESPACE).is_empty() {
873 error!(
874 "trim_element: both element and textual children which shouldn't happen -- ignoring text '{single_text}'"
875 );
876 }
877 return;
878 }
879 if e.children().is_empty() && !single_text.is_empty() {
880 e.set_text(&WHITESPACE_MATCH.replace_all(&single_text, " "));
882 }
883
884 fn make_leaf_element(mathml_leaf: Element) {
885 let children = mathml_leaf.children();
890 if children.is_empty() {
891 return;
892 }
893
894 if rewrite_and_flatten_embedded_mathml(mathml_leaf) {
895 return;
896 }
897
898 let mut text = "".to_string();
900 for child in children {
901 let child_text = match child {
902 ChildOfElement::Element(child) => {
903 if name(child) == "mglyph" {
904 child.attribute_value("alt").as_deref().unwrap_or("").to_string()
905 } else {
906 gather_text(child)
907 }
908 }
909 ChildOfElement::Text(t) => {
910 t.text().to_string()
912 }
913 _ => "".to_string(),
914 };
915 if !child_text.is_empty() {
916 text += &child_text;
917 }
918 }
919
920 mathml_leaf.clear_children();
922 mathml_leaf.set_text(WHITESPACE_MATCH.replace_all(&text, " ").trim_matches(WHITESPACE));
923 fn gather_text(html: Element) -> String {
927 let mut text = "".to_string(); for child in html.children() {
929 match child {
930 ChildOfElement::Element(child) => {
931 text += &gather_text(child);
932 }
933 ChildOfElement::Text(t) => text += as_str!(t.text()),
934 _ => (),
935 }
936 }
937 return text;
939 }
940 }
941
942 fn rewrite_and_flatten_embedded_mathml(mathml_leaf: Element) -> bool {
943 let mut needs_rewrite = false;
946 for child in mathml_leaf.children() {
947 if let Some(element) = child.element() {
948 if name(element) != "math" {
949 return false; }
951 needs_rewrite = true;
952 }
953 };
954
955 if !needs_rewrite {
956 return false;
957 }
958
959 let leaf_name = name(mathml_leaf);
961 let doc = mathml_leaf.document();
962 let mut new_children = Vec::new();
963 let mut is_last_mtext = false;
964 for child in mathml_leaf.children() {
965 if let Some(element) = child.element() {
966 trim_element(element, true);
967 new_children.append(&mut element.children()); is_last_mtext = false;
969 } else if let Some(text) = child.text() {
970 if is_last_mtext {
972 let last_child = new_children.last_mut().unwrap().element().unwrap();
973 let new_text = as_str!(as_text(last_child)).to_string() + as_str!(text.text());
974 last_child.set_text(&new_text);
975 } else {
976 let new_leaf_node = create_mathml_element(&doc, as_str!(leaf_name));
977 new_leaf_node.set_text(as_str!(text.text()));
978 new_children.push(ChildOfElement::Element(new_leaf_node));
979 is_last_mtext = true;
980 }
981 }
982 };
983
984 for child in &mut new_children {
986 if let Some(element) = child.element() && is_leaf(element) {
987 let text = as_str!(as_text(element));
988 let cleaned_text = WHITESPACE_MATCH.replace_all(text, " ").trim_matches(WHITESPACE).to_string();
989 element.set_text(&cleaned_text);
990 }
991 }
992
993 crate::canonicalize::set_mathml_name(mathml_leaf, "mrow");
994 mathml_leaf.clear_children();
995 mathml_leaf.append_children(new_children);
996
997 return true;
999 }
1000}
1001
1002#[allow(dead_code)]
1005fn is_same_doc(doc1: &Document, doc2: &Document) -> Result<()> {
1006 if doc1.root().children().len() != doc2.root().children().len() {
1009 bail!(
1010 "Children of docs have {} != {} children",
1011 doc1.root().children().len(),
1012 doc2.root().children().len()
1013 );
1014 }
1015
1016 for (i, (c1, c2)) in doc1
1017 .root()
1018 .children()
1019 .iter()
1020 .zip(doc2.root().children().iter())
1021 .enumerate()
1022 {
1023 match c1 {
1024 ChildOfRoot::Element(e1) => {
1025 if let ChildOfRoot::Element(e2) = c2 {
1026 is_same_element(*e1, *e2, &[])?;
1027 } else {
1028 bail!("child #{}, first is element, second is something else", i);
1029 }
1030 }
1031 ChildOfRoot::Comment(com1) => {
1032 if let ChildOfRoot::Comment(com2) = c2 {
1033 if com1.text() != com2.text() {
1034 bail!("child #{} -- comment text differs", i);
1035 }
1036 } else {
1037 bail!("child #{}, first is comment, second is something else", i);
1038 }
1039 }
1040 ChildOfRoot::ProcessingInstruction(p1) => {
1041 if let ChildOfRoot::ProcessingInstruction(p2) = c2 {
1042 if p1.target() != p2.target() || p1.value() != p2.value() {
1043 bail!("child #{} -- processing instruction differs", i);
1044 }
1045 } else {
1046 bail!(
1047 "child #{}, first is processing instruction, second is something else",
1048 i
1049 );
1050 }
1051 }
1052 }
1053 }
1054 return Ok(());
1055}
1056
1057#[allow(dead_code)]
1060pub fn is_same_element(e1: Element, e2: Element, ignore_attrs: &[&str]) -> Result<()> {
1061 enable_logs();
1062 if name(e1) != name(e2) {
1063 bail!("Names not the same: {}, {}", name(e1), name(e2));
1064 }
1065
1066 if e1.children().len() != e2.children().len() {
1069 bail!(
1070 "Children of {} have {} != {} children",
1071 name(e1),
1072 e1.children().len(),
1073 e2.children().len()
1074 );
1075 }
1076
1077 if let Err(e) = attrs_are_same(e1.attributes(), e2.attributes(), ignore_attrs) {
1078 bail!("In element {}, {}", name(e1), e);
1079 }
1080
1081 for (i, (c1, c2)) in e1.children().iter().zip(e2.children().iter()).enumerate() {
1082 match c1 {
1083 ChildOfElement::Element(child1) => {
1084 if let ChildOfElement::Element(child2) = c2 {
1085 is_same_element(*child1, *child2, ignore_attrs)?;
1086 } else {
1087 bail!("{} child #{}, first is element, second is something else", name(e1), i);
1088 }
1089 }
1090 ChildOfElement::Comment(com1) => {
1091 if let ChildOfElement::Comment(com2) = c2 {
1092 if com1.text() != com2.text() {
1093 bail!("{} child #{} -- comment text differs", name(e1), i);
1094 }
1095 } else {
1096 bail!("{} child #{}, first is comment, second is something else", name(e1), i);
1097 }
1098 }
1099 ChildOfElement::ProcessingInstruction(p1) => {
1100 if let ChildOfElement::ProcessingInstruction(p2) = c2 {
1101 if p1.target() != p2.target() || p1.value() != p2.value() {
1102 bail!("{} child #{} -- processing instruction differs", name(e1), i);
1103 }
1104 } else {
1105 bail!(
1106 "{} child #{}, first is processing instruction, second is something else",
1107 name(e1),
1108 i
1109 );
1110 }
1111 }
1112 ChildOfElement::Text(t1) => {
1113 if let ChildOfElement::Text(t2) = c2 {
1114 if t1.text() != t2.text() {
1115 bail!("{} child #{} -- text differs", name(e1), i);
1116 }
1117 } else {
1118 bail!("{} child #{}, first is text, second is something else", name(e1), i);
1119 }
1120 }
1121 }
1122 }
1123 return Ok(());
1124
1125 fn attrs_are_same(attrs1: Vec<Attribute>, attrs2: Vec<Attribute>, ignore: &[&str]) -> Result<()> {
1127 let attrs1 = attrs1.iter()
1128 .filter(|a| !ignore.contains(&as_qname!(a.name()).local_part())).cloned()
1129 .collect::<Vec<Attribute>>();
1130 let attrs2 = attrs2.iter()
1131 .filter(|a| !ignore.contains(&as_qname!(a.name()).local_part())).cloned()
1132 .collect::<Vec<Attribute>>();
1133 if attrs1.len() != attrs2.len() {
1134 bail!("Attributes have different length: {:?} != {:?}", attrs1, attrs2);
1135 }
1136 for attr1 in attrs1 {
1138 if let Some(found_attr2) = attrs2
1139 .iter()
1140 .find(|&attr2| as_qname!(attr1.name()).local_part() == as_qname!(attr2.name()).local_part())
1141 {
1142 if attr1.value() == found_attr2.value() {
1143 continue;
1144 } else {
1145 bail!(
1146 "Attribute named {} has differing values:\n '{}'\n '{}'",
1147 as_qname!(attr1.name()).local_part(),
1148 attr1.value(),
1149 found_attr2.value()
1150 );
1151 }
1152 } else {
1153 bail!(
1154 "Attribute name {} not in [{}]",
1155 print_attr(&attr1),
1156 print_attrs(&attrs2)
1157 );
1158 }
1159 }
1160 return Ok(());
1161
1162 fn print_attr(attr: &Attribute) -> String {
1163 return format!("@{}='{}'", as_qname!(attr.name()).local_part(), attr.value());
1164 }
1165 fn print_attrs(attrs: &[Attribute]) -> String {
1166 return attrs.iter().map(print_attr).collect::<Vec<String>>().join(", ");
1167 }
1168 }
1169}
1170
1171#[cfg(test)]
1172mod tests {
1173 #[allow(unused_imports)]
1174 use super::super::init_logger;
1175 use super::*;
1176
1177 fn interface_test<F>(f: F) -> Result<()>
1178 where
1179 F: FnOnce() -> Result<()> + std::panic::UnwindSafe,
1180 {
1181 use std::panic::{catch_unwind, AssertUnwindSafe};
1182 init_panic_handler();
1183 let result = catch_unwind(AssertUnwindSafe(f));
1184 return report_any_panic(result);
1185 }
1186
1187 fn are_parsed_strs_equal(test: &str, target: &str) -> bool {
1188 let test_package = &parser::parse(test).expect("Failed to parse input");
1189 let test_doc = test_package.as_document();
1190 trim_doc(&test_doc);
1191 debug!("test:\n{}", mml_to_string(get_element(test_package)));
1192
1193 let target_package = &parser::parse(target).expect("Failed to parse input");
1194 let target_doc = target_package.as_document();
1195 trim_doc(&target_doc);
1196 debug!("target:\n{}", mml_to_string(get_element(target_package)));
1197
1198 match is_same_doc(&test_doc, &target_doc) {
1199 Ok(_) => return true,
1200 Err(e) => panic!("{}", e),
1201 }
1202 }
1203
1204 #[test]
1205 fn trim_same() {
1206 let trimmed_str = "<math><mrow><mo>-</mo><mi>a</mi></mrow></math>";
1207 assert!(are_parsed_strs_equal(trimmed_str, trimmed_str));
1208 }
1209
1210 #[test]
1211 fn trim_whitespace() {
1212 let trimmed_str = "<math><mrow><mo>-</mo><mi> a </mi></mrow></math>";
1213 let whitespace_str = "<math> <mrow ><mo>-</mo><mi> a </mi></mrow ></math>";
1214 assert!(are_parsed_strs_equal(trimmed_str, whitespace_str));
1215 }
1216
1217 #[test]
1218 fn no_trim_whitespace_nbsp() {
1219 let trimmed_str = "<math><mrow><mo>-</mo><mtext>  a </mtext></mrow></math>";
1220 let whitespace_str = "<math> <mrow ><mo>-</mo><mtext>  a </mtext></mrow ></math>";
1221 assert!(are_parsed_strs_equal(trimmed_str, whitespace_str));
1222 }
1223
1224 #[test]
1225 fn trim_comment() {
1226 let whitespace_str = "<math> <mrow ><mo>-</mo><mi> a </mi></mrow ></math>";
1227 let comment_str = "<math><mrow><mo>-</mo><!--a comment --><mi> a </mi></mrow></math>";
1228 assert!(are_parsed_strs_equal(comment_str, whitespace_str));
1229 }
1230
1231 #[test]
1232 fn replace_mglyph() {
1233 let mglyph_str = "<math>
1234 <mrow>
1235 <mi>X<mglyph fontfamily='my-braid-font' index='2' alt='23braid' /></mi>
1236 <mo>+</mo>
1237 <mi>
1238 <mglyph fontfamily='my-braid-font' index='5' alt='132braid' />Y
1239 </mi>
1240 <mo>=</mo>
1241 <mi>
1242 <mglyph fontfamily='my-braid-font' index='3' alt='13braid' />
1243 </mi>
1244 </mrow>
1245 </math>";
1246 let result_str = "<math>
1247 <mrow>
1248 <mi>X23braid</mi>
1249 <mo>+</mo>
1250 <mi>132braidY</mi>
1251 <mo>=</mo>
1252 <mi>13braid</mi>
1253 </mrow>
1254 </math>";
1255 assert!(are_parsed_strs_equal(mglyph_str, result_str));
1256 }
1257
1258 #[test]
1259 fn trim_differs() {
1260 let whitespace_str = "<math> <mrow ><mo>-</mo><mi> a </mi></mrow ></math>";
1261 let different_str = "<math> <mrow ><mo>-</mo><mi> b </mi></mrow ></math>";
1262
1263 let package1 = &parser::parse(whitespace_str).expect("Failed to parse input");
1265 let doc1 = package1.as_document();
1266 trim_doc(&doc1);
1267 debug!("doc1:\n{}", mml_to_string(get_element(package1)));
1268
1269 let package2 = parser::parse(different_str).expect("Failed to parse input");
1270 let doc2 = package2.as_document();
1271 trim_doc(&doc2);
1272 debug!("doc2:\n{}", mml_to_string(get_element(&package2)));
1273
1274 assert!(is_same_doc(&doc1, &doc2).is_err());
1275 }
1276
1277 #[test]
1278 fn test_entities() -> Result<()> {
1279 return interface_test(|| {
1280 set_rules_dir(super::super::abs_rules_dir_path())?;
1281
1282 let entity_str = set_mathml("<math><mrow><mo>−</mo><mi>𝕞</mi></mrow></math>")?;
1283 let converted_str =
1284 set_mathml("<math><mrow><mo>−</mo><mi>𝕞</mi></mrow></math>")?;
1285
1286 static ID_MATCH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"id='.+?' "#).unwrap());
1288 let entity_str = ID_MATCH.replace_all(&entity_str, "");
1289 let converted_str = ID_MATCH.replace_all(&converted_str, "");
1290 assert_eq!(entity_str, converted_str, "normal entity test failed");
1291
1292 let entity_str = set_mathml(
1293 "<math data-quot=\""value"\" data-apos=''value''><mi>XXX</mi></math>",
1294 )?;
1295 let converted_str =
1296 set_mathml("<math data-quot='\"value\"' data-apos=\"'value'\"><mi>XXX</mi></math>")?;
1297 let entity_str = ID_MATCH.replace_all(&entity_str, "");
1298 let converted_str = ID_MATCH.replace_all(&converted_str, "");
1299 assert_eq!(entity_str, converted_str, "special entities quote test failed");
1300
1301 let entity_str =
1302 set_mathml("<math><mo><</mo><mo>></mo><mtext>&lt;</mtext></math>")?;
1303 let converted_str =
1304 set_mathml("<math><mo><</mo><mo>></mo><mtext>&lt;</mtext></math>")?;
1305 let entity_str = ID_MATCH.replace_all(&entity_str, "");
1306 let converted_str = ID_MATCH.replace_all(&converted_str, "");
1307 assert_eq!(entity_str, converted_str, "special entities <,>,& test failed");
1308 return Ok( () );
1309 });
1310 }
1311
1312 #[test]
1313 fn can_recover_from_invalid_set_rules_dir() -> Result<()> {
1314 return interface_test(|| {
1315 use std::env;
1316 unsafe { env::set_var("MathCATRulesDir", "MathCATRulesDir"); } assert!(set_rules_dir("someInvalidRulesDir").is_err());
1319 assert!(
1320 set_rules_dir(super::super::abs_rules_dir_path()).is_ok(),
1321 "\nset_rules_dir to '{}' failed",
1322 super::super::abs_rules_dir_path()
1323 );
1324 assert!(set_mathml("<math><mn>1</mn></math>").is_ok());
1325 return Ok( () );
1326 });
1327 }
1328
1329 #[test]
1330 fn single_html_in_mtext() {
1331 let test = "<math><mn>1</mn> <mtext>a<p> para 1</p>bc</mtext> <mi>y</mi></math>";
1332 let target = "<math><mn>1</mn> <mtext>a para 1bc</mtext> <mi>y</mi></math>";
1333 assert!(are_parsed_strs_equal(test, target));
1334 }
1335
1336 #[test]
1337 fn multiple_html_in_mtext() {
1338 let test = "<math><mn>1</mn> <mtext>a<p>para 1</p> <p>para 2</p>bc </mtext> <mi>y</mi></math>";
1339 let target = "<math><mn>1</mn> <mtext>apara 1 para 2bc</mtext> <mi>y</mi></math>";
1340 assert!(are_parsed_strs_equal(test, target));
1341 }
1342
1343 #[test]
1344 fn nested_html_in_mtext() {
1345 let test = "<math><mn>1</mn> <mtext>a <ol><li>first</li><li>second</li></ol> bc</mtext> <mi>y</mi></math>";
1346 let target = "<math><mn>1</mn> <mtext>a firstsecond bc</mtext> <mi>y</mi></math>";
1347 assert!(are_parsed_strs_equal(test, target));
1348 }
1349
1350 #[test]
1351 fn empty_html_in_mtext() {
1352 let test = "<math><mn>1</mn> <mtext>a<br/>bc</mtext> <mi>y</mi></math>";
1353 let target = "<math><mn>1</mn> <mtext>abc</mtext> <mi>y</mi></math>";
1354 assert!(are_parsed_strs_equal(test, target));
1355 }
1356
1357 #[test]
1358 fn mathml_in_mtext() {
1359 let test = "<math><mtext>if <math> <msup><mi>n</mi><mn>2</mn></msup></math> is real</mtext></math>";
1360 let target = "<math><mrow><mtext>if </mtext><msup><mi>n</mi><mn>2</mn></msup><mtext> is real</mtext></mrow></math>";
1361 assert!(are_parsed_strs_equal(test, target));
1362 }
1363
1364 #[test]
1365 fn stack_overflow_protection() -> Result<()> {
1366 return interface_test(|| {
1367 set_rules_dir(super::super::abs_rules_dir_path())?;
1368 let mut bad_mathml = String::from("<math>");
1369 for _ in 0..MAX_DEPTH+1 {
1370 bad_mathml.push_str("<msqrt><mi>n</mi>");
1371 }
1372 for _ in 0..MAX_DEPTH+1 {
1373 bad_mathml.push_str("</msqrt>");
1374 }
1375 bad_mathml.push_str("</math>");
1376 assert_eq!(set_mathml(bad_mathml).unwrap_err().to_string(), "MathML is too deeply nested to process");
1377 return Ok( () );
1378 });
1379 }
1380
1381 #[test]
1382 fn old_mathml_cleared_on_error() -> Result<()> {
1383 return interface_test(|| {
1384 set_rules_dir(super::super::abs_rules_dir_path())?;
1385 let good_mathml = "<math><mn>3</mn></math>";
1386 set_mathml(good_mathml)?;
1387 let bad_mathml = "<math><mi>&xabc;</mi></math>";
1388 assert!(set_mathml(bad_mathml).is_err());
1389 assert!(get_spoken_text()? == "");
1390 set_mathml(good_mathml)?;
1391 let bad_mathml = "<math>garbage";
1392 assert!(set_mathml(bad_mathml).is_err());
1393 assert!(get_spoken_text()? == "");
1394 return Ok( () );
1395 });
1396 }
1397
1398
1399
1400 fn setup_speech_ssml() -> Result<()> {
1401 set_rules_dir(super::super::abs_rules_dir_path())?;
1402 set_preference("Language", "en")?;
1403 set_preference("TTS", "SSML")?;
1404 set_preference("MathRate", "80")?;
1405 set_preference("SpeechStyle", "SimpleSpeak")?;
1406 set_preference("Verbosity", "Medium")?;
1407 return Ok( () );
1408 }
1409
1410 #[test]
1411 fn test_no_escaping() -> Result<()> {
1412 return interface_test(|| {
1413 setup_speech_ssml()?;
1414 let expr = " <math>
1415 <mfrac>
1416 <mrow> <mi>x</mi><mo>+</mo><mi>y</mi> </mrow>
1417 <mrow> <mi>x</mi><mo>-</mo><mi>y</mi> </mrow>
1418 </mfrac>
1419 </math>";
1420 set_mathml(&expr)?;
1421 let speech = get_spoken_text()?;
1422 assert!(!speech.contains("<"));
1424 assert!(!speech.contains(">"));
1425 assert!(!speech.contains("&lt;"));
1426 return Ok(());
1427 });
1428 }
1429
1430 fn assert_ssml_attack_neutralized(speech: &str, illegal_ssml: &str) {
1432 assert!(
1433 !speech.contains(illegal_ssml),
1434 "attack payload ({illegal_ssml}) appears verbatim in output: {speech}"
1435 );
1436 assert!(
1437 !speech.contains(r#"time="5000ms""#) && !speech.contains("time='5000ms'"),
1438 "attack break duration in output: {speech}"
1439 );
1440 }
1441
1442 const PAYLOAD: &str = r#"<break time="50000ms"/>"#;
1444 const PAYLOAD_ATTR_XML: &str = "<break time="50000ms"/>";
1446 const PAYLOAD_LEAF_XML: &str = "<break time="50000ms"/>note";
1448
1449 #[test]
1450 fn leaf_text_ssml_attack_neutralized_in_speech() -> Result<()> {
1452 return interface_test(|| {
1453 setup_speech_ssml()?;
1454 let mathml = format!(
1456 r#"<math><mrow><mtext>{PAYLOAD_LEAF_XML}</mtext><mo>+</mo>
1457 <mi>{PAYLOAD_LEAF_XML}</mi><mo>+</mo>
1458 <ms>{PAYLOAD_LEAF_XML}</ms><mo>+</mo>
1459 <mn>{PAYLOAD_LEAF_XML}</mn></mrow></math>"#
1460 );
1461 set_mathml(&mathml)?;
1462 let speech = get_spoken_text()?;
1463 assert_ssml_attack_neutralized(&speech, PAYLOAD);
1464 assert!(speech.contains("note") || speech.contains("<"));
1465 let mathml = format!(
1466 "<math><mrow><mtext>{PAYLOAD_LEAF_XML}</mtext><mo>+</mo><mn>1</mn></mrow></math>"
1467 );
1468 set_mathml(&mathml)?;
1469 let speech = get_spoken_text()?;
1470 assert_ssml_attack_neutralized(&speech, PAYLOAD);
1471 assert!(speech.contains("note") || speech.contains("<"));
1472 return Ok(());
1473 });
1474 }
1475
1476 #[test]
1477 fn attribute_ssml_attack_neutralized_in_speech() -> Result<()> {
1479 return interface_test(|| {
1480 use crate::speech::{SpeechRulesWithContext, SPEECH_RULES};
1481
1482 setup_speech_ssml()?;
1483 let mathml = format!(
1484 r#"<math data-ssml-attack="{PAYLOAD_ATTR_XML}"><mn>x</mn></math>"#
1485 );
1486 set_mathml(&mathml)?;
1487 let speech = get_spoken_text()?;
1488 assert_ssml_attack_neutralized(&speech, PAYLOAD);
1489
1490 SPEECH_RULES.with(|rules| {
1492 rules.borrow_mut().read_files()?;
1493 let rules_ref = rules.borrow();
1494 let package = parser::parse(&mathml)?;
1495 let math = get_element(&package);
1496 let attr = math
1497 .attribute("data-ssml-attack")
1498 .expect("data-ssml-attack attribute");
1499 let work_package = Package::new();
1500 let mut ctx =
1501 SpeechRulesWithContext::new(&rules_ref, work_package.as_document(), "", 0);
1502 let from_attr = ctx.replace_chars(as_str!(attr.value()), math)?;
1503 assert_ssml_attack_neutralized(&from_attr, PAYLOAD);
1504 assert!(
1505 from_attr.contains("<"),
1506 "attribute value should be XML-escaped for SSML: {from_attr}"
1507 );
1508 Ok::<(), Error>(())
1509 })?;
1510 return Ok(());
1511 });
1512 }
1513}