Coverage Report

Created: 2026-05-25 08:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/MathCAT/MathCAT/src/interface.rs
Line
Count
Source
1
//! The interface module provides functionality both for calling from an API and also running the code from `main`.
2
//!
3
#![allow(non_snake_case)]
4
#![allow(clippy::needless_return)]
5
use std::cell::RefCell;
6
use std::sync::LazyLock;
7
8
use crate::canonicalize::{as_text, create_mathml_element};
9
use crate::errors::*;
10
use phf::phf_map;
11
use regex::{Captures, Regex};
12
use sxd_document::dom::{Element, Document, ChildOfRoot, ChildOfElement, Attribute};
13
use sxd_document::parser;
14
use sxd_document::Package;
15
16
use crate::canonicalize::{as_element, name};
17
use crate::shim_filesystem::{find_all_dirs_shim, find_files_in_dir_that_ends_with_shim};
18
use log::{debug, error};
19
20
use crate::navigate::*;
21
use crate::pretty_print::mml_to_string;
22
use crate::xpath_functions::{is_leaf, IsNode};
23
use std::panic::{catch_unwind, AssertUnwindSafe};
24
25
/// Maximum depth to prevent stack overflow on deeply nested MathML
26
pub const MAX_DEPTH: usize = 512;
27
28
#[cfg(feature = "enable-logs")]
29
use std::sync::Once;
30
#[cfg(feature = "enable-logs")]
31
static INIT: Once = Once::new();
32
33
45.0k
fn enable_logs() {
34
    #[cfg(feature = "enable-logs")]
35
    INIT.call_once(||{
36
        #[cfg(target_os = "android")]
37
        {
38
            use log::*;
39
            use android_logger::*;
40
        
41
            android_logger::init_once(
42
                Config::default()
43
                .with_max_level(LevelFilter::Trace)
44
                .with_tag("MathCat")
45
            );    
46
            trace!("Activated Android logger!");  
47
        }    
48
    });
49
45.0k
}
50
51
// For getting a message from a panic
52
thread_local! {
53
    // Stores (Message, File, Line)
54
    static PANIC_INFO: RefCell<Option<(String, String, u32)>> = const { RefCell::new(None) };
55
}
56
57
/// Initialize the panic handler to catch panics and store the message, file, and line number in `PANIC_INFO`.
58
13.5k
pub fn init_panic_handler() {
59
    use std::panic;
60
61
13.5k
    panic::set_hook(Box::new(|info| 
{1
62
1
        let location = info.location()
63
1
            .map(|l| format!("{}:{}", l.file(), l.line()))
64
1
            .unwrap_or_else(|| 
"unknown"0
.
to_string0
());
65
66
1
        let payload = info.payload();
67
1
        let msg = if let Some(
s0
) = payload.downcast_ref::<&'static str>() {
68
0
            s.to_string()
69
1
        } else if let Some(s) = payload.downcast_ref::<String>() {
70
1
            s.clone()
71
        } else {
72
0
            "Unknown panic payload".to_string()
73
        };
74
75
        // Use try_with/try_borrow_mut to ensure the hook never panics itself
76
1
        let _ = PANIC_INFO.try_with(|cell| {
77
1
            if let Ok(mut slot) = cell.try_borrow_mut() {
78
1
                *slot = Some((msg, location, 0));
79
1
            
}0
80
1
        });
81
1
    }));
