## 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
    }