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