82
13.5k
}
83
84
41.1k
pub fn report_any_panic<T>(result: Result<Result<T, Error>, Box<dyn std::any::Any + Send>>) -> Result<T, Error> {
85
41.1k
    match result {
86
41.1k
        Ok(val) => val,
87
        Err(_) => {
88
            // Retrieve the smuggled info
89
1
            let details = PANIC_INFO.with(|cell| cell.borrow_mut().take());
90
            
91
1
            if let Some((msg, file, line)) = details {
92
1
                Err(anyhow::anyhow!(
93
1
                    "MathCAT crash! Please report the following information: '{}' at {}:{}",
94
1
                    msg, file, line
95
1
                ))
96
            } else {
97
0
                Err(anyhow::anyhow!("MathCAT crash! -- please report"))
98
            }
99
        }
100
    }
101
41.1k
} 
102
103
// wrap up some common functionality between the call from 'main' and AT
104
4.91k
fn cleanup_mathml(mathml: Element) -> Result<Element> {
105
4.91k
    trim_element(mathml, false);
106
4.91k
    let 
mathml4.91k
= crate::canonicalize::canonicalize(mathml)
?1
;
107
4.91k
    let mathml = add_ids(mathml);
108
4.91k
    return Ok(mathml);
109
4.91k
}
110
111
thread_local! {
112
    /// The current node being navigated (also spoken and brailled) is stored in `MATHML_INSTANCE`.
113
    pub static MATHML_INSTANCE: RefCell<Package> = init_mathml_instance();
114
}
115
116
3.92k
fn init_mathml_instance() -> RefCell<Package> {
117
3.92k
    let package = parser::parse("<math></math>")
118
3.92k
        .expect("Internal error in 'init_mathml_instance;: didn't parse initializer string");
119
3.92k
    return RefCell::new(package);
120
3.92k
}
121
122
/// Set the Rules directory
123
/// IMPORTANT: this should be the very first call to MathCAT. If 'dir' is an empty string, the environment var 'MathCATRulesDir' is tried.
124
5.08k
pub fn set_rules_dir(dir: impl AsRef<str>) -> Result<()> {
125
5.08k
    enable_logs();
126
5.08k
    init_panic_handler();
127
5.08k
    let dir = dir.as_ref().to_string();
128
5.08k
    let result = catch_unwind(AssertUnwindSafe(|| {
129
        use std::path::PathBuf;
130
5.08k
        let dir_os = if dir.is_empty() {
131
0
            std::env::var_os("MathCATRulesDir").unwrap_or_default()
132
        } else {
133
5.08k
            std::ffi::OsString::from(&dir)
134
        };
135
5.08k
        let pref_manager = crate::prefs::PreferenceManager::get();
136
5.08k
        pref_manager.borrow_mut().initialize(PathBuf::from(dir_os))
137
5.08k
    }));
138
5.08k
    return report_any_panic(result);
139
5.08k
}
140
141
/// Returns the version number (from Cargo.toml) of the build
142
0
pub fn get_version() -> String {
143
0
    enable_logs();
144
    const VERSION: &str = env!("CARGO_PKG_VERSION");
145
0
    return VERSION.to_string();
146
0
}
147
148
/// This will override any previous MathML that was set.
149
/// This returns canonical MathML with 'id's set on any node that doesn't have an id.
150
/// The ids can be used for sync highlighting if the `Bookmark` API preference is true.
151
4.88k
pub fn set_mathml(mathml_str: impl AsRef<str>) -> Result<String> {
152
4.88k
    enable_logs();
153
    // if these are present when resent to MathJaX, MathJaX crashes (https://github.com/mathjax/MathJax/issues/2822)
154
3
    static MATHJAX_V2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"class *= *['"]MJX-.*?['"]"#).unwrap());
155
3
    static MATHJAX_V3: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"class *= *['"]data-mjx-.*?['"]"#).unwrap());
156
157
    // Strip out processing instructions and comments -- these are not MathML and can cause DOS problems in the parser
158
3
    static PROCESSING_INSTRUCTION: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"<\?[\s\S]{1,2048}\?>"#).unwrap());
159
3
    static XML_COMMENT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"(?s)"#).unwrap());
160
161
    // These have some length limits to avoid DOS attacks via long strings
162
3
    static NAMESPACE_DECL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"xmlns:[[:alpha:]]{1,32}"#).unwrap());
163
3
    static PREFIX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"(</?)[[:alpha:]]{1,32}:"#).unwrap());
164
3
    static HTML_ENTITIES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"&([a-zA-Z]{2,10});"#).unwrap());
165
4.88k
    let result = catch_unwind(AssertUnwindSafe(|| {
166
4.88k
        NAVIGATION_STATE.with(|nav_stack| {
167
4.88k
            nav_stack.borrow_mut().reset();
168
4.88k
        });
169
170
        // We need the main definitions files to be read in so canonicalize can work.
171
        // This call reads all of them for the current preferences, but that's ok since they will likely be used
172
4.88k
        crate::speech::SPEECH_RULES.with(|rules| rules.borrow_mut().read_files())
?0
;
173
174
4.88k
        let mathml_str = mathml_str.as_ref();
175
        // Safety guard: Reject strings > 1MB to prevent DoS/Stack issues
176
4.88k
        if mathml_str.len() > 1024 * 1024 {
177
0
            bail!("MathML string of size {} bytes exceeds length limit of 1MB", mathml_str.len());
178
4.88k
        }
179
180
4.88k
        return MATHML_INSTANCE.with(|old_package| {
181
            static HTML_ENTITIES_MAPPING: phf::Map<&str, &str> = include!("entities.in");
182
183
4.88k
            let mut error_message = "".to_string(); // can't return a result inside the replace_all, so we do this hack of setting the message and then returning the error
184
                                                                     
185
4.88k
            let mathml_str = XML_COMMENT.replace_all(mathml_str, "");
186
4.88k
            let mathml_str = PROCESSING_INSTRUCTION.replace_all(&mathml_str, "");
187
            // FIX: need to deal with character data and convert to something the parser knows
188
4.88k
            let mathml_str = HTML_ENTITIES.replace_all(&mathml_str, |cap: &Captures| match 
HTML_ENTITIES_MAPPING96
.
get96
(&cap[1]) {
189
                    None => {
190
1
                        error_message = format!("No entity named '{}'", &cap[0]);
191
1
                        cap[0].to_string()
192
                    }
193
95
                    Some(&ch) => ch.to_string(),
194
96
                });
195
196
4.88k
            if !error_message.is_empty() {
197
                // Clear stale state so subsequent API calls do not return previous user's data (security issue)
198
1
                old_package.replace(parser::parse("<math></math>").unwrap());
199
1
                bail!(error_message);
200
4.88k
            }
201
4.88k
            let mathml_str = MATHJAX_V2.replace_all(&mathml_str, "");
202
4.88k
            let mathml_str = MATHJAX_V3.replace_all(&mathml_str, "");
203
204
            // the speech rules use the xpath "name" function and that includes the prefix
205
            // getting rid of the prefix properly probably involves a recursive replacement in the tree
206
            // if the prefix is used, it is almost certainly something like "m" or "mml", so this cheat will work.
207
4.88k
            let mathml_str = NAMESPACE_DECL.replace(&mathml_str, "xmlns"); // do this before the PREFIX replace!
208
4.88k
            let mathml_str = PREFIX.replace_all(&mathml_str, "$1");
209
210
4.88k
            let new_package = parser::parse(&mathml_str);
211
4.88k
            if let Err(
e1
) = new_package {
212
                // Clear stale state so subsequent API calls do not return previous user's data (security issue)
213
1
                old_package.replace(parser::parse("<math></math>").unwrap());
214
1
                bail!("Invalid MathML input:\n{}\nError is: {}", &mathml_str, &e.to_string());
215
4.88k
            }
216
217
4.88k
            let new_package = new_package.unwrap();
218
4.88k
            let mathml = get_element(&new_package);
219
4.88k
            let 
mathml4.88k
= cleanup_mathml(mathml)
?1
;
220
4.88k
            let mathml_string = mml_to_string(mathml);
221
4.88k
            old_package.replace(new_package);
222
223
4.88k
            return Ok(mathml_string);
224
4.88k
        });
225
4.88k
    }));
226
227
4.88k
    return report_any_panic(result);
228
4.88k
}
229
230
/// Get the spoken text of the MathML that was set.
231
/// The speech takes into account any AT or user preferences.
232
3.46k
pub fn get_spoken_text() -> Result<String> {
233
3.46k
    enable_logs();
234
3.46k
    let result = catch_unwind(AssertUnwindSafe(|| {
235
3.46k
        MATHML_INSTANCE.with(|package_instance| {
236
3.46k
            let package_instance = package_instance.borrow();
237
3.46k
            let mathml = get_element(&package_instance);
238
3.46k
            let new_package = Package::new();
239
3.46k
            let intent = crate::speech::intent_from_mathml(mathml, new_package.as_document())
?0
;
240
3.46k
            debug!("Intent tree:\n{}", 
mml_to_string0
(
intent0
));
241
3.46k
            let speech = crate::speech::speak_mathml(intent, "", 0)
?0
;
242
3.46k
            return Ok(speech);
243
3.46k
        })
244
3.46k
    }));
245
3.46k
    return report_any_panic(result);
246
3.46k
}
247
248
/// Get the spoken text for an overview of the MathML that was set.
249
/// The speech takes into account any AT or user preferences.
250
/// Note: this implementation for is currently minimal and should not be used.
251
0
pub fn get_overview_text() -> Result<String> {
252
0
    enable_logs();
253
0
    let result = catch_unwind(AssertUnwindSafe(|| {
254
0
        MATHML_INSTANCE.with(|package_instance| {
255
0
            let package_instance = package_instance.borrow();
256
0
            let mathml = get_element(&package_instance);
257
0
            let speech = crate::speech::overview_mathml(mathml, "", 0)?;
258
0
            return Ok(speech);
259
0
        })
260
0
    }));
261
0
    return report_any_panic(result);
262
0
}
263
264
/// Get the value of the named preference.
265
/// None is returned if `name` is not a known preference.
266
100
pub fn get_preference(name: impl AsRef<str>) -> Result<String> {
267
100
    enable_logs();
268
100
    let name = name.as_ref().to_string();
269
100
    let result = catch_unwind(AssertUnwindSafe(|| {
270
        use crate::prefs::NO_PREFERENCE;
271
100
        crate::speech::SPEECH_RULES.with(|rules| {
272
100
            let rules = rules.borrow();
273
100
            let pref_manager = rules.pref_manager.borrow();
274
100
            let mut value = pref_manager.pref_to_string(&name);
275
100
            if value == NO_PREFERENCE {
276
1
                value = pref_manager.pref_to_string(&name);
277
99
            }
278
100
            if value == NO_PREFERENCE {
279
1
                bail!("No preference named '{}'", name);
280
            } else {
281
99
                return Ok(value);
282
            }
283
100
        })
284
100
    }));
285
100
    return report_any_panic(result);
286
100
}
287
288
/// Set a MathCAT preference. The preference name should be a known preference name.
289
/// The value should either be a string or a number (depending upon the preference being set)
290
/// The list of known user preferences is in the MathCAT user documentation.
291
/// Here are common preferences set by programs (not settable by the user):
292
/// * TTS -- SSML, SAPI5, None
293
/// * Pitch -- normalized at '1.0'
294
/// * Rate -- words per minute (should match current speech rate).
295
///   There is a separate "MathRate" that is user settable that causes a relative percentage change from this rate.
296
/// * Volume -- default 100
297
/// * Voice -- set a voice to use (not implemented)
298
/// * Gender -- set pick any voice of the given gender (not implemented)
299
/// * Bookmark -- set to `true` if a `mark`/`bookmark` should be part of the returned speech (used for sync highlighting)
300
///
301
/// Important: both the preference name and value are case-sensitive
302
///
303
/// This function can be called multiple times to set different values.
304
/// The values are persistent and extend beyond calls to [`set_mathml`].
305
/// A value can be overwritten by calling this function again with a different value.
306
///
307
/// Be careful setting preferences -- these potentially override user settings, so only preferences that really need setting should be set.
308
17.7k
pub fn set_preference(name: impl AsRef<str>, value: impl AsRef<str>) -> Result<()> {
309
17.7k
    enable_logs();
310
17.7k
    let name = name.as_ref().to_string();
311
17.7k
    let value = value.as_ref().to_string();
312
17.7k
    let result = catch_unwind(AssertUnwindSafe(|| {
313
17.7k
        set_preference_impl(&name, &value)
314
17.7k
    }));
315
17.7k
    return report_any_panic(result);
316
17.7k
}
317
318
17.7k
fn set_preference_impl(name: &str, value: &str) -> Result<()> {
319
17.7k
    let mut value = value.to_string();
320
17.7k
    if name == "Language" || 
name == "LanguageAuto"12.7k
{
321
        // check the format
322
5.03k
        if value != "Auto" {
323
            // could get es, es-419, or en-us-nyc ...  we only care about the first two parts so we clean it up a little
324
5.03k
            let mut lang_country_split = value.split('-');
325
5.03k
            let language = lang_country_split.next().unwrap_or("");
326
5.03k
            let country = lang_country_split.next().unwrap_or("");
327
5.03k
            if language.len() != 2 {
328
0
                bail!(
329
                    "Improper format for 'Language' preference '{}'. Should be of form 'en' or 'en-gb'",
330
                    value
331
                );
332
5.03k
            }
333
5.03k
            let mut new_lang_country = language.to_string(); // need a temp value because 'country' is borrowed from 'value' above
334
5.03k
            if !country.is_empty() {
335
321
                new_lang_country.push('-');
336
321
                new_lang_country.push_str(country);
337
4.70k
            }
338
5.03k
            value = new_lang_country;
339
0
        }
340
5.03k
        if name == "LanguageAuto" && 
value == "Auto"0
{
341
0
            bail!("'LanguageAuto' can not have the value 'Auto'");
342
5.03k
        }
343
12.7k
    }
344
345
17.7k
    crate::speech::SPEECH_RULES.with(|rules| -> Result<()> {
346
17.7k
        if let Some(
error_string0
) = rules.borrow().get_error() {
347
0
            bail!("{}", error_string);
348
17.7k
        }
349
17.7k
        Ok(())
350
17.7k
    })
?0
;
351
352
    // Do not hold a SpeechRules borrow while updating preferences: invalidation clears rule caches.
353
17.7k
    let pref_manager = crate::prefs::PreferenceManager::get();
354
17.7k
    let mut pref_manager = pref_manager.borrow_mut();
355
17.7k
    if name == "LanguageAuto" {
356
0
        let language_pref = pref_manager.pref_to_string("Language");
357
0
        if language_pref != "Auto" {
358
0
            bail!(
359
                "'LanguageAuto' can only be used when 'Language' has the value 'Auto'; Language={}",
360
                language_pref
361
            );
362
0
        }
363
17.7k
    }
364
17.7k
    let lower_case_value = value.to_lowercase();
365
17.7k
    if lower_case_value == "true" || 
lower_case_value == "false"17.6k
{
366
1.50k
        pref_manager.set_api_boolean_pref(name, value.to_lowercase() == "true");
367
1.50k
    } else {
368
16.2k
        match name {
369
16.2k
            "Pitch" | "Rate" | "Volume" | "CapitalLetters_Pitch" | "MathRate" | "PauseFactor" => {
370
0
                pref_manager.set_api_float_pref(name, to_float(name, &value)?)
371
            }
372
            _ => {
373
16.2k
                pref_manager.set_string_pref(name, &value)
?0
;
374
            }
375
        }
376
    };
377
378
17.7k
    return Ok(());
379
17.7k
}
380
381
0
fn to_float(name: &str, value: &str) -> Result<f64> {
382
0
    return match value.parse::<f64>() {
383
0
        Ok(val) => Ok(val),
384
0
        Err(_) => bail!("SetPreference: preference'{}'s value '{}' must be a float", name, value),
385
    };
386
0
}
387
388
/// Get the braille associated with the MathML that was set by [`set_mathml`].
389
/// The braille returned depends upon the preference for the `code` preference (default `Nemeth`).
390
/// If 'nav_node_id' is given, it is highlighted based on the value of `BrailleNavHighlight` (default: `EndPoints`)
391
1.36k
pub fn get_braille(nav_node_id: impl AsRef<str>) -> Result<String> {
392
1.36k
    enable_logs();
393
1.36k
    let nav_node_id = nav_node_id.as_ref().to_string();
394
1.36k
    let result = catch_unwind(AssertUnwindSafe(|| {
395
1.36k
        MATHML_INSTANCE.with(|package_instance| {
396
1.36k
            let package_instance = package_instance.borrow();
397
1.36k
            let mathml = get_element(&package_instance);
398
1.36k
            let braille = crate::braille::braille_mathml(mathml, &nav_node_id)
?0
.0;
399
1.36k
            return Ok(braille);
400
1.36k
        })
401
1.36k
    }));
402
1.36k
    return report_any_panic(result);
403
1.36k
}
404
405
/// Get the braille associated with the current navigation focus of the MathML that was set by [`set_mathml`].
406
/// The braille returned depends upon the preference for the `code` preference (default `Nemeth`).
407
/// The returned braille is brailled as if the current navigation focus is the entire expression to be brailled.
408
0
pub fn get_navigation_braille() -> Result<String> {
409
0
    enable_logs();
410
0
    let result = catch_unwind(AssertUnwindSafe(|| {
411
0
        MATHML_INSTANCE.with(|package_instance| {
412
0
            let package_instance = package_instance.borrow();
413
0
            let mathml = get_element(&package_instance);
414
0
            let new_package = Package::new(); // used if we need to create a new tree
415
0
            let new_doc = new_package.as_document();
416
0
            let nav_mathml = NAVIGATION_STATE.with(|nav_stack| {
417
0
                return match nav_stack.borrow_mut().get_navigation_mathml(mathml) {
418
0
                    Err(e) => Err(e),
419
0
                    Ok((found, offset)) => {
420
                        // get the MathML node and wrap it inside of a <math> element
421
                        // if the offset is given, we need to get the character it references
422
0
                        if offset == 0 {
423
0
                            if name(found) == "math" {
424
0
                                Ok(found)
425
                            } else {
426
0
                                let new_mathml = create_mathml_element(&new_doc, "math");
427
0
                                new_mathml.append_child(copy_mathml(found));
428
0
                                new_doc.root().append_child(new_mathml);
429
0
                                Ok(new_mathml)
430
                            }
431
0
                        } else if !is_leaf(found) {
432
0
                            bail!(
433
                                "Internal error: non-zero offset '{}' on a non-leaf element '{}'",
434
                                offset,
435
0
                                name(found)
436
                            );
437
0
                        } else if let Some(ch) = as_text(found).chars().nth(offset) {
438
0
                            let internal_mathml = create_mathml_element(&new_doc, name(found));
439
0
                            internal_mathml.set_text(&ch.to_string());
440
0
                            let new_mathml = create_mathml_element(&new_doc, "math");
441
0
                            new_mathml.append_child(internal_mathml);
442
0
                            new_doc.root().append_child(new_mathml);
443
0
                            Ok(new_mathml)
444
                        } else {
445
0
                            bail!(
446
                                "Internal error: offset '{}' on leaf element '{}' doesn't exist",
447
                                offset,
448
0
                                mml_to_string(found)
449
                            );
450
                        }
451
                    }
452
                };
453
0
            })?;
454
455
0
            let braille = crate::braille::braille_mathml(nav_mathml, "")?.0;
456
0
            return Ok(braille);
457
0
        })
458
0
    }));
459
0
    return report_any_panic(result);
460
0
}
461
462
/// Given a key code along with the modifier keys, the current node is moved accordingly (or value reported in some cases).
463
/// `key` is the [keycode](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#constants_for_keycode_value) for the key (in JavaScript, `ev.key_code`)
464
/// The spoken text for the new current node is returned.
465
0
pub fn do_navigate_keypress(
466
0
    key: usize,
467
0
    shift_key: bool,
468
0
    control_key: bool,
469
0
    alt_key: bool,
470
0
    meta_key: bool,
471
0
) -> Result<String> {
472
0
    enable_logs();
473
0
    let result = catch_unwind(AssertUnwindSafe(|| {
474
0
        MATHML_INSTANCE.with(|package_instance| {
475
0
            let package_instance = package_instance.borrow();
476
0
            let mathml = get_element(&package_instance);
477
0
            return do_mathml_navigate_key_press(mathml, key, shift_key, control_key, alt_key, meta_key);
478
0
        })
479
0
    }));
480
0
    return report_any_panic(result);
481
0
}
482
483
/// Given a navigation command, the current node is moved accordingly.
484
/// This is a higher level interface than `do_navigate_keypress` for applications that want to interpret the keys themselves.
485
/// The valid commands are:
486
/// * Standard move commands:
487
///   `MovePrevious`, `MoveNext`, `MoveStart`, `MoveEnd`, `MoveLineStart`, `MoveLineEnd`
488
/// * Movement in a table or elementary math:
489
///   `MoveCellPrevious`, `MoveCellNext`, `MoveCellUp`, `MoveCellDown`, `MoveColumnStart`, `MoveColumnEnd`
490
/// * Moving into children or out to parents:
491
///   `ZoomIn`, `ZoomOut`, `ZoomOutAll`, `ZoomInAll`
492
/// * Undo the last movement command:
493
///   `MoveLastLocation`
494
/// * Read commands (standard speech):
495
///   `ReadPrevious`, `ReadNext`, `ReadCurrent`, `ReadCellCurrent`, `ReadStart`, `ReadEnd`, `ReadLineStart`, `ReadLineEnd`
496
/// * Describe commands (overview):
497
///   `DescribePrevious`, `DescribeNext`, `DescribeCurrent`
498
/// * Location information:
499
///   `WhereAmI`, `WhereAmIAll`
500
/// * Change navigation modes (circle up/down):
501
///   `ToggleZoomLockUp`, `ToggleZoomLockDown`
502
/// * Speak the current navigation mode
503
///   `ToggleSpeakMode`
504
///
505
/// There are 10 place markers that can be set/read/described or moved to.
506
/// * Setting:
507
///   `SetPlacemarker0`, `SetPlacemarker1`, `SetPlacemarker2`, `SetPlacemarker3`, `SetPlacemarker4`, `SetPlacemarker5`, `SetPlacemarker6`, `SetPlacemarker7`, `SetPlacemarker8`, `SetPlacemarker9`
508
/// * Reading:
509
///   `Read0`, `Read1`, `Read2`, `Read3`, `Read4`, `Read5`, `Read6`, `Read7`, `Read8`, `Read9`
510
/// * Describing:
511
///   `Describe0`, `Describe1`, `Describe2`, `Describe3`, `Describe4`, `Describe5`, `Describe6`, `Describe7`, `Describe8`, `Describe9`
512
/// * Moving:
513
///   `MoveTo0`, `MoveTo1`, `MoveTo2`, `MoveTo3`, `MoveTo4`, `MoveTo5`, `MoveTo6`, `MoveTo7`, `MoveTo8`, `MoveTo9`
514
///
515
/// When done with Navigation, call with `Exit`
516
0
pub fn do_navigate_command(command: impl AsRef<str>) -> Result<String> {
517
0
    enable_logs();
518
0
    let command = command.as_ref().to_string();
519
0
    let result = catch_unwind(AssertUnwindSafe(|| {
520
0
        let cmd = NAV_COMMANDS.get_key(&command); // gets a &'static version of the command
521
0
        if cmd.is_none() {
522
0
            bail!("Unknown command in call to DoNavigateCommand()");
523
0
        };
524
0
        let cmd = *cmd.unwrap();
525
0
        MATHML_INSTANCE.with(|package_instance| {
526
0
            let package_instance = package_instance.borrow();
527
0
            let mathml = get_element(&package_instance);
528
0
            return do_navigate_command_string(mathml, cmd);
529
0
        })
530
0
    }));
531
0
    return report_any_panic(result);
532
0
}
533
534
/// Given an 'id' and an offset (for tokens), set the navigation node to that id.
535
/// An error is returned if the 'id' doesn't exist
536
2
pub fn set_navigation_node(id: impl AsRef<str>, offset: usize) -> Result<()> {
537
2
    enable_logs();
538
2
    let id = id.as_ref().to_string();
539
2
    let result = catch_unwind(AssertUnwindSafe(|| {
540
2
        MATHML_INSTANCE.with(|package_instance| {
541
2
            let package_instance = package_instance.borrow();
542
2
            let mathml = get_element(&package_instance);
543
2
            return set_navigation_node_from_id(mathml, &id, offset);
544
2
        })
545
2
    }));
546
2
    return report_any_panic(result);
547
2
}
548
549
/// Return the MathML associated with the current (navigation) node and the offset (0-based) from that mathml (not yet implemented)
550
/// The offset is needed for token elements that have multiple characters.
551
0
pub fn get_navigation_mathml() -> Result<(String, usize)> {
552
0
    enable_logs();
553
0
    let result = catch_unwind(AssertUnwindSafe(|| {
554
0
        MATHML_INSTANCE.with(|package_instance| {
555
0
            let package_instance = package_instance.borrow();
556
0
            let mathml = get_element(&package_instance);
557
0
            return NAVIGATION_STATE.with(|nav_stack| {
558
0
                return match nav_stack.borrow_mut().get_navigation_mathml(mathml) {
559
0
                    Err(e) => Err(e),
560
0
                    Ok((found, offset)) => Ok((mml_to_string(found), offset)),
561
                };
562
0
            });
563
0
        })
564
0
    }));
565
0
    return report_any_panic(result);
566
0
}
567
568
/// Return the `id` and `offset` (0-based) associated with the current (navigation) node.
569
/// `offset` (not yet implemented)
570
/// The offset is needed for token elements that have multiple characters.
571
2
pub fn get_navigation_mathml_id() -> Result<(String, usize)> {
572
2
    enable_logs();
573
2
    let result = catch_unwind(AssertUnwindSafe(|| {
574
2
        MATHML_INSTANCE.with(|package_instance| {
575
2
            let package_instance = package_instance.borrow();
576
2
            let mathml = get_element(&package_instance);
577
2
            return Ok(NAVIGATION_STATE.with(|nav_stack| {
578
2
                return nav_stack.borrow().get_navigation_mathml_id(mathml);
579
2
            }));
580
2
        })
581
2
    }));
582
2
    return report_any_panic(result);
583
2
}
584
585
/// Return the start and end braille character positions associated with the current (navigation) node.
586
2
pub fn get_braille_position() -> Result<(usize, usize)> {
587
2
    enable_logs();
588
2
    let result = catch_unwind(AssertUnwindSafe(|| {
589
2
        MATHML_INSTANCE.with(|package_instance| {
590
2
            let package_instance = package_instance.borrow();
591
2
            let mathml = get_element(&package_instance);
592
2
            let nav_node = get_navigation_mathml_id()
?0
;
593
2
            let (_, start, end) = crate::braille::braille_mathml(mathml, &nav_node.0)
?0
;
594
2
            return Ok((start, end));
595
2
        })
596
2
    }));
597
2
    return report_any_panic(result);
598
2
}
599
600
/// Given a 0-based braille position, return the smallest MathML node enclosing it.
601
/// This node might be a leaf with an offset.
602
91
pub fn get_navigation_node_from_braille_position(position: usize) -> Result<(String, usize)> {
603
91
    enable_logs();
604
91
    let result = catch_unwind(AssertUnwindSafe(|| {
605
91
        MATHML_INSTANCE.with(|package_instance| {
606
91
            let package_instance = package_instance.borrow();
607
91
            let mathml = get_element(&package_instance);
608
91
            return crate::braille::get_navigation_node_from_braille_position(mathml, position);
609
91
        })
610
91
    }));
611
91
    return report_any_panic(result);
612
91
}
613
614
0
pub fn get_supported_braille_codes() -> Result<Vec<String>> {
615
0
    enable_logs();
616
0
    let result = catch_unwind(AssertUnwindSafe(|| {
617
0
        let rules_dir = crate::prefs::PreferenceManager::get().borrow().get_rules_dir();
618
0
        let braille_dir = rules_dir.join("Braille");
619
0
        let mut braille_code_paths = Vec::new();
620
621
0
        find_all_dirs_shim(&braille_dir, &mut braille_code_paths);
622
0
        let mut braille_code_paths = braille_code_paths.iter()
623
0
                        .map(|path| path.strip_prefix(&braille_dir).unwrap().to_string_lossy().to_string())
624
0
                        .filter(|string_path| !string_path.is_empty() )
625
0
                        .collect::<Vec<String>>();
626
0
        braille_code_paths.sort();
627
628
0
        Ok(braille_code_paths)
629
0
    }));
630
0
    return report_any_panic(result);
631
0
 }
632
633
/// Returns a Vec of all supported languages ("en", "es", ...)
634
1
pub fn get_supported_languages() -> Result<Vec<String>> {
635
1
    enable_logs();
636
1
    let result = catch_unwind(AssertUnwindSafe(|| {
637
1
        let rules_dir = crate::prefs::PreferenceManager::get().borrow().get_rules_dir();
638
1
        let lang_dir = rules_dir.join("Languages");
639
1
        let mut lang_paths = Vec::new();
640
641
1
        find_all_dirs_shim(&lang_dir, &mut lang_paths);
642
1
        let mut language_paths = lang_paths.iter()
643
13
                        .
map1
(|path| path.strip_prefix(&lang_dir).unwrap()
644
13
                                                  .to_string_lossy()
645
13
                                                  .replace(std::path::MAIN_SEPARATOR, "-")
646
13
                                                  .to_string())
647
13
                        .
filter1
(|string_path| !string_path.is_empty() )
648
1
                        .collect::<Vec<String>>();
649
650
        // make sure the 'zz' test dir isn't included (build.rs removes it, but for debugging is there)
651
13
        
language_paths1
.
retain1
(|s| !s.starts_with("zz"));
652
1
        language_paths.sort();
653
1
        Ok(language_paths)
654
1
    }));
655
1
    return report_any_panic(result);
656
1
 }
657
658
0
 pub fn get_supported_speech_styles(lang: impl AsRef<str>) -> Result<Vec<String>> {
659
0
    enable_logs();
660
0
    let lang = lang.as_ref().to_string();
661
0
    let result = catch_unwind(AssertUnwindSafe(|| {
662
0
        let rules_dir = crate::prefs::PreferenceManager::get().borrow().get_rules_dir();
663
0
        let lang_dir = rules_dir.join("Languages").join(&lang);
664
0
        let mut speech_styles = find_files_in_dir_that_ends_with_shim(&lang_dir, "_Rules.yaml");
665
0
        for file_name in &mut speech_styles {
666
0
            file_name.truncate(file_name.len() - "_Rules.yaml".len())
667
        }
668
0
        speech_styles.sort();
669
0
        speech_styles.dedup(); // remove duplicates -- shouldn't be any, but just in case
670
0
        Ok(speech_styles)
671
0
    }));
672
0
    return report_any_panic(result);
673
0
 }
674
675
// utility functions
676
677
/// Copy (recursively) the (MathML) element and return the new one.
678
/// The Element type does not copy and modifying the structure of an element's child will modify the element, so we need a copy
679
/// Convert the returned error from set_mathml, etc., to a useful string for display
680
363
pub fn copy_mathml(mathml: Element) -> Element {
681
363
    return copy_mathml_recursive(mathml, 0);
682
363
}
683
684
4.53k
fn copy_mathml_recursive(mathml: Element, depth: usize) -> Element {
685
    // Safety: Prevent stack overflow on deeply nested MathML
686
4.53k
    if depth > MAX_DEPTH {
687
        // Return the element as a leaf if it's too deep to prevent crash
688
0
        return create_mathml_element(&mathml.document(), name(mathml));
689
4.53k
    }
690
691
    // If it represents MathML, the 'Element' can only have Text and Element children along with attributes
692
4.53k
    let children = mathml.children();
693
4.53k
    let new_mathml = create_mathml_element(&mathml.document(), name(mathml));
694
9.52k
    
mathml.attributes().iter()4.53k
.
for_each4.53k
(|attr| {
695
9.52k
        new_mathml.set_attribute_value(attr.name(), attr.value());
696
9.52k
    });
697
698
    // can't use is_leaf/as_text because this is also used with the intent tree
699
4.53k
    if children.len() == 1 &&
700
3.26k
       let Some(
text2.59k
) = children[0].text() {
701
2.59k
        new_mathml.set_text(text.text());
702
2.59k
        return new_mathml;
703
1.93k
        }
704
705
1.93k
    let mut new_children = Vec::with_capacity(children.len());
706
4.17k
    for child in 
children1.93k
{
707
4.17k
        let child = as_element(child);
708
4.17k
        let new_child = copy_mathml_recursive(child, depth + 1);
709
4.17k
        new_children.push(new_child);
710
4.17k
    }
711
1.93k
    new_mathml.append_children(new_children);
712
1.93k
    return new_mathml;
713
4.53k
}
714
715
0
pub fn errors_to_string(e: &Error) -> String {
716
0
    enable_logs();
717
0
    let mut result = format!("{e}\n");
718
0
    for cause in e.chain().skip(1) { // skips original error
719
0
        result += &format!("caused by: {cause}\n");
720
0
    }
721
0
    result
722
0
}
723
724
4.91k
fn add_ids(mathml: Element) -> Element {
725
    use std::time::SystemTime;
726
4.91k
    let time = if cfg!(target_family = "wasm") {
727
0
        fastrand::usize(..)
728
    } else {
729
4.91k
        SystemTime::now()
730
4.91k
            .duration_since(SystemTime::UNIX_EPOCH)
731
4.91k
            .unwrap()
732
4.91k
            .as_millis() as usize
733
    };
734
4.91k
    let mut time_part = radix_fmt::radix(time, 36).to_string();
735
4.91k
    if time_part.len() < 3 {
736
0
        time_part.push_str("a2c");      // needs to be at least three chars
737
4.91k
    }
738
4.91k
    let mut random_part = radix_fmt::radix(fastrand::u32(..), 36).to_string();
739
4.91k
    if random_part.len() < 4 {
740
0
        random_part.push_str("a1b2");      // needs to be at least four chars
741
4.91k
    }
742
4.91k
    let prefix = "M".to_string() + &time_part[time_part.len() - 3..] + &random_part[random_part.len() - 4..] + "-"; // begin with letter
743
4.91k
    add_ids_to_all(mathml, &prefix, 0);
744
4.91k
    return mathml;
745
746
57.8k
    fn add_ids_to_all(mathml: Element, id_prefix: &str, count: usize) -> usize {
747
57.8k
        let mut count = count;
748
57.8k
        if mathml.attribute("id").is_none() {
749
57.1k
            mathml.set_attribute_value("id", (id_prefix.to_string() + &count.to_string()).as_str());
750
57.1k
            mathml.set_attribute_value("data-id-added", "true");
751
57.1k
            count += 1;
752
57.1k
        
}707
;
753
754
57.8k
        if crate::xpath_functions::is_leaf(mathml) {
755
35.8k
            return count;
756
22.0k
        }
757
758
52.9k
        for child in 
mathml22.0k
.
children22.0k
() {
759
52.9k
            let child = as_element(child);
760
52.9k
            count = add_ids_to_all(child, id_prefix, count);
761
52.9k
        }
762
22.0k
        return count;
763
57.8k
    }
764
4.91k
}
765
766
10.3k
pub fn get_element(package: &Package) -> Element<'_> {
767
10.3k
    enable_logs();
768
10.3k
    let doc = package.as_document();
769
10.3k
    let mut result = None;
770
10.3k
    for root_child in doc.root().children() {
771
10.3k
        if let ChildOfRoot::Element(e) = root_child {
772
10.3k
            assert!(result.is_none());
773
10.3k
            result = Some(e);
774
0
        }
775
    }
776
10.3k
    return result.unwrap();
777
10.3k
}
778
779
/// Get the intent after setting the MathML
780
/// Used in testing
781
#[allow(dead_code)]
782
32
pub fn get_intent<'a>(mathml: Element<'a>, doc: Document<'a>) -> Result<Element<'a>> {
783
32
    crate::speech::SPEECH_RULES.with(|rules|  rules.borrow_mut().read_files().unwrap());
784
32
    let mathml = cleanup_mathml(mathml)
?0
;
785
32
    return crate::speech::intent_from_mathml(mathml, doc);
786
32
}
787
788
#[allow(dead_code)]
789
22
fn trim_doc(doc: &Document) {
790
22
    for root_child in doc.root().children() {
791
22
        if let ChildOfRoot::Element(e) = root_child {
792
22
            trim_element(e, false);
793
22
        } else {
794
0
            doc.root().remove_child(root_child); // comment or processing instruction
795
0
        }
796
    }
797
22
}
798
799
/// Not really meant to be public -- used by tests in some packages
800
55.5k
pub fn trim_element(e: Element, allow_structure_in_leaves: bool) {
801
    // "<mtext>this is text</mtext" results in 3 text children
802
    // these are combined into one child as it makes code downstream simpler
803
804
    // space, tab, newline, carriage return all get collapsed to a single space
805
    const WHITESPACE: &[char] = &[' ', '\u{0009}', '\u{000A}','\u{000C}', '\u{000D}'];
806
3
    static WHITESPACE_MATCH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"[ \u{0009}\u{000A}\u{00C}\u{000D}]+"#).unwrap());
807
808
55.5k
    if is_leaf(e) && (
!allow_structure_in_leaves34.7k
||
IsNode::is_mathml230
(
e230
)) {
809
        // Assume it is HTML inside of the leaf -- turn the HTML into a string
810
34.7k
        make_leaf_element(e);
811
34.7k
        return;
812
20.7k
    }
813
814
20.7k
    let mut single_text = "".to_string();
815
87.6k
    for child in 
e20.7k
.
children20.7k
() {
816
87.6k
        match child {
817
50.1k
            ChildOfElement::Element(c) => {
818
50.1k
                trim_element(c, allow_structure_in_leaves);
819
50.1k
            }
820
37.4k
            ChildOfElement::Text(t) => {
821
37.4k
                single_text += t.text();
822
37.4k
                e.remove_child(child);
823
37.4k
            }
824
21
            _ => {
825
21
                e.remove_child(child);
826
21
            }
827
        }
828
    }
829
830
    // CSS considers only space, tab, linefeed, and carriage return as collapsable whitespace
831
20.7k
    if !(is_leaf(e) || name(e) == "intent-literal" || single_text.is_empty()) {
832
        // intent-literal comes from testing intent
833
        // FIX: we have a problem -- what should happen???
834
        // FIX: For now, just keep the children and ignore the text and log an error -- shouldn't panic/crash
835
14.3k
        if !single_text.trim_matches(WHITESPACE).is_empty() {
836
20
            error!(
837
                "trim_element: both element and textual children which shouldn't happen -- ignoring text '{single_text}'"
838
            );
839
14.2k
        }
840
14.3k
        return;
841
6.44k
    }
842
6.44k
    if e.children().is_empty() && 
!single_text.is_empty()276
{
843
0
        // debug!("Combining text in {}: '{}' -> '{}'", e.name().local_part(), single_text, trimmed_text);
844
0
        e.set_text(&WHITESPACE_MATCH.replace_all(&single_text, " "));
845
6.44k
    }
846
847
34.7k
    fn make_leaf_element(mathml_leaf: Element) {
848
        // MathML leaves like <mn> really shouldn't have non-textual content, but you could have embedded HTML
849
        // Here, we convert them to leaves by grabbing up all the text and making that the content
850
        // Potentially, we leave them and let (default) rules do something, but it makes other parts of the code
851
        //   messier because checking the text of a leaf becomes Option<&str> rather than just &str
852
34.7k
        let children = mathml_leaf.children();
853
34.7k
        if children.is_empty() {
854
503
            return;
855
34.2k
        }
856
857
34.2k
        if rewrite_and_flatten_embedded_mathml(mathml_leaf) {
858
1
            return;
859
34.2k
        }
860
861
        // gather up the text
862
34.2k
        let mut text = "".to_string();
863
34.6k
        for child in 
children34.2k
{
864
34.6k
            let child_text = match child {
865
8
                ChildOfElement::Element(child) => {
866
8
                    if name(child) == "mglyph" {
867
3
                        child.attribute_value("alt").unwrap_or("").to_string()
868
                    } else {
869
5
                        gather_text(child)
870
                    }
871
                }
872
34.4k
                ChildOfElement::Text(t) => {
873
                    // debug!("ChildOfElement::Text: '{}'", t.text());
874
34.4k
                    t.text().to_string()
875
                }
876
222
                _ => "".to_string(),
877
            };
878
34.6k
            if !child_text.is_empty() {
879
34.4k
                text += &child_text;
880
34.4k
            
}223
881
        }
882
883
        // get rid of the old children and replace with the text we just built
884
34.2k
        mathml_leaf.clear_children();
885
34.2k
        mathml_leaf.set_text(WHITESPACE_MATCH.replace_all(&text, " ").trim_matches(WHITESPACE));
886
        // debug!("make_leaf_element: text is '{}'", crate::canonicalize::as_text(mathml_leaf));
887
888
        /// gather up all the contents of the element and return them with a leading space
889
7
        fn gather_text(html: Element) -> String {
890
7
            let mut text = "".to_string(); // since we are throwing out the element tag, add a space between the contents
891
7
            for child in html.children() {
892
7
                match child {
893
2
                    ChildOfElement::Element(child) => {
894
2
                        text += &gather_text(child);
895
2
                    }
896
5
                    ChildOfElement::Text(t) => text += t.text(),
897
0
                    _ => (),
898
                }
899
            }
900
            // debug!("gather_text: '{}'", text);
901
7
            return text;
902
7
        }
903
34.7k
    }
904
905
34.2k
    fn rewrite_and_flatten_embedded_mathml(mathml_leaf: Element) -> bool {
906
        // first see if it can or needs to be rewritten
907
        // this is likely rare, so we do a check and if true, to a second pass building the result
908
34.2k
        let mut needs_rewrite = false;
909
34.6k
        for child in 
mathml_leaf34.2k
.
children34.2k
() {
910
34.6k
            if let Some(
element8
) = child.element() {
911
8
                if name(element) != "math" {
912
7
                    return false; // something other than MathML as a child -- can't rewrite
913
1
                }
914
1
                needs_rewrite = true;
915
34.6k
            }
916
        };
917
918
34.2k
        if !needs_rewrite {
919
34.2k
            return false;
920
1
        }
921
922
        // now do the rewrite, flatting out the mathml and returning an mrow with the children
923
1
        let leaf_name = name(mathml_leaf);
924
1
        let doc = mathml_leaf.document();
925
1
        let mut new_children = Vec::new();
926
1
        let mut is_last_mtext = false;
927
5
        for child in 
mathml_leaf1
.
children1
() {
928
5
            if let Some(
element1
) = child.element() {
929
1
                trim_element(element, true);
930
1
                new_children.append(&mut element.children());   // don't want 'math' wrapper
931
1
                is_last_mtext = false;
932
4
            } else if let Some(text) = child.text() {
933
                // combine adjacent text nodes into single nodes
934
4
                if is_last_mtext {
935
2
                    let last_child = new_children.last_mut().unwrap().element().unwrap();
936
2
                    let new_text = as_text(last_child).to_string() + text.text();
937
2
                    last_child.set_text(&new_text);
938
2
                } else {
939
2
                    let new_leaf_node = create_mathml_element(&doc, leaf_name);
940
2
                    new_leaf_node.set_text(text.text());
941
2
                    new_children.push(ChildOfElement::Element(new_leaf_node));
942
2
                    is_last_mtext = true;
943
2
                }
944
0
            }
945
        };
946
947
        // clean up whitespace in text nodes
948
3
        for child in 
&mut new_children1
{
949
3
            if let Some(element) = child.element() && is_leaf(element) {
950
2
                let text = as_text(element);
951
2
                let cleaned_text = WHITESPACE_MATCH.replace_all(text, " ").trim_matches(WHITESPACE).to_string();
952
2
                element.set_text(&cleaned_text);
953
2
            
}1
954
        }
955
        
956
1
        crate::canonicalize::set_mathml_name(mathml_leaf, "mrow");
957
1
        mathml_leaf.clear_children();
958
1
        mathml_leaf.append_children(new_children);
959
960
        // debug!("rewrite_and_flatten_embedded_mathml: flattened\n'{}'", mml_to_string(mathml_leaf));
961
1
        return true;
962
34.2k
    }
963
55.5k
}
964
965
// used for testing trim
966
/// returns Ok() if two Documents are equal or some info where they differ in the Err
967
#[allow(dead_code)]
968
11
fn is_same_doc(doc1: &Document, doc2: &Document) -> Result<()> {
969
    // assume 'e' doesn't have element children until proven otherwise
970
    // this means we keep Text children until we are proven they aren't needed
971
11
    if doc1.root().children().len() != doc2.root().children().len() {
972
0
        bail!(
973
            "Children of docs have {} != {} children",
974
0
            doc1.root().children().len(),
975
0
            doc2.root().children().len()
976
        );
977
11
    }
978
979
11
    for (i, (c1, c2)) in doc1
980
11
        .root()
981
11
        .children()
982
11
        .iter()
983
11
        .zip(doc2.root().children().iter())
984
11
        .enumerate()
985
    {
986
11
        match c1 {
987
11
            ChildOfRoot::Element(e1) => {
988
11
                if let ChildOfRoot::Element(e2) = c2 {
989
11
                    is_same_element(*e1, *e2, &[])
?1
;
990
                } else {
991
0
                    bail!("child #{}, first is element, second is something else", i);
992
                }
993
            }
994
0
            ChildOfRoot::Comment(com1) => {
995
0
                if let ChildOfRoot::Comment(com2) = c2 {
996
0
                    if com1.text() != com2.text() {
997
0
                        bail!("child #{} -- comment text differs", i);
998
0
                    }
999
                } else {
1000
0
                    bail!("child #{}, first is comment, second is something else", i);
1001
                }
1002
            }
1003
0
            ChildOfRoot::ProcessingInstruction(p1) => {
1004
0
                if let ChildOfRoot::ProcessingInstruction(p2) = c2 {
1005
0
                    if p1.target() != p2.target() || p1.value() != p2.value() {
1006
0
                        bail!("child #{} -- processing instruction differs", i);
1007
0
                    }
1008
                } else {
1009
0
                    bail!(
1010
                        "child #{}, first is processing instruction, second is something else",
1011
                        i
1012
                    );
1013
                }
1014
            }
1015
        }
1016
    }
1017
10
    return Ok(());
1018
11
}
1019
1020
/// returns Ok() if two Documents are equal or some info where they differ in the Err
1021
// Not really meant to be public -- used by tests in some packages
1022
#[allow(dead_code)]
1023
1.92k
pub fn is_same_element(e1: Element, e2: Element, ignore_attrs: &[&str]) -> Result<()> {
1024
1.92k
    enable_logs();
1025
1.92k
    if name(e1) != name(e2) {
1026
0
        bail!("Names not the same: {}, {}", name(e1), name(e2));
1027
1.92k
    }
1028
1029
    // assume 'e' doesn't have element children until proven otherwise
1030
    // this means we keep Text children until we are proven they aren't needed
1031
1.92k
    if e1.children().len() != e2.children().len() {
1032
0
        bail!(
1033
            "Children of {} have {} != {} children",
1034
0
            name(e1),
1035
0
            e1.children().len(),
1036
0
            e2.children().len()
1037
        );
1038
1.92k
    }
1039
1040
1.92k
    if let Err(
e0
) = attrs_are_same(e1.attributes(), e2.attributes(), ignore_attrs) {
1041
0
        bail!("In element {}, {}", name(e1), e);
1042
1.92k
    }
1043
1044
2.86k
    for (i, (c1, c2)) in 
e1.children().iter()1.92k
.
zip1.92k
(
e2.children().iter()1.92k
).
enumerate1.92k
() {
1045
2.86k
        match c1 {
1046
1.72k
            ChildOfElement::Element(child1) => {
1047
1.72k
                if let ChildOfElement::Element(child2) = c2 {
1048
1.72k
                    is_same_element(*child1, *child2, ignore_attrs)
?2
;
1049
                } else {
1050
0
                    bail!("{} child #{}, first is element, second is something else", name(e1), i);
1051
                }
1052
            }
1053
0
            ChildOfElement::Comment(com1) => {
1054
0
                if let ChildOfElement::Comment(com2) = c2 {
1055
0
                    if com1.text() != com2.text() {
1056
0
                        bail!("{} child #{} -- comment text differs", name(e1), i);
1057
0
                    }
1058
                } else {
1059
0
                    bail!("{} child #{}, first is comment, second is something else", name(e1), i);
1060
                }
1061
            }
1062
0
            ChildOfElement::ProcessingInstruction(p1) => {
1063
0
                if let ChildOfElement::ProcessingInstruction(p2) = c2 {
1064
0
                    if p1.target() != p2.target() || p1.value() != p2.value() {
1065
0
                        bail!("{} child #{} -- processing instruction differs", name(e1), i);
1066
0
                    }
1067
                } else {
1068
0
                    bail!(
1069
                        "{} child #{}, first is processing instruction, second is something else",
1070
0
                        name(e1),
1071
                        i
1072
                    );
1073
                }
1074
            }
1075
1.14k
            ChildOfElement::Text(t1) => {
1076
1.14k
                if let ChildOfElement::Text(t2) = c2 {
1077
1.14k
                    if t1.text() != t2.text() {
1078
1
                        bail!("{} child #{} --  text differs", name(e1), i);
1079
1.14k
                    }
1080
                } else {
1081
0
                    bail!("{} child #{}, first is text, second is something else", name(e1), i);
1082
                }
1083
            }
1084
        }
1085
    }
1086
1.91k
    return Ok(());
1087
1088
    /// compares attributes -- '==' didn't seems to work
1089
1.92k
    fn attrs_are_same(attrs1: Vec<Attribute>, attrs2: Vec<Attribute>, ignore: &[&str]) -> Result<()> {
1090
1.92k
        let attrs1 = attrs1.iter()
1091
1.92k
                .filter(|a| !
ignore1.40k
.
contains1.40k
(
&a.name().local_part()1.40k
)).cloned()
1092
1.92k
                .collect::<Vec<Attribute>>();
1093
1.92k
        let attrs2 = attrs2.iter()
1094
1.92k
                .filter(|a| !
ignore1.40k
.
contains1.40k
(
&a.name().local_part()1.40k
)).cloned()
1095
1.92k
                .collect::<Vec<Attribute>>();
1096
1.92k
        if attrs1.len() != attrs2.len() {
1097
0
            bail!("Attributes have different length: {:?} != {:?}", attrs1, attrs2);
1098
1.92k
        }
1099
        // can't guarantee attrs are in the same order
1100
1.92k
        for 
attr11.40k
in attrs1 {
1101
1.40k
            if let Some(found_attr2) = attrs2
1102
1.40k
                .iter()
1103
1.88k
                .
find1.40k
(|&attr2| attr1.name().local_part() == attr2.name().local_part())
1104
            {
1105
1.40k
                if attr1.value() == found_attr2.value() {
1106
1.40k
                    continue;
1107
                } else {
1108
0
                    bail!(
1109
                        "Attribute named {} has differing values:\n  '{}'\n  '{}'",
1110
0
                        attr1.name().local_part(),
1111
0
                        attr1.value(),
1112
0
                        found_attr2.value()
1113
                    );
1114
                }
1115
            } else {
1116
0
                bail!(
1117
                    "Attribute name {} not in [{}]",
1118
0
                    print_attr(&attr1),
1119
0
                    print_attrs(&attrs2)
1120
                );
1121
            }
1122
        }
1123
1.92k
        return Ok(());
1124
1125
0
        fn print_attr(attr: &Attribute) -> String {
1126
0
            return format!("@{}='{}'", attr.name().local_part(), attr.value());
1127
0
        }
1128
0
        fn print_attrs(attrs: &[Attribute]) -> String {
1129
0
            return attrs.iter().map(print_attr).collect::<Vec<String>>().join(", ");
1130
0
        }
1131
1.92k
    }
1132
1.92k
}
1133
1134
#[cfg(test)]
1135
mod tests {
1136
    #[allow(unused_imports)]
1137
    use super::super::init_logger;
1138
    use super::*;
1139
1140
10
    fn are_parsed_strs_equal(test: &str, target: &str) -> bool {
1141
10
        let test_package = &parser::parse(test).expect("Failed to parse input");
1142
10
        let test_doc = test_package.as_document();
1143
10
        trim_doc(&test_doc);
1144
10
        debug!("test:\n{}", 
mml_to_string0
(
get_element0
(
test_package0
)));
1145
1146
10
        let target_package = &parser::parse(target).expect("Failed to parse input");
1147
10
        let target_doc = target_package.as_document();
1148
10
        trim_doc(&target_doc);
1149
10
        debug!("target:\n{}", 
mml_to_string0
(
get_element0
(
target_package0
)));
1150
1151
10
        match is_same_doc(&test_doc, &target_doc) {
1152
10
            Ok(_) => return true,
1153
0
            Err(e) => panic!("{}", e),
1154
        }
1155
10
    }
1156
1157
    #[test]
1158
1
    fn trim_same() {
1159
1
        let trimmed_str = "<math><mrow><mo>-</mo><mi>a</mi></mrow></math>";
1160
1
        assert!(are_parsed_strs_equal(trimmed_str, trimmed_str));
1161
1
    }
1162
1163
    #[test]
1164
1
    fn trim_whitespace() {
1165
1
        let trimmed_str = "<math><mrow><mo>-</mo><mi> a </mi></mrow></math>";
1166
1
        let whitespace_str = "<math> <mrow ><mo>-</mo><mi> a </mi></mrow ></math>";
1167
1
        assert!(are_parsed_strs_equal(trimmed_str, whitespace_str));
1168
1
    }
1169
1170
    #[test]
1171
1
    fn no_trim_whitespace_nbsp() {
1172
1
        let trimmed_str = "<math><mrow><mo>-</mo><mtext> &#x00A0;a </mtext></mrow></math>";
1173
1
        let whitespace_str = "<math> <mrow ><mo>-</mo><mtext> &#x00A0;a </mtext></mrow ></math>";
1174
1
        assert!(are_parsed_strs_equal(trimmed_str, whitespace_str));
1175
1
    }
1176
1177
    #[test]
1178
1
    fn trim_comment() {
1179
1
        let whitespace_str = "<math> <mrow ><mo>-</mo><mi> a </mi></mrow ></math>";
1180
1
        let comment_str = "<math><mrow><mo>-</mo><!--a comment --><mi> a </mi></mrow></math>";
1181
1
        assert!(are_parsed_strs_equal(comment_str, whitespace_str));
1182
1
    }
1183
1184
    #[test]
1185
1
    fn replace_mglyph() {
1186
1
        let mglyph_str = "<math>
1187
1
                <mrow>
1188
1
                    <mi>X<mglyph fontfamily='my-braid-font' index='2' alt='23braid' /></mi>
1189
1
                    <mo>+</mo>
1190
1
                    <mi>
1191
1
                        <mglyph fontfamily='my-braid-font' index='5' alt='132braid' />Y
1192
1
                    </mi>
1193
1
                    <mo>=</mo>
1194
1
                    <mi>
1195
1
                        <mglyph fontfamily='my-braid-font' index='3' alt='13braid' />
1196
1
                    </mi>
1197
1
                </mrow>
1198
1
            </math>";
1199
1
        let result_str = "<math>
1200
1
            <mrow>
1201
1
                <mi>X23braid</mi>
1202
1
                <mo>+</mo>
1203
1
                <mi>132braidY</mi>
1204
1
                <mo>=</mo>
1205
1
                <mi>13braid</mi>
1206
1
            </mrow>
1207
1
        </math>";
1208
1
        assert!(are_parsed_strs_equal(mglyph_str, result_str));
1209
1
    }
1210
1211
    #[test]
1212
1
    fn trim_differs() {
1213
1
        let whitespace_str = "<math> <mrow ><mo>-</mo><mi> a </mi></mrow ></math>";
1214
1
        let different_str = "<math> <mrow ><mo>-</mo><mi> b </mi></mrow ></math>";
1215
1216
        // need to manually do this since failure shouldn't be a panic
1217
1
        let package1 = &parser::parse(whitespace_str).expect("Failed to parse input");
1218
1
        let doc1 = package1.as_document();
1219
1
        trim_doc(&doc1);
1220
1
        debug!("doc1:\n{}", 
mml_to_string0
(
get_element0
(
package10
)));
1221
1222
1
        let package2 = parser::parse(different_str).expect("Failed to parse input");
1223
1
        let doc2 = package2.as_document();
1224
1
        trim_doc(&doc2);
1225
1
        debug!("doc2:\n{}", 
mml_to_string0
(
get_element0
(
&package20
)));
1226
1227
1
        assert!(is_same_doc(&doc1, &doc2).is_err());
1228
1
    }
1229
1230
    #[test]
1231
1
    fn test_entities() {
1232
        // this forces initialization
1233
1
        set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
1234
1235
1
        let entity_str = set_mathml("<math><mrow><mo>&minus;</mo><mi>&mopf;</mi></mrow></math>").unwrap();
1236
1
        let converted_str =
1237
1
            set_mathml("<math><mrow><mo>&#x02212;</mo><mi>&#x1D55E;</mi></mrow></math>").unwrap();
1238
1239
        // need to remove unique ids
1240
1
        static ID_MATCH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"id='.+?' "#).unwrap());
1241
1
        let entity_str = ID_MATCH.replace_all(&entity_str, "");
1242
1
        let converted_str = ID_MATCH.replace_all(&converted_str, "");
1243
1
        assert_eq!(entity_str, converted_str, "normal entity test failed");
1244
1245
1
        let entity_str = set_mathml(
1246
            "<math data-quot=\"&quot;value&quot;\" data-apos='&apos;value&apos;'><mi>XXX</mi></math>",
1247
        )
1248
1
        .unwrap();
1249
1
        let converted_str =
1250
1
            set_mathml("<math data-quot='\"value\"' data-apos=\"'value'\"><mi>XXX</mi></math>").unwrap();
1251
1
        let entity_str = ID_MATCH.replace_all(&entity_str, "");
1252
1
        let converted_str = ID_MATCH.replace_all(&converted_str, "");
1253
1
        assert_eq!(entity_str, converted_str, "special entities quote test failed");
1254
1255
1
        let entity_str =
1256
1
            set_mathml("<math><mo>&lt;</mo><mo>&gt;</mo><mtext>&amp;lt;</mtext></math>").unwrap();
1257
1
        let converted_str =
1258
1
            set_mathml("<math><mo>&#x003C;</mo><mo>&#x003E;</mo><mtext>&#x0026;lt;</mtext></math>")
1259
1
                .unwrap();
1260
1
        let entity_str = ID_MATCH.replace_all(&entity_str, "");
1261
1
        let converted_str = ID_MATCH.replace_all(&converted_str, "");
1262
1
        assert_eq!(entity_str, converted_str, "special entities <,>,& test failed");
1263
1
    }
1264
1265
    #[test]
1266
1
    fn can_recover_from_invalid_set_rules_dir() {
1267
        use std::env;
1268
        // MathCAT will check the env var "MathCATRulesDir" as an override, so the following test might succeed if we don't override the env var
1269
1
        unsafe { env::set_var("MathCATRulesDir", "MathCATRulesDir"); }   // safe because we are single threaded
1270
1
        assert!(set_rules_dir("someInvalidRulesDir").is_err());
1271
1
        assert!(
1272
1
            set_rules_dir(super::super::abs_rules_dir_path()).is_ok(),
1273
            "\nset_rules_dir to '{}' failed",
1274
0
            super::super::abs_rules_dir_path()
1275
        );
1276
1
        assert!(set_mathml("<math><mn>1</mn></math>").is_ok());
1277
1
    }
1278
1279
    #[test]
1280
1
    fn single_html_in_mtext() {
1281
1
        let test = "<math><mn>1</mn> <mtext>a<p> para  1</p>bc</mtext> <mi>y</mi></math>";
1282
1
        let target = "<math><mn>1</mn> <mtext>a para 1bc</mtext> <mi>y</mi></math>";
1283
1
        assert!(are_parsed_strs_equal(test, target));
1284
1
    }
1285
1286
    #[test]
1287
1
    fn multiple_html_in_mtext() {
1288
1
        let test = "<math><mn>1</mn> <mtext>a<p>para 1</p> <p>para 2</p>bc  </mtext> <mi>y</mi></math>";
1289
1
        let target = "<math><mn>1</mn> <mtext>apara 1 para 2bc</mtext> <mi>y</mi></math>";
1290
1
        assert!(are_parsed_strs_equal(test, target));
1291
1
    }
1292
1293
    #[test]
1294
1
    fn nested_html_in_mtext() {
1295
1
        let test = "<math><mn>1</mn> <mtext>a <ol><li>first</li><li>second</li></ol> bc</mtext> <mi>y</mi></math>";
1296
1
        let target = "<math><mn>1</mn> <mtext>a firstsecond bc</mtext> <mi>y</mi></math>";
1297
1
        assert!(are_parsed_strs_equal(test, target));
1298
1
    }
1299
1300
    #[test]
1301
1
    fn empty_html_in_mtext() {
1302
1
        let test = "<math><mn>1</mn> <mtext>a<br/>bc</mtext> <mi>y</mi></math>";
1303
1
        let target = "<math><mn>1</mn> <mtext>abc</mtext> <mi>y</mi></math>";
1304
1
        assert!(are_parsed_strs_equal(test, target));
1305
1
    }
1306
1307
    #[test]
1308
1
    fn mathml_in_mtext() {
1309
1
        let test = "<math><mtext>if&#xa0;<math> <msup><mi>n</mi><mn>2</mn></msup></math>&#xa0;is real</mtext></math>";
1310
1
        let target = "<math><mrow><mtext>if&#xa0;</mtext><msup><mi>n</mi><mn>2</mn></msup><mtext>&#xa0;is real</mtext></mrow></math>";
1311
1
        assert!(are_parsed_strs_equal(test, target));
1312
1
    }
1313
1314
    #[test]
1315
1
    fn stack_overflow_protection() {
1316
1
        set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
1317
1
        let mut bad_mathml = String::from("<math>");
1318
513
        for _ in 
0..MAX_DEPTH+11
{
1319
513
            bad_mathml.push_str("<msqrt><mi>n</mi>");
1320
513
        }
1321
513
        for _ in 
0..MAX_DEPTH+11
{
1322
513
            bad_mathml.push_str("</msqrt>");
1323
513
        }
1324
1
        bad_mathml.push_str("</math>");
1325
1
        assert_eq!(set_mathml(bad_mathml).unwrap_err().to_string(), "MathML is too deeply nested to process");
1326
1
    }
1327
1328
    #[test]
1329
1
    fn old_mathml_cleared_on_error() {
1330
1
        set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
1331
1
        let good_mathml = "<math><mn>3</mn></math>";
1332
1
        set_mathml(good_mathml).unwrap();
1333
1
        let bad_mathml = "<math><mi>&xabc;</mi></math>";
1334
1
        assert!(set_mathml(bad_mathml).is_err());
1335
1
        assert!(get_spoken_text().unwrap() == "");
1336
1
        set_mathml(good_mathml).unwrap();
1337
1
        let bad_mathml = "<math>garbage";
1338
1
        assert!(set_mathml(bad_mathml).is_err());
1339
1
        assert!(get_spoken_text().unwrap() == "");
1340
1
    }
1341
}