## 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/speech.rs

    Line
    Count
    Source
    1
    //! The speech module is where the speech rules are read in and speech generated.
    2
    //!
    3
    //! The speech rules call out to the preferences and tts modules and the dividing line is not always clean.
    4
    //! A number of useful utility functions used by other modules are defined here.
    5
    #![allow(clippy::needless_return)]
    6
    use std::path::PathBuf;
    7
    use std::collections::HashMap;
    8
    use std::cell::{RefCell, RefMut};
    9
    use std::sync::LazyLock;
    10
    use sxd_document::dom::{ChildOfElement, Document, Element};
    11
    use sxd_document::{Package, QName};
    12
    use sxd_xpath::context::Evaluation;
    13
    use sxd_xpath::{Factory, Value, XPath};
    14
    use sxd_xpath::nodeset::Node;
    15
    use std::fmt;
    16
    use std::time::SystemTime;
    17
    use crate::definitions::read_definitions_file;
    18
    use crate::errors::*;
    19
    use crate::prefs::*;
    20
    use crate::xpath_functions::is_leaf;
    21
    use yaml_rust::{YamlLoader, Yaml, yaml::Hash};
    22
    use crate::tts::*;
    23
    use crate::infer_intent::*;
    24
    use crate::pretty_print::{mml_to_string, yaml_to_string};
    25
    use std::path::Path;
    26
    use std::rc::Rc;
    27
    use crate::shim_filesystem::{read_to_string_shim, canonicalize_shim};
    28
    use crate::canonicalize::{as_element, create_mathml_element, set_mathml_name, name, MATHML_FROM_NAME_ATTR};
    29
    use regex::Regex;
    30
    use log::{debug, error, info};
    31
    32
    33
    pub const NAV_NODE_SPEECH_NOT_FOUND: &str = "NAV_NODE_NOT_FOUND";
    34
    35
    /// Like lisp's ' (quote foo), this is used to block "replace_chars" being called.
    36
    ///   Unlike lisp, this appended to the end of a string (more efficient)
    37
    /// At the moment, the only use is BrailleChars(...) -- internally, it calls replace_chars and we don't want it called again.
    38
    /// Note: an alternative to this hack is to add "xq" (execute but don't eval the result), but that's heavy-handed for the current need
    39
    const NO_EVAL_QUOTE_CHAR: char = '\u{efff}';            // a private space char
    40
    const NO_EVAL_QUOTE_CHAR_AS_BYTES: [u8;3] = [0xee,0xbf,0xbf];
    41
    const N_BYTES_NO_EVAL_QUOTE_CHAR: usize = NO_EVAL_QUOTE_CHAR.len_utf8();
    42
    43
    /// Converts 'string' into a "quoted" string -- use is_quoted_string and unquote_string
    44
    12.5k
    pub fn make_quoted_string(mut string: String) -> String {
    45
    12.5k
        string.push(NO_EVAL_QUOTE_CHAR);
    46
    12.5k
        return string;
    47
    12.5k
    }
    48
    49
    /// Checks the string to see if it is "quoted"
    50
    58.0k
    pub fn is_quoted_string(str: &str) -> bool {
    51
    58.0k
        if str.len() < N_BYTES_NO_EVAL_QUOTE_CHAR {
    52
    34.1k
            return false;
    53
    23.9k
        }
    54
    23.9k
        let bytes = str.as_bytes();
    55
    23.9k
        return bytes[bytes.len()-N_BYTES_NO_EVAL_QUOTE_CHAR..] == NO_EVAL_QUOTE_CHAR_AS_BYTES;
    56
    58.0k
    }
    57
    58
    /// Converts 'string' into a "quoted" string -- use is_quoted_string and unquote_string
    59
    /// IMPORTANT: this assumes the string is quoted -- no check is made
    60
    12.5k
    pub fn unquote_string(str: &str) -> &str {
    61
    12.5k
        return &str[..str.len()-N_BYTES_NO_EVAL_QUOTE_CHAR];
    62
    12.5k
    }
    63
    64
    65
    /// The main external call, `intent_from_mathml` returns a string for the speech associated with the `mathml`.
    66
    ///   It matches against the rules that are computed by user prefs such as "Language" and "SpeechStyle".
    67
    ///
    68
    /// The speech rules assume `mathml` has been "cleaned" via the canonicalization step.
    69
    ///
    70
    /// If the preferences change (and hence the speech rules to use change), or if the rule file changes,
    71
    ///   `intent_from_mathml` will detect that and (re)load the proper rules.
    72
    ///
    73
    /// A string is returned in call cases.
    74
    /// If there is an error, the speech string will indicate an error.
    75
    3.88k
    pub fn intent_from_mathml<'m>(mathml: Element, doc: Document<'m>) -> Result<Element<'m>> {
    76
    3.88k
        let intent_tree3.87k = intent_rules(&INTENT_RULES, doc, mathml, "")?9;
    77
    3.87k
        doc.root().append_child(intent_tree);
    78
    3.87k
        return Ok(intent_tree);
    79
    3.88k
    }
    80
    81
    3.96k
    pub fn speak_mathml(mathml: Element, nav_node_id: &str, nav_node_offset: usize) -> Result<String> {
    82
    3.96k
        return speak_rules(&SPEECH_RULES, mathml, nav_node_id, nav_node_offset);
    83
    3.96k
    }
    84
    85
    14
    pub fn overview_mathml(mathml: Element, nav_node_id: &str, nav_node_offset: usize) -> Result<String> {
    86
    14
        return speak_rules(&OVERVIEW_RULES, mathml, nav_node_id, nav_node_offset);
    87
    14
    }
    88
    89
    90
    3.88k
    fn intent_rules<'m>(rules: &'static std::thread::LocalKey<RefCell<SpeechRules>>, doc: Document<'m>, mathml: Element, nav_node_id: &'m str) -> Result<Element<'m>> {
    91
    3.88k
        rules.with(|rules| {
    92
    3.88k
            rules.borrow_mut().read_files()?0;
    93
    3.88k
            let rules = rules.borrow();
    94
            // debug!("intent_rules:\n{}", mml_to_string(mathml));
    95
    3.88k
            let should_set_literal_intent = rules.pref_manager.borrow().pref_to_string("SpeechStyle").as_str() == "LiteralSpeak";
    96
    3.88k
            let original_intent = mathml.attribute_value("intent");
    97
    3.88k
            if should_set_literal_intent {
    98
    10
                if let Some(intent4) = original_intent {
    99
    4
                    let intent = if intent.contains('(') {intent2.replace2('(', ":literal("2)} else {intent2.to_string() + ":literal"};
    100
    4
                    mathml.set_attribute_value("intent", &intent);
    101
    6
                } else {
    102
    6
                    mathml.set_attribute_value("intent", ":literal");
    103
    6
                };
    104
    3.87k
            }
    105
    3.88k
            let mut rules_with_context = SpeechRulesWithContext::new(&rules, doc, nav_node_id, 0);
    106
    3.88k
            let intent3.87k =  rules_with_context.match_pattern::<Element<'m>>(mathml)
    107
    3.88k
                        .context("Pattern match/replacement failure!")?9;
    108
    3.87k
            let answer = if name(intent) == "TEMP_NAME" {   // unneeded extra layer
    109
    0
                assert_eq!(intent.children().len(), 1);
    110
    0
                as_element(intent.children()[0])
    111
            } else {
    112
    3.87k
                intent
    113
            };
    114
    3.87k
            if should_set_literal_intent {
    115
    10
                if let Some(original_intent4) = original_intent {
    116
    4
                    mathml.set_attribute_value("intent", original_intent);
    117
    6
                } else {
    118
    6
                    mathml.remove_attribute("intent");
    119
    6
                }
    120
    3.86k
            }
    121
    3.87k
            return Ok(answer);
    122
    3.88k
        })
    123
    3.88k
    }
    124
    125
    /// Speak the MathML
    126
    /// If 'nav_node_id' is not an empty string, then the element with that id will have [[...]] around it
    127
    3.98k
    fn speak_rules(rules: &'static std::thread::LocalKey<RefCell<SpeechRules>>, mathml: Element, nav_node_id: &str, nav_node_offset: usize) -> Result<String> {
    128
    3.98k
        return rules.with(|rules| {
    129
    3.98k
            rules.borrow_mut().read_files()?0;
    130
    3.98k
            let rules = rules.borrow();
    131
            // debug!("speak_rules:\n{}", mml_to_string(mathml));
    132
    3.98k
            let new_package = Package::new();
    133
    3.98k
            let mut rules_with_context = SpeechRulesWithContext::new(&rules, new_package.as_document(), nav_node_id, nav_node_offset);
    134
    3.98k
            let speech_string3.98k = nestable_speak_rules(& mut rules_with_context, mathml)?1;
    135
    136
    3.98k
            return Ok( rules.pref_manager.borrow().get_tts()
    137
    3.98k
                .merge_pauses(remove_optional_indicators(
    138
    3.98k
                    &speech_string.replace(CONCAT_STRING, "")
    139
    3.98k
                                       .replace(CONCAT_INDICATOR, "")
    140
    3.98k
                                       .replace(POSTFIX_CONCAT_STRING, "")
    141
    3.98k
                                       .replace(POSTFIX_CONCAT_INDICATOR, "")
    142
    3.98k
                                )
    143
    3.98k
                .trim_start().trim_end_matches([' ', ',', ';'])) );
    144
    3.98k
        });
    145
    146
    3.99k
        fn nestable_speak_rules<'c, 's:'c, 'm:'c>(rules_with_context: &mut SpeechRulesWithContext<'c, 's, 'm>, mathml: Element<'c>) -> Result<String> {
    147
    3.99k
            let mut speech_string = rules_with_context.match_pattern::<String>(mathml)
    148
    3.99k
                        .context("Pattern match/replacement failure!")?0;
    149
            // Note: [[...]] is added around a matching child, but if the "id" is on 'mathml', the whole string is used
    150
    3.99k
            if !rules_with_context.nav_node_id.is_empty() {
    151
                // See https://github.com/NSoiffer/MathCAT/issues/174 for why we can just start the speech at the nav node
    152
    536
                let intent_attr = mathml.attribute_value("data-intent-property").unwrap_or_default();
    153
    536
                if let Some(start521) = speech_string.find("[[") {
    154
    521
                    match speech_string[start+2..].find("]]") {
    155
    0
                        None => bail!("Internal error: looking for '[[...]]' during navigation -- only found '[[' in '{}'", speech_string),
    156
    521
                        Some(end) => speech_string = speech_string[start+2..start+2+end].to_string(),
    157
                    }
    158
    15
                } else if !intent_attr.contains(":literal:") {
    159
                    // try again with LiteralSpeak -- some parts might have been elided in other SpeechStyles
    160
    14
                    mathml.set_attribute_value("data-intent-property", (":literal:".to_string() + intent_attr).as_str());
    161
    14
                    let speech = nestable_speak_rules(rules_with_context, mathml);
    162
    14
                    mathml.set_attribute_value("data-intent-property", intent_attr);
    163
    14
                    return speech;
    164
                } else {
    165
    1
                    bail!(NAV_NODE_SPEECH_NOT_FOUND); //  NAV_NODE_SPEECH_NOT_FOUND is tested for later
    166
                }
    167
    3.46k
            }
    168
    3.98k
            return Ok(speech_string);
    169
    3.99k
        }
    170
    3.98k
    }
    171
    172
    /// Converts its argument to a string that can be used in a debugging message.
    173
    0
    pub fn yaml_to_type(yaml: &Yaml) -> String {
    174
    0
        return match yaml {
    175
    0
            Yaml::Real(v)=> format!("real='{v:#}'"),
    176
    0
            Yaml::Integer(v)=> format!("integer='{v:#}'"),
    177
    0
            Yaml::String(v)=> format!("string='{v:#}'"),
    178
    0
            Yaml::Boolean(v)=> format!("boolean='{v:#}'"),
    179
    0
            Yaml::Array(v)=> match v.len() {
    180
    0
                0 => "array with no entries".to_string(),
    181
    0
                1 => format!("array with the entry: {}", yaml_to_type(&v[0])),
    182
    0
                _ => format!("array with {} entries. First entry: {}", v.len(), yaml_to_type(&v[0])),
    183
            }
    184
    0
            Yaml::Hash(h)=> {
    185
    0
                let first_pair =
    186
    0
                    if h.is_empty() {
    187
    0
                        "no pairs".to_string()
    188
                    } else {
    189
    0
                        let (key, val) = h.iter().next().unwrap();
    190
    0
                        format!("({}, {})", yaml_to_type(key), yaml_to_type(val))
    191
                    };
    192
    0
                format!("dictionary with {} pair{}. A pair: {}", h.len(), if h.len()==1 {""} else {"s"}, first_pair)
    193
            }
    194
    0
            Yaml::Alias(_)=> "Alias".to_string(),
    195
    0
            Yaml::Null=> "Null".to_string(),
    196
    0
            Yaml::BadValue=> "BadValue".to_string(),
    197
        }
    198
    0
    }
    199
    200
    0
    fn yaml_type_err(yaml: &Yaml, str: &str) -> Error {
    201
    0
        anyhow!("Expected {}, found {}", str, yaml_to_type(yaml))
    202
    0
    }
    203
    204
    // fn yaml_key_err(dict: &Yaml, key: &str, yaml_type: &str) -> String {
    205
    //     if dict.as_hash().is_none() {
    206
    //        return format!("Expected dictionary with key '{}', found\n{}", key, yaml_to_string(dict, 1));
    207
    //     }
    208
    //     let str = &dict[key];
    209
    //     if str.is_badvalue() {
    210
    //         return format!("Did not find '{}' in\n{}", key,  yaml_to_string(dict, 1));
    211
    //     }
    212
    //     return format!("Type of '{}' is not a {}.\nIt is a {}. YAML value is\n{}",
    213
    //             key, yaml_type, yaml_to_type(str), yaml_to_string(dict, 0));
    214
    // }
    215
    216
    4.86M
    fn find_str<'a>(dict: &'a Yaml, key: &'a str) -> Option<&'a str> {
    217
    4.86M
        return dict[key].as_str();
    218
    4.86M
    }
    219
    220
    /// Returns the Yaml as a `Hash` or an error if it isn't.
    221
    175k
    pub fn as_hash_checked(value: &Yaml) -> Result<&Hash> {
    222
    175k
        let result = value.as_hash();
    223
    175k
        let result = result.ok_or_else(|| yaml_type_err0(value0, "hashmap"0))?0;
    224
    175k
        return Ok( result );
    225
    175k
    }
    226
    227
    /// Returns the Yaml as a `Vec` or an error if it isn't.
    228
    11.7k
    pub fn as_vec_checked(value: &Yaml) -> Result<&Vec<Yaml>> {
    229
    11.7k
        let result = value.as_vec();
    230
    11.7k
        let result = result.ok_or_else(|| yaml_type_err0(value0, "array"0))?0;
    231
    11.7k
        return Ok( result );
    232
    11.7k
    }
    233
    234
    /// Returns the Yaml as a `&str` or an error if it isn't.
    235
    8.09M
    pub fn as_str_checked(yaml: &Yaml) -> Result<&str> {
    236
    8.09M
        return yaml.as_str().ok_or_else(|| yaml_type_err0(yaml0, "string"0));
    237
    8.09M
    }
    238
    239
    240
    /// A bit of a hack to concatenate replacements (without a ' ').
    241
    /// The CONCAT_INDICATOR is added by a "ct:" (instead of 't:') in the speech rules
    242
    /// and checked for by the tts code.
    243
    pub const CONCAT_INDICATOR: &str = "\u{F8FE}";
    244
    245
    // This is the pattern that needs to be matched (and deleted)
    246
    pub const CONCAT_STRING: &str = " \u{F8FE}";
    247
    248
    // a similar hack to delete a space afterward
    249
    pub const POSTFIX_CONCAT_INDICATOR: &str = "\u{F8FF}";
    250
    251
    // This is the pattern that needs to be matched (and deleted)
    252
    pub const POSTFIX_CONCAT_STRING: &str = "\u{F8FF} ";
    253
    254
    // a similar hack to potentially delete (repetitive) optional replacements
    255
    // the OPTIONAL_INDICATOR is added by "ot:" before and after the optional string
    256
    const OPTIONAL_INDICATOR: &str  = "\u{F8FD}";
    257
    const OPTIONAL_INDICATOR_LEN: usize = OPTIONAL_INDICATOR.len();
    258
    259
    5.10k
    pub fn remove_optional_indicators(str: &str) -> String {
    260
    5.10k
        return str.replace(OPTIONAL_INDICATOR, "");
    261
    5.10k
    }
    262
    263
    /// Given a string that should be Yaml, it calls `build_fn` with that string.
    264
    /// The build function/closure should process the Yaml as appropriate and capture any errors and write them to `std_err`.
    265
    /// The returned value should be a Vector containing the paths of all the files that were included.
    266
    56.4k
    pub fn compile_rule<F>(str: &str, mut build_fn: F) -> Result<Vec<PathBuf>> where
    267
    56.4k
                F: FnMut(&Yaml) -> Result<Vec<PathBuf>> {
    268
    56.4k
        let docs = YamlLoader::load_from_str(str);
    269
    56.4k
        match docs {
    270
    0
            Err(e) => {
    271
    0
                bail!("Parse error!!: {}", e);
    272
            },
    273
    56.4k
            Ok(docs) => {
    274
    56.4k
                if docs.len() != 1 {
    275
    0
                    bail!("Didn't find rules!");
    276
    56.4k
                }
    277
    56.4k
                return build_fn(&docs[0]);
    278
            }
    279
        }
    280
    56.4k
    }
    281
    282
    36.6k
    pub fn process_include<F>(current_file: &Path, new_file_name: &str, mut read_new_file: F) -> Result<Vec<PathBuf>>
    283
    36.6k
                        where F: FnMut(&Path) -> Result<Vec<PathBuf>> {
    284
    36.6k
        let parent_path = current_file.parent();
    285
    36.6k
        if parent_path.is_none() {
    286
    0
            bail!("Internal error: {:?} is not a valid file name", current_file);
    287
    36.6k
        }
    288
    36.6k
        let mut new_file = match canonicalize_shim(parent_path.unwrap()) {
    289
    36.6k
            Ok(path) => path,
    290
    0
            Err(e) => bail!("process_include: canonicalize failed for {} with message {}", parent_path.unwrap().display(), e),
    291
        };
    292
    293
        // the referenced file might be in a directory that hasn't been zipped up -- find the dir and call the unzip function
    294
    89.1k
        for unzip_dir in new_file.ancestors()36.6k {
    295
    89.1k
            if unzip_dir.ends_with("Rules") {
    296
    36.6k
                break;      // nothing to unzip
    297
    52.5k
            }
    298
    52.5k
            if unzip_dir.ends_with("Languages") || unzip_dir28.5k.ends_with28.5k("Braille") {
    299
                // get the subdir ...Rules/Braille/en/...
    300
                // could have ...Rules/Braille/definitions.yaml, so 'next()' doesn't exist in this case, but the file wasn't zipped up
    301
    26.0k
                if let Some(subdir25.0k) = new_file.strip_prefix(unzip_dir).unwrap().iter().next() {
    302
    25.0k
                    let default_lang = if unzip_dir.ends_with("Languages") {"en"23.9k} else {"UEB;"1.06k};
    303
    25.0k
                    PreferenceManager::unzip_files(unzip_dir, subdir.to_str().unwrap(), Some(default_lang)).unwrap_or_default();
    304
    1.06k
                }
    305
    26.4k
            }
    306
        }
    307
    36.6k
        new_file.push(new_file_name);
    308
    36.6k
        info!("...processing include: {new_file_name}...");
    309
    36.6k
        let new_file = match crate::shim_filesystem::canonicalize_shim(new_file.as_path()) {
    310
    36.6k
            Ok(buf) => buf,
    311
    0
            Err(msg) => bail!("-include: constructed file name '{}' causes error '{}'",
    312
    0
                                     new_file.to_str().unwrap(), msg),
    313
        };
    314
    315
    36.6k
        let mut included_files = read_new_file(new_file.as_path())?0;
    316
    36.6k
        let mut files_read = vec![new_file];
    317
    36.6k
        files_read.append(&mut included_files);
    318
    36.6k
        return Ok(files_read);
    319
    36.6k
    }
    320
    321
    /// As the name says, TreeOrString is either a Tree (Element) or a String
    322
    /// It is used to share code during pattern matching
    323
    pub trait TreeOrString<'c, 'm:'c, T> {
    324
        fn from_element(e: Element<'m>) -> Result<T>;
    325
        fn from_string(s: String, doc: Document<'m>) -> Result<T>;
    326
        fn replace_tts<'s:'c, 'r>(tts: &TTS, command: &TTSCommandRule, prefs: &PreferenceManager, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T>;
    327
        fn replace<'s:'c, 'r>(ra: &ReplacementArray, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T>;
    328
        fn replace_nodes<'s:'c, 'r>(rules: &'r mut SpeechRulesWithContext<'c, 's,'m>, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<T>;
    329
        fn highlight_braille(braille: T, highlight_style: String) -> T;
    330
        fn mark_nav_speech(speech: T) -> T;
    331
    }
    332
    333
    impl<'c, 'm:'c> TreeOrString<'c, 'm, String> for String {
    334
    0
        fn from_element(_e: Element<'m>) -> Result<String> {
    335
    0
             bail!("from_element not allowed for strings");
    336
    0
        }
    337
    338
    180k
        fn from_string(s: String, _doc: Document<'m>) -> Result<String> {
    339
    180k
            return Ok(s);
    340
    180k
        }
    341
    342
    60.7k
        fn replace_tts<'s:'c, 'r>(tts: &TTS, command: &TTSCommandRule, prefs: &PreferenceManager, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<String> {
    343
    60.7k
            return tts.replace_string(command, prefs, rules_with_context, mathml);
    344
    60.7k
        }
    345
    346
    142k
        fn replace<'s:'c, 'r>(ra: &ReplacementArray, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<String> {
    347
    142k
            return ra.replace_array_string(rules_with_context, mathml);
    348
    142k
        }
    349
    350
    72.9k
        fn replace_nodes<'s:'c, 'r>(rules: &'r mut SpeechRulesWithContext<'c, 's,'m>, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<String> {
    351
    72.9k
            return rules.replace_nodes_string(nodes, mathml);
    352
    72.9k
        }
    353
    354
    469
        fn highlight_braille(braille: String, highlight_style: String) -> String {
    355
    469
            return SpeechRulesWithContext::highlight_braille_string(braille, highlight_style);
    356
    469
        }
    357
    358
    521
        fn mark_nav_speech(speech: String) -> String {
    359
    521
            return SpeechRulesWithContext::mark_nav_speech(speech);
    360
    521
        }
    361
    }
    362
    363
    impl<'c, 'm:'c> TreeOrString<'c, 'm, Element<'m>> for Element<'m> {
    364
    48.0k
        fn from_element(e: Element<'m>) -> Result<Element<'m>> {
    365
    48.0k
             return Ok(e);
    366
    48.0k
        }
    367
    368
    213
        fn from_string(s: String, doc: Document<'m>) -> Result<Element<'m>> {
    369
            // FIX: is 'mi' really ok?  Don't want to use TEMP_NAME because this name needs to move to the outside world
    370
    213
            let leaf = create_mathml_element(&doc, "mi");
    371
    213
            leaf.set_text(&s);
    372
    213
            return Ok(leaf);
    373
    213
    }
    374
    375
    0
        fn replace_tts<'s:'c, 'r>(_tts: &TTS, _command: &TTSCommandRule, _prefs: &PreferenceManager, _rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, _mathml: Element<'c>) -> Result<Element<'m>> {
    376
    0
            bail!("Internal error: applying a TTS rule to a tree");
    377
    0
        }
    378
    379
    132k
        fn replace<'s:'c, 'r>(ra: &ReplacementArray, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<Element<'m>> {
    380
    132k
            return ra.replace_array_tree(rules_with_context, mathml);
    381
    132k
        }
    382
    383
    48.6k
        fn replace_nodes<'s:'c, 'r>(rules: &'r mut SpeechRulesWithContext<'c, 's,'m>, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<Element<'m>> {
    384
    48.6k
            return rules.replace_nodes_tree(nodes, mathml);
    385
    48.6k
        }
    386
    387
    0
        fn highlight_braille(_braille: Element<'c>, _highlight_style: String) -> Element<'m> {
    388
    0
            panic!("Internal error: highlight_braille called on a tree");
    389
        }
    390
    391
    0
        fn mark_nav_speech(_speech: Element<'c>) -> Element<'m> {
    392
    0
            panic!("Internal error: mark_nav_speech called on a tree");
    393
        }
    394
    }
    395
    396
    /// 'Replacement' is an enum that contains all the potential replacement types/structs
    397
    /// Hence there are fields 'Test' ("test:"), 'Text" ("t:"), "XPath", etc
    398
    #[derive(Debug, Clone)]
    399
    #[allow(clippy::upper_case_acronyms)]
    400
    enum Replacement {
    401
        // Note: all of these are pointer types
    402
        Text(String),
    403
        XPath(MyXPath),
    404
        Intent(Box<Intent>),
    405
        Test(Box<TestArray>),
    406
        TTS(Box<TTSCommandRule>),
    407
        With(Box<With>),
    408
        SetVariables(Box<SetVariables>),
    409
        Insert(Box<InsertChildren>),
    410
        Translate(TranslateExpression),
    411
    }
    412
    413
    impl fmt::Display for Replacement {
    414
    10
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    415
    10
            return write!(f, "{}",
    416
    10
                match self {
    417
    0
                    Replacement::Test(c) => c.to_string(),
    418
    0
                    Replacement::Text(t) => format!("t: \"{t}\""),
    419
    10
                    Replacement::XPath(x) => x.to_string(),
    420
    0
                    Replacement::Intent(i) => i.to_string(),
    421
    0
                    Replacement::TTS(t) => t.to_string(),
    422
    0
                    Replacement::With(w) => w.to_string(),
    423
    0
                    Replacement::SetVariables(v) => v.to_string(),
    424
    0
                    Replacement::Insert(ic) => ic.to_string(),
    425
    0
                    Replacement::Translate(x) => x.to_string(),
    426
                }
    427
            );
    428
    10
        }
    429
    }
    430
    431
    impl Replacement {
    432
    13.5M
        fn build(replacement: &Yaml) -> Result<Replacement> {
    433
            // Replacement -- single key/value (see below for allowed values)
    434
    13.5M
            let dictionary = replacement.as_hash();
    435
    13.5M
            if dictionary.is_none() {
    436
    0
                bail!("  expected a key/value pair. Found {}.",  yaml_to_string(replacement, 0));
    437
    13.5M
            };
    438
    13.5M
            let dictionary = dictionary.unwrap();
    439
    13.5M
            if dictionary.is_empty() {
    440
    0
                bail!("No key/value pairs found for key 'replace'.\n\
    441
                    Suggestion: are the following lines indented properly?");
    442
    13.5M
            }
    443
    13.5M
            if dictionary.len() > 1 {
    444
    0
                bail!("Should only be one key/value pair for the replacement.\n    \
    445
                        Suggestion: are the following lines indented properly?\n    \
    446
    0
                        The key/value pairs found are\n{}", yaml_to_string(replacement, 2));
    447
    13.5M
            }
    448
    449
            // get the single value
    450
    13.5M
            let (key, value) = dictionary.iter().next().unwrap();
    451
    13.5M
            let key = key.as_str().ok_or_else(|| anyhow!0("replacement key(e.g, 't') is not a string"))?0;
    452
    13.5M
            match key {
    453
    13.5M
                "t" | "T"10.5M => {
    454
    5.75M
                    return Ok( Replacement::Text( as_str_checked(value)?0.to_string() ) );
    455
                },
    456
    7.78M
                "ct" | "CT"7.75M => {
    457
    24.7k
                    return Ok( Replacement::Text( CONCAT_INDICATOR.to_string() + as_str_checked(value)?0 ) );
    458
                },
    459
    7.75M
                "tc" | "TC"7.74M => {
    460
    6.93k
                    return Ok( Replacement::Text( as_str_checked(value)?0.to_string() + POSTFIX_CONCAT_INDICATOR ) );
    461
                },
    462
    7.74M
                "ot" | "OT"7.71M => {
    463
    36.3k
                    return Ok( Replacement::Text( OPTIONAL_INDICATOR.to_string() + as_str_checked(value)?0 + OPTIONAL_INDICATOR ) );
    464
                },
    465
    7.71M
                "x" => {
    466
    2.28M
                    return Ok( Replacement::XPath( MyXPath::build(value)
    467
    2.28M
                        .context("while trying to evaluate value of 'x:'")?0 ) );
    468
                },
    469
    5.42M
                "pause" | "rate"4.59M | "pitch"4.59M | "volume"4.37M | "audio"4.37M | "gender"4.15M | "voice"4.15M | "spell"4.15M | "SPELL"3.48M | "bookmark"3.18M | "pronounce"3.00M | "PRONOUNCE"3.00M => {
    470
    2.42M
                    return Ok( Replacement::TTS( TTS::build(&key.to_ascii_lowercase(), value)?0 ) );
    471
                },
    472
    3.00M
                "intent" => {
    473
    284k
                    return Ok( Replacement::Intent( Intent::build(value)?0 ) );
    474
                },
    475
    2.71M
                "test" => {
    476
    2.59M
                    return Ok( Replacement::Test( Box::new( TestArray::build(value)?0 ) ) );
    477
                },
    478
    129k
                "with" => {
    479
    77.7k
                    return Ok( Replacement::With( With::build(value)?0 ) );
    480
                },
    481
    51.5k
                "set_variables" => {
    482
    30.4k
                    return Ok( Replacement::SetVariables( SetVariables::build(value)?0 ) );
    483
                },
    484
    21.0k
                "insert" => {
    485
    20.9k
                    return Ok( Replacement::Insert( InsertChildren::build(value)?0 ) );
    486
                },
    487
    104
                "translate" => {
    488
    104
                    return Ok( Replacement::Translate( TranslateExpression::build(value)
    489
    104
                        .context("while trying to evaluate value of 'speak:'")?0 ) );
    490
                },
    491
                _ => {
    492
    0
                    bail!("Unknown 'replace' command ({}) with value: {}", key, yaml_to_string(value, 0));
    493
                }
    494
            }
    495
    13.5M
        }
    496
    }
    497
    498
    // structure used when "insert:" is encountered in a rule
    499
    // the 'replacements' are inserted between each node in the 'xpath'
    500
    #[derive(Debug, Clone)]
    501
    struct InsertChildren {
    502
        xpath: MyXPath,                     // the replacement nodes
    503
        replacements: ReplacementArray,     // what is inserted between each node
    504
    }
    505
    506
    #[cfg_attr(coverage, coverage(off))]
    507
    impl fmt::Display for InsertChildren {
    508
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    509
            return write!(f, "InsertChildren:\n  nodes {}\n  replacements {}", self.xpath, &self.replacements);
    510
        }
    511
    }
    512
    513
    514
    impl InsertChildren {
    515
    20.9k
        fn build(insert: &Yaml) -> Result<Box<InsertChildren>> {
    516
            // 'insert:' -- 'nodes': xxx 'replace': xxx
    517
    20.9k
            if insert.as_hash().is_none() {
    518
    0
                bail!("")
    519
    20.9k
            }
    520
    20.9k
            let nodes = &insert["nodes"];
    521
    20.9k
            if nodes.is_badvalue() {
    522
    0
                bail!("Missing 'nodes' as part of 'insert'.\n    \
    523
                      Suggestion: add 'nodes:' or if present, indent so it is contained in 'insert'");
    524
    20.9k
            }
    525
    20.9k
            let nodes = as_str_checked(nodes)?0;
    526
    20.9k
            let replace = &insert["replace"];
    527
    20.9k
            if replace.is_badvalue() {
    528
    0
                bail!("Missing 'replace' as part of 'insert'.\n    \
    529
                      Suggestion: add 'replace:' or if present, indent so it is contained in 'insert'");
    530
    20.9k
            }
    531
    20.9k
            return Ok( Box::new( InsertChildren {
    532
    20.9k
                xpath: MyXPath::new(nodes.to_string())?0,
    533
    20.9k
                replacements: ReplacementArray::build(replace).context("'replace:'")?0,
    534
            } ) );
    535
    20.9k
        }
    536
    537
        // It would be most efficient to do an xpath eval, get the nodes (type: NodeSet) and then intersperse the node_replace()
    538
        //   calls with replacements for the ReplacementArray parts. But that causes problems with the "pause: auto" calculation because
    539
        //   the replacements are segmented (can't look to neighbors for the calculation there)
    540
        // An alternative is to introduce another Replacement enum value, but that's a lot of complication for not that much
    541
        //    gain (and Node's have contagious lifetimes)
    542
        // The solution adopted is to find out the number of nodes and build up MyXPaths with each node selected (e.g, "*" => "*[3]")
    543
        //    and put those nodes into a flat ReplacementArray and then do a standard replace on that.
    544
        //    This is slower than the alternatives, but reuses a bunch of code and hence is less complicated.
    545
    7.45k
        fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
    546
    7.45k
            let result = self.xpath.evaluate(&rules_with_context.context_stack.base, mathml)
    547
    7.45k
                    .with_context(||format!0("in '{}' replacing after pattern match", &self.xpath.rc.string0) )?0;
    548
    7.45k
            match result {
    549
    7.45k
                Value::Nodeset(nodes) => {
    550
    7.45k
                    if nodes.size() == 0 {
    551
    0
                        bail!("During replacement, no matching element found");
    552
    7.45k
                    };
    553
    7.45k
                    let nodes = nodes.document_order();
    554
    7.45k
                    let n_nodes = nodes.len();
    555
    7.45k
                    let mut expanded_result = Vec::with_capacity(n_nodes + (n_nodes+1)*self.replacements.replacements.len());
    556
    7.45k
                    expanded_result.push(
    557
                        Replacement::XPath(
    558
    7.45k
                            MyXPath::new(format!("{}[{}]", self.xpath.rc.string , 1))?0
    559
                        )
    560
                    );
    561
    19.3k
                    for i in 2..n_nodes+17.45k {
    562
    19.3k
                        expanded_result.extend_from_slice(&self.replacements.replacements);
    563
    19.3k
                        expanded_result.push(
    564
                            Replacement::XPath(
    565
    19.3k
                                MyXPath::new(format!("{}[{}]", self.xpath.rc.string , i))?0
    566
                            )
    567
                        );
    568
                    }
    569
    7.45k
                    let replacements = ReplacementArray{ replacements: expanded_result };
    570
    7.45k
                    return replacements.replace(rules_with_context, mathml);
    571
                },
    572
    573
                // FIX: should the options be errors???
    574
    0
                Value::String(t) => { return T::from_string(rules_with_context.replace_chars(&t, mathml)?, rules_with_context.doc); },
    575
    0
                Value::Number(num)  => { return T::from_string( num.to_string(), rules_with_context.doc ); },
    576
    0
                Value::Boolean(b)  => { return T::from_string( b.to_string(), rules_with_context.doc ); },          // FIX: is this right???
    577
            }
    578
    579
    7.45k
        }
    580
    }
    581
    582
    583
    2
    static ATTR_NAME_VALUE: LazyLock<Regex> = LazyLock::new(|| {
    584
    2
        Regex::new(
    585
            // match name='value', where name is sort of an NCNAME (see CONCEPT_OR_LITERAL in infer_intent.rs)
    586
            // The quotes can be either single or double quotes
    587
    2
            r#"(?P<name>[^\s\u{0}-\u{40}\[\\\]^`\u{7B}-\u{BF}][^\s\u{0}-\u{2C}/:;<=>?@\[\\\]^`\u{7B}-\u{BF}]*)\s*=\s*('(?P<value>[^']+)'|"(?P<dqvalue>[^"]+)")"#
    588
    2
        ).unwrap()
    589
    2
    });
    590
    591
    // structure used when "intent:" is encountered in a rule
    592
    // the name is either a string or an xpath that needs evaluation. 99% of the time it is a string
    593
    #[derive(Debug, Clone)]
    594
    struct Intent {
    595
        name: Option<String>,           // name of node
    596
        xpath: Option<MyXPath>,         // alternative to directly using the string
    597
        attrs: String,                  // optional attrs -- format "attr1='val1' [attr2='val2'...]"
    598
        children: ReplacementArray,     // children of node
    599
    }
    600
    601
    impl fmt::Display for Intent {
    602
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    603
    0
            let name = if let Some(name) = &self.name {
    604
    0
                name.to_string()
    605
            } else {
    606
    0
                self.xpath.as_ref().unwrap().to_string()
    607
            };
    608
    0
            return write!(f, "intent: {}: {},  attrs='{}'>\n      children: {}",
    609
    0
                            if self.name.is_some() {"name"} else {"xpath-name"}, name,
    610
                            self.attrs,
    611
    0
                            &self.children);
    612
    0
        }
    613
    }
    614
    615
    impl Intent {
    616
    284k
        fn build(yaml_dict: &Yaml) -> Result<Box<Intent>> {
    617
            // 'intent:' -- 'name': xxx 'children': xxx
    618
    284k
            if yaml_dict.as_hash().is_none() {
    619
    0
                bail!("Array found for contents of 'intent' -- should be dictionary with keys 'name' and 'children'")
    620
    284k
            }
    621
    284k
            let name = &yaml_dict["name"];
    622
    284k
            let xpath_name = &yaml_dict["xpath-name"];
    623
    284k
            if name.is_badvalue() && xpath_name31.6k.is_badvalue31.6k(){
    624
    0
                bail!("Missing 'name' or 'xpath-name' as part of 'intent'.\n    \
    625
                      Suggestion: add 'name:' or if present, indent so it is contained in 'intent'");
    626
    284k
            }
    627
    284k
            let attrs = &yaml_dict["attrs"];
    628
    284k
            let replace = &yaml_dict["children"];
    629
    284k
            if replace.is_badvalue() {
    630
    0
                bail!("Missing 'children' as part of 'intent'.\n    \
    631
                      Suggestion: add 'children:' or if present, indent so it is contained in 'intent'");
    632
    284k
            }
    633
    284k
            return Ok( Box::new( Intent {
    634
    284k
                name: if name.is_badvalue() {None31.6k} else {Some(as_str_checked252k(name252k).context252k("'name'")?0.to_string252k())},
    635
    284k
                xpath: if xpath_name.is_badvalue() {None252k} else {Some(MyXPath::build31.6k(xpath_name31.6k).context31.6k("'intent'")?0)},
    636
    284k
                attrs: if attrs.is_badvalue() {""129k.to_string129k()} else {as_str_checked155k(attrs155k).context155k("'attrs'")?0.to_string155k()},
    637
    284k
                children: ReplacementArray::build(replace).context("'children:'")?0,
    638
            } ) );
    639
    284k
        }
    640
    641
    45.5k
        fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
    642
    45.5k
            let result = self.children.replace::<Element<'m>>(rules_with_context, mathml)
    643
    45.5k
                        .context("replacing inside 'intent'")?0;
    644
    45.5k
            let mut result = lift_children(result);
    645
    45.5k
            if name(result) != "TEMP_NAME" && name(result) != "Unknown"3.43k {
    646
    235
                // this case happens when you have an 'intent' replacement as a direct child of an 'intent' replacement
    647
    235
                let temp = create_mathml_element(&result.document(), "TEMP_NAME");
    648
    235
                temp.append_child(result);
    649
    235
                result = temp;
    650
    45.3k
            }
    651
    45.5k
            if let Some(intent_name11.2k) = &self.name {
    652
    11.2k
                result.set_attribute_value(MATHML_FROM_NAME_ATTR, name(mathml));
    653
    11.2k
                set_mathml_name(result, intent_name.as_str());
    654
    34.2k
            }
    655
    45.5k
            if let Some(my_xpath34.2k) = &self.xpath{    // self.xpath_name must be != None
    656
    34.2k
                let xpath_value = my_xpath.evaluate(rules_with_context.get_context(), mathml)?0;
    657
    34.2k
                match xpath_value {
    658
    34.2k
                    Value::String(intent_name) => {
    659
    34.2k
                        result.set_attribute_value(MATHML_FROM_NAME_ATTR, name(mathml));
    660
    34.2k
                        set_mathml_name(result, intent_name.as_str())
    661
                    },
    662
    0
                    _ => bail!("'xpath-name' value '{}' was not a string", &my_xpath),
    663
                }
    664
    11.2k
            }
    665
    45.5k
            if self.name.is_none() && self.xpath34.2k.is_none34.2k() {
    666
    0
                bail!("Intent::replace: internal error -- neither 'name' nor 'xpath' is set");
    667
    45.5k
            };
    668
    669
    100k
            for attr in mathml45.5k.attributes45.5k() {
    670
    100k
                result.set_attribute_value(attr.name(), attr.value());
    671
    100k
            }
    672
    673
            // can't test against name == "math" because intent might a new element
    674
    45.5k
            if mathml.parent().is_some() && mathml.parent().unwrap().element().is_some() &&
    675
    41.7k
               result.attribute_value("id") == crate::canonicalize::get_parent(mathml).attribute_value("id") {
    676
    32
                // avoid duplicate ids -- it's a bug if it does, but this helps in that case
    677
    32
                result.remove_attribute("id");
    678
    45.5k
            }
    679
    680
    45.5k
            if !self.attrs.is_empty() {
    681
                // debug!("MathML after children, before attr processing:\n{}", mml_to_string(mathml));
    682
                // debug!("Result after children, before attr processing:\n{}", mml_to_string(result));
    683
                // debug!("Intent::replace attrs = \"{}\"", &self.attrs);
    684
    5.63k
                for cap in ATTR_NAME_VALUE5.58k.captures_iter(&self.attrs) {
    685
    5.63k
                    let matched_value = if cap["value"].is_empty() {&cap["dqvalue"]0} else {&cap["value"]};
    686
    5.63k
                    let value_as_xpath = MyXPath::new(matched_value.to_string()).context("attr value inside 'intent'")?0;
    687
    5.63k
                    let value = value_as_xpath.evaluate(rules_with_context.get_context(), result)
    688
    5.63k
                            .context("attr xpath evaluation value inside 'intent'")?0;
    689
    5.63k
                    let mut value = value.into_string();
    690
    5.63k
                    if &cap["name"] == INTENT_PROPERTY {
    691
    5.23k
                        value = simplify_fixity_properties(&value);
    692
    5.23k
                    }397
    693
                    // debug!("Intent::replace match\n  name={}\n  value={}\n  xpath value={}", &cap["name"], &cap["value"], &value);
    694
    5.63k
                    if &cap["name"] == INTENT_PROPERTY && value == ":"5.23k {
    695
    1.81k
                        // should have been an empty string, so remove the attribute
    696
    1.81k
                        result.remove_attribute(INTENT_PROPERTY);
    697
    3.82k
                    } else {
    698
    3.82k
                        result.set_attribute_value(&cap["name"], &value);
    699
    3.82k
                    }
    700
                };
    701
    39.9k
            }
    702
    703
            // debug!("Result from 'intent:'\n{}", mml_to_string(result));
    704
    45.5k
            return T::from_element(result);
    705
    706
    707
            /// "lift" up the children any "TEMP_NAME" child -- could short circuit when only one child
    708
    45.5k
            fn lift_children(result: Element) -> Element {
    709
                // debug!("lift_children:\n{}", mml_to_string(result));
    710
                // most likely there will be the same number of new children as result has, but there could be more
    711
    45.5k
                let mut new_children = Vec::with_capacity(2*result.children().len());
    712
    69.6k
                for child_of_element in result45.5k.children45.5k() {
    713
    69.6k
                    match child_of_element {
    714
    69.6k
                        ChildOfElement::Element(child) => {
    715
    69.6k
                            if name(child) == "TEMP_NAME" {
    716
    34.1k
                                new_children.append(&mut child.children());  // almost always just one
    717
    35.5k
                            } else {
    718
    35.5k
                                new_children.push(child_of_element);
    719
    35.5k
                            }
    720
                        },
    721
    7
                        _ => new_children.push(child_of_element),      // text()
    722
                    }
    723
                }
    724
    45.5k
                result.replace_children(new_children);
    725
    45.5k
                return result;
    726
    45.5k
            }
    727
    45.5k
        }
    728
    }
    729
    730
    // structure used when "with:" is encountered in a rule
    731
    // the variables are placed on (and later) popped of a variable stack before/after the replacement
    732
    #[derive(Debug, Clone)]
    733
    struct With {
    734
        variables: VariableDefinitions,     // variables and values
    735
        replacements: ReplacementArray,     // what to do with these vars
    736
    }
    737
    738
    #[cfg_attr(coverage, coverage(off))]
    739
    impl fmt::Display for With {
    740
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    741
            return write!(f, "with:\n      variables: {}\n      replace: {}", &self.variables, &self.replacements);
    742
        }
    743
    }
    744
    745
    746
    impl With {
    747
    77.7k
        fn build(vars_replacements: &Yaml) -> Result<Box<With>> {
    748
            // 'with:' -- 'variables': xxx 'replace': xxx
    749
    77.7k
            if vars_replacements.as_hash().is_none() {
    750
    0
                bail!("Array found for contents of 'with' -- should be dictionary with keys 'variables' and 'replace'")
    751
    77.7k
            }
    752
    77.7k
            let var_defs = &vars_replacements["variables"];
    753
    77.7k
            if var_defs.is_badvalue() {
    754
    0
                bail!("Missing 'variables' as part of 'with'.\n    \
    755
                      Suggestion: add 'variables:' or if present, indent so it is contained in 'with'");
    756
    77.7k
            }
    757
    77.7k
            let replace = &vars_replacements["replace"];
    758
    77.7k
            if replace.is_badvalue() {
    759
    0
                bail!("Missing 'replace' as part of 'with'.\n    \
    760
                      Suggestion: add 'replace:' or if present, indent so it is contained in 'with'");
    761
    77.7k
            }
    762
    77.7k
            return Ok( Box::new( With {
    763
    77.7k
                variables: VariableDefinitions::build(var_defs).context("'variables'")?0,
    764
    77.7k
                replacements: ReplacementArray::build(replace).context("'replace:'")?0,
    765
            } ) );
    766
    77.7k
        }
    767
    768
    7.28k
        fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
    769
    7.28k
            rules_with_context.context_stack.push(self.variables.clone(), mathml)?0;
    770
    7.28k
            let result = self.replacements.replace(rules_with_context, mathml)
    771
    7.28k
                        .context("replacing inside 'with'")?0;
    772
    7.28k
            rules_with_context.context_stack.pop();
    773
    7.28k
            return Ok( result );
    774
    7.28k
        }
    775
    }
    776
    777
    // structure used when "set_variables:" is encountered in a rule
    778
    // the variables are global and are placed in the base context and never popped off
    779
    #[derive(Debug, Clone)]
    780
    struct SetVariables {
    781
        variables: VariableDefinitions,     // variables and values
    782
    }
    783
    784
    #[cfg_attr(coverage, coverage(off))]
    785
    impl fmt::Display for SetVariables {
    786
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    787
            return write!(f, "SetVariables: variables {}", &self.variables);
    788
        }
    789
    }
    790
    791
    792
    impl SetVariables {
    793
    30.4k
        fn build(vars: &Yaml) -> Result<Box<SetVariables>> {
    794
            // 'set_variables:' -- 'variables': xxx (array)
    795
    30.4k
            if vars.as_vec().is_none() {
    796
    0
                bail!("'set_variables' -- should be an array of variable name, xpath value");
    797
    30.4k
            }
    798
    30.4k
            return Ok( Box::new( SetVariables {
    799
    30.4k
                variables: VariableDefinitions::build(vars).context("'set_variables'")?0
    800
            } ) );
    801
    30.4k
        }
    802
    803
    3.78k
        fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
    804
    3.78k
            rules_with_context.context_stack.set_globals(self.variables.clone(), mathml)?0;
    805
    3.78k
            return T::from_string( "".to_string(), rules_with_context.doc );
    806
    3.78k
        }
    807
    }
    808
    809
    810
    /// Allow speech of an expression in the middle of a rule (used by "WhereAmI" for navigation)
    811
    #[derive(Debug, Clone)]
    812
    struct TranslateExpression {
    813
        xpath: MyXPath,     // variables and values
    814
    }
    815
    816
    #[cfg_attr(coverage, coverage(off))]
    817
    impl fmt::Display for TranslateExpression {
    818
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    819
            return write!(f, "speak: {}", &self.xpath);
    820
        }
    821
    }
    822
    823
    824
    impl TranslateExpression {
    825
    104
        fn build(vars: &Yaml) -> Result<TranslateExpression> {
    826
            // 'translate:' -- xpath (should evaluate to an id)
    827
    104
            return Ok( TranslateExpression { xpath: MyXPath::build(vars).context("'translate'")?0 } );
    828
    104
        }
    829
    830
    2
        fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
    831
    2
            if self.xpath.rc.string.starts_with('@') {
    832
    2
                let xpath_value = self.xpath.evaluate(rules_with_context.get_context(), mathml)?0;
    833
    2
                let id = match xpath_value {
    834
    0
                    Value::String(s) => Some(s),
    835
    2
                    Value::Nodeset(nodes) => {
    836
    2
                        if nodes.size() == 1 {
    837
    2
                            nodes.document_order_first().unwrap().attribute().map(|attr| attr.value().to_string())
    838
                        } else {
    839
    0
                            None
    840
                        }
    841
                    },
    842
    0
                    _ => None,
    843
                };
    844
    2
                match id {
    845
    0
                    None => bail!("'translate' value '{}' is not a string or an attribute value (correct by using '@id'??):\n", self.xpath),
    846
    2
                    Some(id) => {
    847
    2
                        let speech = speak_mathml(mathml, &id, 0)?0;
    848
    2
                        return T::from_string(speech, rules_with_context.doc);
    849
                    }
    850
                }
    851
            } else {
    852
    0
                return T::from_string(
    853
    0
                    self.xpath.replace(rules_with_context, mathml).context("'translate'")?,
    854
    0
                    rules_with_context.doc
    855
                );
    856
            }
    857
    2
        }
    858
    }
    859
    860
    861
    /// An array of rule `Replacement`s (text, xpath, tts commands, etc)
    862
    #[derive(Debug, Clone)]
    863
    pub struct ReplacementArray {
    864
        replacements: Vec<Replacement>
    865
    }
    866
    867
    impl fmt::Display for ReplacementArray {
    868
    1
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    869
    1
            return write!(f, "{}", self.pretty_print_replacements());
    870
    1
        }
    871
    }
    872
    873
    impl ReplacementArray {
    874
        /// Return an empty `ReplacementArray`
    875
    1.99M
        pub fn build_empty() -> ReplacementArray {
    876
    1.99M
            return ReplacementArray {
    877
    1.99M
                replacements: vec![]
    878
    1.99M
            }
    879
    1.99M
        }
    880
    881
        /// Convert a Yaml input into a [`ReplacementArray`].
    882
        /// Any errors are passed back out.
    883
    9.25M
        pub fn build(replacements: &Yaml) -> Result<ReplacementArray> {
    884
            // replacements is either a single replacement or an array of replacements
    885
    9.25M
            let result= if replacements.is_array() {
    886
    9.23M
                let replacements = replacements.as_vec().unwrap();
    887
    9.23M
                replacements
    888
    9.23M
                    .iter()
    889
    9.23M
                    .enumerate()    // useful for errors
    890
    13.5M
                    .map9.23M(|(i, r)| Replacement::build(r)
    891
    13.5M
                                .with_context(|| format!0("replacement #{} of {}", i+10, replacements0.len0())))
    892
    9.23M
                    .collect::<Result<Vec<Replacement>>>()?0
    893
            } else {
    894
    21.2k
                vec![ Replacement::build(replacements)?0]
    895
            };
    896
    897
    9.25M
            return Ok( ReplacementArray{ replacements: result } );
    898
    9.25M
        }
    899
    900
        /// Do all the replacements in `mathml` using `rules`.
    901
    275k
        pub fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
    902
    275k
            return T::replace(self, rules_with_context, mathml);
    903
    275k
        }
    904
    905
    142k
        pub fn replace_array_string<'c, 's:'c, 'm:'c>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<String> {
    906
            // loop over the replacements and build up a vector of strings, excluding empty ones.
    907
            // * eliminate any redundance
    908
            // * add/replace auto-pauses
    909
            // * join the remaining vector together
    910
    142k
            let mut replacement_strings = Vec::with_capacity(self.replacements.len());   // probably conservative guess
    911
    271k
            for replacement in self.replacements.iter()142k {
    912
    271k
                let string: String = rules_with_context.replace(replacement, mathml)?0;
    913
    271k
                if !string.is_empty() {
    914
    200k
                    replacement_strings.push(string);
    915
    200k
                }70.3k
    916
            }
    917
    918
    142k
            if replacement_strings.is_empty() {
    919
    12.9k
                return Ok( "".to_string() );
    920
    129k
            }
    921
            // delete an optional text that is repetitive
    922
            // we do this by looking for the optional text marker, and if present, check for repetition at end of previous string
    923
            // if repetitive, we delete the optional string
    924
            // if not, we leave the markers because the repetition might happen several "levels" up
    925
            // this could also be done in a final cleanup of the entire string (where we remove any markers),
    926
            //   but the match is harder (rust regex lacks look behind pattern match) and it is less efficient
    927
            // Note: we skip the first string since it can't be repetitive of something at this level
    928
    129k
            for i45.4k in 1..replacement_strings.len()-1 {
    929
    45.4k
                if let Some(bytes13) = is_repetitive(&replacement_strings[i-1], &replacement_strings[i])  {
    930
    13
                    replacement_strings[i] = bytes.to_string();
    931
    45.4k
                }
    932
            }
    933
    934
    200k
            for i in 0..replacement_strings.len()129k {
    935
    200k
                if replacement_strings[i].contains(PAUSE_AUTO_STR) {
    936
    19.5k
                    let before = if i == 0 {""194} else {&replacement_strings[i-1]19.3k};
    937
    19.5k
                    let after = if i+1 == replacement_strings.len() {""230} else {&replacement_strings[i+1]19.3k};
    938
    19.5k
                    replacement_strings[i] = replacement_strings[i].replace(
    939
    19.5k
                        PAUSE_AUTO_STR,
    940
    19.5k
                        &rules_with_context.speech_rules.pref_manager.borrow().get_tts().compute_auto_pause(&rules_with_context.speech_rules.pref_manager.borrow(), before, after));
    941
    181k
                }
    942
            }
    943
    944
            // join the strings together with spaces in between
    945
            // concatenation (removal of spaces) is saved for the top level because they otherwise are stripped at the wrong sometimes
    946
    129k
            return Ok( replacement_strings.join(" ") );
    947
    948
            /// delete an optional text (in 'next') that is repetitive at the end of 'prev'
    949
            /// we do this by looking for the optional text marker, and if present, check for repetition at end of previous string
    950
            /// if repetitive, we delete the optional string
    951
    45.4k
            fn is_repetitive<'a>(prev: &str, next: &'a str) -> Option<&'a str> {
    952
                // OPTIONAL_INDICATOR optionally surrounds the end of 'prev'(ignoring trailing whitespace)
    953
                // OPTIONAL_INDICATOR surrounds the start of 'next'
    954
                // minor optimization -- lots of short strings and the OPTIONAL_INDICATOR takes a few bytes, so skip the check for those strings
    955
    45.4k
                if next.len() <=  2 * OPTIONAL_INDICATOR_LEN {
    956
    14.2k
                    return None;
    957
    31.2k
                }
    958
    959
                // should be exactly one match -- ignore more than one for now
    960
    31.2k
                let i_start36 = next.find(OPTIONAL_INDICATOR)?31.2k;
    961
    36
                let start_repeat_word_in_next = &next[i_start + OPTIONAL_INDICATOR_LEN..];
    962
    36
                let i_end = start_repeat_word_in_next.find(OPTIONAL_INDICATOR)
    963
    36
                    .unwrap_or_else(|| panic!0("Internal error: missing end optional char -- text handling is corrupted!"));
    964
    36
                let repeat_word = &start_repeat_word_in_next[..i_end];
    965
                // debug!("check if '{}' is repetitive, end_index={}", repeat_word, i_end);
    966
                // debug!("   prev: '{}', next '{}'", prev, next);
    967
    968
    36
                let prev_trimmed = prev.trim_end();
    969
    36
                let ends_with_word = prev_trimmed.len() > repeat_word.len() && prev_trimmed35.ends_with35(repeat_word35);
    970
    36
                let ends_with_wrapped_word =
    971
    36
                    prev_trimmed
    972
    36
                        .strip_suffix(OPTIONAL_INDICATOR)
    973
    36
                        .and_then(|s| s0.strip_suffix0(repeat_word0))
    974
    36
                        .and_then(|s| s0.strip_suffix0(OPTIONAL_INDICATOR))
    975
    36
                        .is_some();
    976
    36
                if ends_with_word || ends_with_wrapped_word23 {
    977
                    // debug!("  is repetitive");
    978
    13
                    Some(start_repeat_word_in_next[i_end + OPTIONAL_INDICATOR_LEN..].trim_start())  // remove repeat word and OPTIONAL_INDICATOR
    979
                } else {
    980
    23
                    None
    981
                }
    982
    45.4k
            }
    983
    142k
        }
    984
    985
    132k
        pub fn replace_array_tree<'c, 's:'c, 'm:'c>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<Element<'m>> {
    986
            // shortcut for common case (don't build a new tree node)
    987
    132k
            if self.replacements.len() == 1 {
    988
    129k
                return rules_with_context.replace::<Element<'m>>(&self.replacements[0], mathml);
    989
    3.20k
            }
    990
    991
    3.20k
            let new_element = create_mathml_element(&rules_with_context.doc, "Unknown");  // Hopefully set later (in Intent::Replace())
    992
    3.20k
            let mut new_children = Vec::with_capacity(self.replacements.len());
    993
    6.12k
            for child in self.replacements.iter()3.20k {
    994
    6.12k
                let child = rules_with_context.replace::<Element<'m>>(child, mathml)?0;
    995
    6.12k
                new_children.push(ChildOfElement::Element(child));
    996
            };
    997
    3.20k
            new_element.append_children(new_children);
    998
    3.20k
            return Ok(new_element);
    999
    132k
        }
    1000
    1001
    1002
        /// Return true if there are no replacements.
    1003
    29.8k
        pub fn is_empty(&self) -> bool {
    1004
    29.8k
            return self.replacements.is_empty();
    1005
    29.8k
        }
    1006
    1007
    10
        fn pretty_print_replacements(&self) -> String {
    1008
    10
            let mut group_string = String::with_capacity(128);
    1009
    10
            if self.replacements.len() == 1 {
    1010
    9
                group_string += &format!("[{}]", self.replacements[0]);
    1011
    9
            } else {
    1012
    1
                group_string += &self.replacements.iter()
    1013
    1
                        .map(|replacement| format!0("\n  - {replacement}"))
    1014
    1
                        .collect::<Vec<String>>()
    1015
    1
                        .join("");
    1016
    1
                group_string += "\n";
    1017
            }
    1018
    10
            return group_string;
    1019
    10
        }
    1020
    }
    1021
    1022
    1023
    1024
    // MyXPath is a wrapper around an 'XPath' that keeps around the original xpath expr (as a string) so it can be used in error reporting.
    1025
    // Because we want to be able to clone them and XPath doesn't support clone(), this is a wrapper around an internal MyXPath.
    1026
    // It supports the standard SpeechRule functionality of building and replacing.
    1027
    #[derive(Debug)]
    1028
    struct RCMyXPath {
    1029
        xpath: XPath,
    1030
        string: String,        // store for error reporting
    1031
    }
    1032
    1033
    #[derive(Debug, Clone)]
    1034
    pub struct MyXPath {
    1035
        rc: Rc<RCMyXPath>        // rather than putting Rc around both 'xpath' and 'string', just use one and indirect to internal RCMyXPath
    1036
    }
    1037
    1038
    1039
    impl fmt::Display for MyXPath {
    1040
    2.79k
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    1041
    2.79k
            return write!(f, "\"{}\"", self.rc.string);
    1042
    2.79k
        }
    1043
    }
    1044
    1045
    // pub fn xpath_count() -> (usize, usize) {
    1046
    //     return (XPATH_CACHE.with( |cache| cache.borrow().len()), unsafe{XPATH_CACHE_HITS} );
    1047
    // }
    1048
    thread_local!{
    1049
        static XPATH_CACHE: RefCell<HashMap<String, MyXPath>> = RefCell::new( HashMap::with_capacity(2047) );
    1050
    }
    1051
    // static mut XPATH_CACHE_HITS: usize = 0;
    1052
    1053
    impl MyXPath {
    1054
    8.91M
        fn new(xpath: String) -> Result<MyXPath> {
    1055
    8.91M
            return XPATH_CACHE.with( |cache|  {
    1056
    8.91M
                let mut cache = cache.borrow_mut();
    1057
                return Ok(
    1058
    8.91M
                    match cache.get(&xpath) {
    1059
    5.83M
                        Some(compiled_xpath) => {
    1060
                            // unsafe{ XPATH_CACHE_HITS += 1;};
    1061
    5.83M
                            compiled_xpath.clone()
    1062
                        },
    1063
                        None => {
    1064
    3.07M
                            let new_xpath = MyXPath {
    1065
    3.07M
                                rc: Rc::new( RCMyXPath {
    1066
    3.07M
                                    xpath: MyXPath::compile_xpath(&xpath)?0,
    1067
    3.07M
                                    string: xpath.clone()
    1068
                                })};
    1069
    3.07M
                            cache.insert(xpath.clone(), new_xpath.clone());
    1070
    3.07M
                            new_xpath
    1071
                        },
    1072
                    }
    1073
                )
    1074
    8.91M
            });
    1075
    8.91M
        }
    1076
    1077
    8.86M
        pub fn build(xpath: &Yaml) -> Result<MyXPath> {
    1078
    8.86M
            let xpath = match xpath {
    1079
    8.66M
                Yaml::String(s) => s.to_string(),
    1080
    0
                Yaml::Integer(i) => i.to_string(),
    1081
    0
                Yaml::Real(s) => s.to_string(),
    1082
    0
                Yaml::Boolean(s) => s.to_string(),
    1083
    193k
                Yaml::Array(v) =>
    1084
                    // array of strings -- concatenate them together
    1085
    193k
                    v.iter()
    1086
    193k
                        .map(as_str_checked)
    1087
    193k
                        .collect::<Result<Vec<&str>>>()?0
    1088
    193k
                        .join(" "),
    1089
    0
                _ => bail!("Bad value when trying to create an xpath: {}", yaml_to_string(xpath, 1)),
    1090
            };
    1091
    8.86M
            return MyXPath::new(xpath);
    1092
    8.86M
        }
    1093
    1094
    3.07M
        fn compile_xpath(xpath: &str) -> Result<XPath> {
    1095
    3.07M
            let factory = Factory::new();
    1096
    3.07M
            let xpath_with_debug_info = MyXPath::add_debug_string_arg(xpath)?0;
    1097
    3.07M
            let compiled_xpath = factory.build(&xpath_with_debug_info)
    1098
    3.07M
                            .with_context(|| format!0(
    1099
                                "Could not compile XPath for pattern:\n{}{}",
    1100
    0
                                &xpath, more_details(xpath)))?;
    1101
    3.07M
            return match compiled_xpath {
    1102
    3.07M
                Some(xpath) => Ok(xpath),
    1103
    0
                None => bail!("Problem compiling Xpath for pattern:\n{}{}",
    1104
    0
                                &xpath, more_details(xpath)),
    1105
            };
    1106
    1107
    1108
    0
            fn more_details(xpath: &str) -> String {
    1109
                // try to give a better error message by counting [], (), 's, and "s
    1110
    0
                if xpath.is_empty() {
    1111
    0
                    return "xpath is empty string".to_string();
    1112
    0
                }
    1113
    0
                let as_bytes = xpath.trim().as_bytes();
    1114
    0
                if as_bytes[0] == b'\'' && as_bytes[as_bytes.len()-1] != b'\'' {
    1115
    0
                    return "\nmissing \"'\"".to_string();
    1116
    0
                }
    1117
    0
                if (as_bytes[0] == b'"' && as_bytes[as_bytes.len()-1] != b'"') ||
    1118
    0
                   (as_bytes[0] != b'"' && as_bytes[as_bytes.len()-1] == b'"'){
    1119
    0
                    return "\nmissing '\"'".to_string();
    1120
    0
                }
    1121
    1122
    0
                let mut i_bytes = 0;      // keep track of # of bytes into string for error reporting
    1123
    0
                let mut paren_count = 0;    // counter to make sure they are balanced
    1124
    0
                let mut i_paren = 0;      // position of the outermost open paren
    1125
    0
                let mut bracket_count = 0;
    1126
    0
                let mut i_bracket = 0;
    1127
    0
                for ch in xpath.chars() {
    1128
    0
                    if ch == '(' {
    1129
    0
                        if paren_count == 0 {
    1130
    0
                            i_paren = i_bytes;
    1131
    0
                        }
    1132
    0
                        paren_count += 1;
    1133
    0
                    } else if ch == '[' {
    1134
    0
                        if bracket_count == 0 {
    1135
    0
                            i_bracket = i_bytes;
    1136
    0
                        }
    1137
    0
                        bracket_count += 1;
    1138
    0
                    } else if ch == ')' {
    1139
    0
                        if paren_count == 0 {
    1140
    0
                            return format!("\nExtra ')' found after '{}'", &xpath[i_paren..i_bytes]);
    1141
    0
                        }
    1142
    0
                        paren_count -= 1;
    1143
    0
                        if paren_count == 0 && bracket_count > 0 && i_bracket > i_paren {
    1144
    0
                            return format!("\nUnclosed brackets found at '{}'", &xpath[i_paren..i_bytes]);
    1145
    0
                        }
    1146
    0
                    } else if ch == ']' {
    1147
    0
                        if bracket_count == 0 {
    1148
    0
                            return format!("\nExtra ']' found after '{}'", &xpath[i_bracket..i_bytes]);
    1149
    0
                        }
    1150
    0
                        bracket_count -= 1;
    1151
    0
                        if bracket_count == 0 && paren_count > 0 && i_paren > i_bracket {
    1152
    0
                            return format!("\nUnclosed parens found at '{}'", &xpath[i_bracket..i_bytes]);
    1153
    0
                        }
    1154
    0
                    }
    1155
    0
                    i_bytes += ch.len_utf8();
    1156
                }
    1157
    0
                return "".to_string();
    1158
    0
            }
    1159
    3.07M
        }
    1160
    1161
        /// Convert DEBUG(...) input to the internal function which is DEBUG(arg, arg_as_string)
    1162
    3.08M
        fn add_debug_string_arg(xpath: &str) -> Result<String> {
    1163
            // do a quick check to see if "DEBUG" is in the string -- this is the common case
    1164
    3.08M
            let debug_start = xpath.find("DEBUG(");
    1165
    3.08M
            if debug_start.is_none() {
    1166
    3.07M
                return Ok( xpath.to_string() );
    1167
    1.56k
            }
    1168
    1169
    1.56k
            let debug_start = debug_start.unwrap();
    1170
    1.56k
            let mut before_paren = xpath[..debug_start+5].to_string();   // includes "DEBUG"
    1171
    1.56k
            let chars = xpath[debug_start+5..].chars().collect::<Vec<char>>();     // begins at '('
    1172
    1.56k
            before_paren.push_str(&chars_add_debug_string_arg(&chars).with_context(|| format!0("In xpath='{xpath}'"))?0);
    1173
            // debug!("add_debug_string_arg: {}", before_paren);
    1174
    1.56k
            return Ok(before_paren);
    1175
    1176
    1.56k
            fn chars_add_debug_string_arg(chars: &[char]) -> Result<String>  {
    1177
                // Find all the DEBUG(...) commands in 'xpath' and adds a string argument.
    1178
                // The DEBUG function that is used internally takes two arguments, the second one being a string version of the DEBUG arg.
    1179
                //   Being a string, any quotes need to be escaped, and DEBUGs inside of DEBUGs need more escaping.
    1180
                //   This is done via recursive calls to this function.
    1181
    1.56k
                assert_eq!(chars[0], '(', "{} does not start with ')'", chars0.iter0().collect0::<String>());
    1182
    1.56k
                let mut count = 1;  // open/close count
    1183
    1.56k
                let mut i = 1;
    1184
    1.56k
                let mut inside_quote = false;
    1185
    50.9k
                while i < chars.len() {
    1186
    50.9k
                    let ch = chars[i];
    1187
    808
                    match ch {
    1188
                        '\\' => {
    1189
    0
                            if i+1 == chars.len() {
    1190
    0
                                bail!("Syntax error in DEBUG: last char is escape char\nDebug string: '{}'", chars.iter().collect::<String>());
    1191
    0
                            }
    1192
    0
                            i += 1;
    1193
                        },
    1194
    2.22k
                        '\'' => inside_quote = !inside_quote,
    1195
    807
                        '(' if !inside_quote => {
    1196
    807
                            count += 1;
    1197
    807
                            // FIX: it would be more efficient to spot "DEBUG" preceding this and recurse rather than matching the whole string and recursing
    1198
    807
                        },
    1199
    1
                        '(' => (),
    1200
    2.37k
                        ')' if !inside_quote => {
    1201
    2.37k
                            count -= 1;
    1202
    2.37k
                            if count == 0 {
    1203
    1.56k
                                let arg = &chars[1..i].iter().collect::<String>();
    1204
    1.56k
                                let escaped_arg = arg.replace('"', "\\\"");
    1205
                                // DEBUG(...) may be inside 'arg' -- recurse
    1206
    1.56k
                                let processed_arg = MyXPath::add_debug_string_arg(arg)?0;
    1207
    1208
                                // DEBUG(...) may be in the remainder of the string -- recurse
    1209
    1.56k
                                let processed_rest = MyXPath::add_debug_string_arg(&chars[i+1..].iter().collect::<String>())?0;
    1210
    1.56k
                                return Ok( format!("({processed_arg}, \"{escaped_arg}\"){processed_rest}") );
    1211
    807
                            }
    1212
                        },
    1213
    0
                        ')' => (),
    1214
    45.5k
                        _ => (),
    1215
                    }
    1216
    49.4k
                    i += 1;
    1217
                }
    1218
    0
                bail!("Syntax error in DEBUG: didn't find matching closing paren\nDEBUG{}", chars.iter().collect::<String>());
    1219
    1.56k
            }
    1220
    3.08M
        }
    1221
    1222
    156k
        fn is_true(&self, context: &sxd_xpath::Context, mathml: Element) -> Result<bool> {
    1223
            // return true if there is no condition or if the condition evaluates to true
    1224
            return Ok(
    1225
    156k
                match self.evaluate(context, mathml)?0 {
    1226
    115k
                    Value::Boolean(b) => b,
    1227
    40.6k
                    Value::Nodeset(nodes) => nodes.size() > 0,
    1228
    0
                    _                      => false,
    1229
                }
    1230
            )
    1231
    156k
        }
    1232
    1233
    153k
        pub fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
    1234
    153k
            if self.rc.string == "process-intent(.)" {
    1235
    2.47k
                return T::from_element2.46k( infer_intent(rules_with_context, mathml)?9 );
    1236
    150k
            }
    1237
    1238
    150k
            let result = self.evaluate(&rules_with_context.context_stack.base, mathml)
    1239
    150k
                    .with_context(|| format!0("in '{}' replacing after pattern match", &self.rc.string0) )?0;
    1240
    150k
            let string28.9k = match result {
    1241
    121k
                    Value::Nodeset(nodes) => {
    1242
    121k
                        if nodes.size() == 0 {
    1243
    0
                            bail!("During replacement, no matching element found");
    1244
    121k
                        }
    1245
    121k
                        return rules_with_context.replace_nodes(nodes.document_order(), mathml);
    1246
                    },
    1247
    25.1k
                    Value::String(s) => s,
    1248
    3.80k
                    Value::Number(num) => num.to_string(),
    1249
    0
                    Value::Boolean(b) => b.to_string(),          // FIX: is this right???
    1250
            };
    1251
            // Hack!: this test for input that starts with a '$' (defined variable), avoids a double evaluate;
    1252
            // We don't need NO_EVAL_QUOTE_CHAR here, but the more general solution of a quoted execute (- xq:) would avoid this hack
    1253
    28.9k
            let result = if self.rc.string.starts_with('$') {string5.63k} else {rules_with_context23.3k.replace_chars23.3k(&string23.3k, mathml23.3k)?0};
    1254
    28.9k
            return T::from_string(result, rules_with_context.doc );
    1255
    153k
        }
    1256
    1257
    1.29M
        pub fn evaluate<'c>(&self, context: &sxd_xpath::Context<'c>, mathml: Element<'c>) -> Result<Value<'c>> {
    1258
            // debug!("evaluate: {}", self);
    1259
    1.29M
            let result = self.rc.xpath.evaluate(context, mathml);
    1260
    1.29M
            return match result {
    1261
    1.29M
                Ok(val) => Ok( val ),
    1262
    0
                Err(e) => {
    1263
                    // debug!("MyXPath::trying to evaluate:\n  '{}'\n caused the error\n'{}'", self, e.to_string().replace("OwnedPrefixedName { prefix: None, local_part:", "").replace(" }", ""));
    1264
    0
                    bail!( "{}\n\n",
    1265
                         // remove confusing parts of error message from xpath
    1266
    0
                        e.to_string().replace("OwnedPrefixedName { prefix: None, local_part:", "").replace(" }", "") );
    1267
                }
    1268
            };
    1269
    1.29M
        }
    1270
    1271
    0
        pub fn test_input<F>(self, f: F) -> bool where F: Fn(&str) -> bool {
    1272
    0
            return f(self.rc.string.as_ref());
    1273
    0
        }
    1274
    }
    1275
    1276
    // 'SpeechPattern' holds a single pattern.
    1277
    // Some info is not needed beyond converting the Yaml to the SpeechPattern, but is useful for error reporting.
    1278
    // The two main parts are the pattern to be matched and the replacements to do if there is a match.
    1279
    // Any variables/prefs that are defined/set are also stored.
    1280
    #[derive(Debug)]
    1281
    struct SpeechPattern {
    1282
        pattern_name: String,
    1283
        tag_name: String,
    1284
        file_name: String,
    1285
        pattern: MyXPath,                     // the xpath expr to attempt to match
    1286
        match_uses_var_defs: bool,            // include var_defs in context for matching
    1287
        var_defs: VariableDefinitions,        // any variable definitions [can be and probably is an empty vector most of the time]
    1288
        replacements: ReplacementArray,       // the replacements in case there is a match
    1289
    }
    1290
    1291
    impl fmt::Display for SpeechPattern {
    1292
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    1293
    0
            return write!(f, "[name: {}, tag: {},\n  variables: {:?}, pattern: {},\n  replacement: {}]",
    1294
                    self.pattern_name, self.tag_name, self.var_defs, self.pattern,
    1295
    0
                    self.replacements.pretty_print_replacements());
    1296
    0
        }
    1297
    }
    1298
    1299
    impl SpeechPattern  {
    1300
    896k
        fn build(dict: &Yaml, file: &Path, rules: &mut SpeechRules) -> Result<Option<Vec<PathBuf>>> {
    1301
            // Rule::SpeechPattern
    1302
            //   build { "pattern_name", "tag_name", "pattern", "replacement" }
    1303
            // or recurse via include: file_name
    1304
    1305
            // debug!("\nbuild_speech_pattern: dict:\n{}", yaml_to_string(dict, 0));
    1306
    896k
            if let Some(include_file_name30.3k) = find_str(dict, "include") {
    1307
    30.3k
                let do_include_fn = |new_file: &Path| {
    1308
    30.3k
                    rules.read_patterns(new_file)
    1309
    30.3k
                };
    1310
    1311
    30.3k
                return Ok( Some(process_include(file, include_file_name, do_include_fn)?0) );
    1312
    866k
            }
    1313
    1314
    866k
            let pattern_name = find_str(dict, "name");
    1315
    1316
            // tag_named can be either a string (most common) or an array of strings
    1317
    866k
            let mut tag_names: Vec<&str> = Vec::new();
    1318
    866k
            match find_str(dict, "tag") {
    1319
    741k
                Some(str) => tag_names.push(str),
    1320
                None => {
    1321
                    // check for array
    1322
    124k
                    let tag_array  = &dict["tag"];
    1323
    124k
                    tag_names = vec![];
    1324
    124k
                    if tag_array.is_array() {
    1325
    264k
                        for (i, name) in tag_array124k.as_vec().unwrap().iter().enumerate124k() {
    1326
    264k
                            match as_str_checked(name) {
    1327
    0
                                Err(e) => return Err(
    1328
    0
                                    e.context(
    1329
    0
                                        format!("tag name '{}' is not a string in:\n{}",
    1330
    0
                                            &yaml_to_string(&tag_array.as_vec().unwrap()[i], 0),
    1331
    0
                                            &yaml_to_string(dict, 1)))
    1332
    0
                                ),
    1333
    264k
                                Ok(str) => tag_names.push(str),
    1334
                            };
    1335
                        }
    1336
                    } else {
    1337
    0
                        bail!("Errors trying to find 'tag' in:\n{}", &yaml_to_string(dict, 1));
    1338
                    }
    1339
                }
    1340
            }
    1341
    1342
    866k
            if pattern_name.is_none() {
    1343
    0
                if dict.is_null() {
    1344
    0
                    bail!("Error trying to find 'name': empty value (two consecutive '-'s?");
    1345
                } else {
    1346
    0
                    bail!("Errors trying to find 'name' in:\n{}", &yaml_to_string(dict, 1));
    1347
                };
    1348
    866k
            };
    1349
    866k
            let pattern_name = pattern_name.unwrap().to_string();
    1350
    1351
            // FIX: add check to make sure tag_name is a valid MathML tag name
    1352
    866k
            if dict["match"].is_badvalue() {
    1353
    0
                bail!("Did not find 'match' in\n{}", yaml_to_string(dict, 1));
    1354
    866k
            }
    1355
    866k
            if dict["replace"].is_badvalue() {
    1356
    0
                bail!("Did not find 'replace' in\n{}", yaml_to_string(dict, 1));
    1357
    866k
            }
    1358
    1359
            // xpath's can't be cloned, so we need to do a 'build_xxx' for each tag name
    1360
    1.00M
            for tag_name in tag_names866k {
    1361
    1.00M
                let tag_name = tag_name.to_string();
    1362
    1.00M
                let pattern_xpath = MyXPath::build(&dict["match"])
    1363
    1.00M
                        .with_context(|| {0
    1364
    0
                            format!("value for 'match' in rule ({}: {}):\n{}",
    1365
    0
                                    tag_name, pattern_name, yaml_to_string(dict, 1))
    1366
    0
                        })?;
    1367
    1.00M
                let speech_pattern =
    1368
    1.00M
                    Box::new( SpeechPattern{
    1369
    1.00M
                        pattern_name: pattern_name.clone(),
    1370
    1.00M
                        tag_name: tag_name.clone(),
    1371
    1.00M
                        file_name: file.to_str().unwrap().to_string(),
    1372
    1.00M
                        match_uses_var_defs: dict["variables"].is_array() && pattern_xpath.rc.string.contains('$')169k,    // FIX: should look at var_defs for actual name
    1373
    1.00M
                        pattern: pattern_xpath,
    1374
    1.00M
                        var_defs: VariableDefinitions::build(&dict["variables"])
    1375
    1.00M
                            .with_context(|| {0
    1376
    0
                                format!("value for 'variables' in rule ({}: {}):\n{}",
    1377
    0
                                        tag_name, pattern_name, yaml_to_string(dict, 1))
    1378
    0
                            })?,
    1379
    1.00M
                        replacements: ReplacementArray::build(&dict["replace"])
    1380
    1.00M
                            .with_context(|| {0
    1381
    0
                                format!("value for 'replace' in rule ({}: {}). Replacements:\n{}",
    1382
    0
                                        tag_name, pattern_name, yaml_to_string(&dict["replace"], 1))
    1383
    0
                        })?
    1384
                    } );
    1385
                // get the array of rules for the tag name
    1386
    1.00M
                let rule_value = rules.rules.entry(tag_name).or_default();
    1387
    1388
                // if the name exists, replace it. Otherwise add the new rule
    1389
    2.67M
                match rule_value.iter().enumerate()1.00M.find1.00M(|&pattern| pattern.1.pattern_name == speech_pattern.pattern_name) {
    1390
    1.00M
                    None => rule_value.push(speech_pattern),
    1391
    9
                    Some((i, _old_pattern)) => {
    1392
    9
                        let old_rule = &rule_value[i];
    1393
    9
                        info!("\n\n***WARNING***: replacing {}/'{}' in {} with rule from {}\n",
    1394
                                old_rule.tag_name, old_rule.pattern_name, old_rule.file_name, speech_pattern.file_name);
    1395
    9
                        rule_value[i] = speech_pattern;
    1396
                    },
    1397
                }
    1398
            }
    1399
    1400
    866k
            return Ok(None);
    1401
    896k
        }
    1402
    1403
    870k
        fn is_match(&self, context: &sxd_xpath::Context, mathml: Element) -> Result<bool> {
    1404
    870k
            if self.tag_name != mathml.name().local_part() && self.tag_name != "*"224k && self.tag_name != "!*"164k {
    1405
    0
                return Ok( false );
    1406
    870k
            }
    1407
    1408
            // debug!("\nis_match: pattern='{}'", self.pattern_name);
    1409
            // debug!("    pattern_expr {:?}", self.pattern);
    1410
            // debug!("is_match: mathml is\n{}", mml_to_string(mathml));
    1411
            return Ok(
    1412
    870k
                match self.pattern.evaluate(context, mathml)?0 {
    1413
    661k
                    Value::Boolean(b)       => b,
    1414
    208k
                    Value::Nodeset(nodes) => nodes.size() > 0,
    1415
    0
                    _                             => false,
    1416
                }
    1417
            );
    1418
    870k
        }
    1419
    }
    1420
    1421
    1422
    // 'Test' holds information used if the replacement is a "test:" clause.
    1423
    // The condition is an xpath expr and the "else:" part is optional.
    1424
    1425
    #[derive(Debug, Clone)]
    1426
    struct TestArray {
    1427
        tests: Vec<Test>
    1428
    }
    1429
    1430
    impl fmt::Display for TestArray {
    1431
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    1432
    0
            for test in &self.tests {
    1433
    0
                writeln!(f, "{test}")?;
    1434
            }
    1435
    0
            return Ok( () );
    1436
    0
        }
    1437
    }
    1438
    1439
    impl TestArray {
    1440
    3.30M
        fn build(test: &Yaml) -> Result<TestArray> {
    1441
            // 'test:' for convenience takes either a dictionary with keys if/else_if/then/then_test/else/else_test or
    1442
            //      or an array of those values (there should be at most one else/else_test)
    1443
    1444
            // if 'test' is a dictionary ('Hash'), we convert it to an array with one entry and proceed
    1445
    3.30M
            let tests = if test.as_hash().is_some() {
    1446
    3.01M
                vec![test]
    1447
    287k
            } else if let Some(vec) = test.as_vec() {
    1448
    287k
                vec.iter().collect()
    1449
            } else {
    1450
    0
                bail!("Value for 'test:' is neither a dictionary or an array.")
    1451
            };
    1452
    1453
            // each entry in 'tests' should be a dictionary with keys if/then/then_test/else/else_test
    1454
            // a valid entry is one of:
    1455
            //   if:/else_if:, then:/then_test: and optional else:/else_test:
    1456
            //   else:/else_test: -- if this case, it should be the last entry in 'tests'
    1457
            // 'if:' should only be the first entry in the array; 'else_if' should never be the first entry. Otherwise, they are the same
    1458
    3.30M
            let mut test_array = vec![];
    1459
    3.74M
            for test in tests3.30M {
    1460
    3.74M
                if test.as_hash().is_none() {
    1461
    0
                    bail!("Value for array entry in 'test:' must be a dictionary/contain keys");
    1462
    3.74M
                }
    1463
    3.74M
                let if_part = &test[if test_array.is_empty() {"if"3.30M} else {"else_if"437k}];
    1464
    3.74M
                if !if_part.is_badvalue() {
    1465
                    // first case: if:, then:, optional else:
    1466
    3.69M
                    let condition = Some( MyXPath::build(if_part)?0 );
    1467
    3.69M
                    let then_part = TestOrReplacements::build(test, "then", "then_test", true)?0;
    1468
    3.69M
                    let else_part = TestOrReplacements::build(test, "else", "else_test", false)?0;
    1469
    3.69M
                    let n_keys = if else_part.is_none() {22.45M} else {31.24M};
    1470
    3.69M
                    if test.as_hash().unwrap().len() > n_keys {
    1471
    0
                        bail!("A key other than 'if', 'else_if', 'then', 'then_test', 'else', or 'else_test' was found in the 'then' clause of 'test'");
    1472
    3.69M
                    };
    1473
    3.69M
                    test_array.push(
    1474
    3.69M
                        Test { condition, then_part, else_part }
    1475
                    );
    1476
                } else {
    1477
                    // second case: should be else/else_test
    1478
    42.4k
                    let else_part = TestOrReplacements::build(test, "else", "else_test", true)?0;
    1479
    42.4k
                    if test.as_hash().unwrap().len() > 1 {
    1480
    0
                        bail!("A key other than 'if', 'else_if', 'then', 'then_test', 'else', or 'else_test' was found the 'else' clause of 'test'");
    1481
    42.4k
                    };
    1482
    42.4k
                    test_array.push(
    1483
    42.4k
                        Test { condition: None, then_part: None, else_part }
    1484
                    );
    1485
    1486
                    // there shouldn't be any trailing tests
    1487
    42.4k
                    if test_array.len() < test.as_hash().unwrap().len() {
    1488
    0
                        bail!("'else'/'else_test' key is not last key in 'test:'");
    1489
    42.4k
                    }
    1490
                }
    1491
            };
    1492
    1493
    3.30M
            if test_array.is_empty() {
    1494
    0
                bail!("No entries for 'test:'");
    1495
    3.30M
            }
    1496
    1497
    3.30M
            return Ok( TestArray { tests: test_array } );
    1498
    3.30M
        }
    1499
    1500
    121k
        fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
    1501
    156k
            for test in &self.tests121k {
    1502
    156k
                if test.is_true(&rules_with_context.context_stack.base, mathml)?0 {
    1503
    85.2k
                    assert!(test.then_part.is_some());
    1504
    85.2k
                    return test.then_part.as_ref().unwrap().replace(rules_with_context, mathml);
    1505
    71.1k
                } else if let Some(else_part12.9k) = test.else_part.as_ref() {
    1506
    12.9k
                    return else_part.replace(rules_with_context, mathml);
    1507
    58.1k
                }
    1508
            }
    1509
    23.4k
            return T::from_string("".to_string(), rules_with_context.doc);
    1510
    121k
        }
    1511
    }
    1512
    1513
    #[derive(Debug, Clone)]
    1514
    // Used to hold then/then_test and also else/else_test -- only one of these can be present at a time
    1515
    enum TestOrReplacements {
    1516
        Replacements(ReplacementArray),     // replacements to use when a test is true
    1517
        Test(TestArray),                    // the array of if/then/else tests
    1518
    }
    1519
    1520
    impl fmt::Display for TestOrReplacements {
    1521
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    1522
    0
            if let TestOrReplacements::Test(_) = self {
    1523
    0
                write!(f, "  _test")?;
    1524
    0
            }
    1525
    0
            write!(f, ":")?;
    1526
    0
            return match self {
    1527
    0
                TestOrReplacements::Test(t) => write!(f, "{t}"),
    1528
    0
                TestOrReplacements::Replacements(r) => write!(f, "{r}"),
    1529
            };
    1530
    0
        }
    1531
    }
    1532
    1533
    impl TestOrReplacements {
    1534
    7.44M
        fn build(test: &Yaml, replace_key: &str, test_key: &str, key_required: bool) -> Result<Option<TestOrReplacements>> {
    1535
    7.44M
            let part = &test[replace_key];
    1536
    7.44M
            let test_part = &test[test_key];
    1537
    7.44M
            if !part.is_badvalue() && !test_part.is_badvalue()4.26M {
    1538
    0
                bail!(format!("Only one of '{}' or '{}' is allowed as part of 'test'.\n{}\n    \
    1539
                      Suggestion: delete one or adjust indentation",
    1540
    0
                        replace_key, test_key, yaml_to_string(test, 2)));
    1541
    7.44M
            }
    1542
    7.44M
            if part.is_badvalue() && test_part3.17M.is_badvalue3.17M() {
    1543
    2.45M
                if key_required {
    1544
    0
                    bail!(format!("Missing one of '{}'/'{}:' as part of 'test:'\n{}\n   \
    1545
                        Suggestion: add the missing key or indent so it is contained in 'test'",
    1546
    0
                        replace_key, test_key, yaml_to_string(test, 2)))
    1547
                } else {
    1548
    2.45M
                    return Ok( None );
    1549
                }
    1550
    4.98M
            }
    1551
            // at this point, we have only one of the two options
    1552
    4.98M
            if test_part.is_badvalue() {
    1553
    4.26M
                return Ok( Some( TestOrReplacements::Replacements( ReplacementArray::build(part)?0 ) ) );
    1554
            } else {
    1555
    713k
                return Ok( Some( TestOrReplacements::Test( TestArray::build(test_part)?0 ) ) );
    1556
            }
    1557
    7.44M
        }
    1558
    1559
    98.2k
        fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
    1560
    98.2k
            return match self {
    1561
    92.4k
                TestOrReplacements::Replacements(r) => r.replace(rules_with_context, mathml),
    1562
    5.74k
                TestOrReplacements::Test(t) => t.replace(rules_with_context, mathml),
    1563
            }
    1564
    98.2k
        }
    1565
    }
    1566
    1567
    #[derive(Debug, Clone)]
    1568
    struct Test {
    1569
        condition: Option<MyXPath>,
    1570
        then_part: Option<TestOrReplacements>,
    1571
        else_part: Option<TestOrReplacements>,
    1572
    }
    1573
    impl fmt::Display for Test {
    1574
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    1575
    0
            write!(f, "test: [ ")?;
    1576
    0
            if let Some(if_part) = &self.condition {
    1577
    0
                write!(f, " if: '{if_part}'")?;
    1578
    0
            }
    1579
    0
            if let Some(then_part) = &self.then_part {
    1580
    0
                write!(f, " then{then_part}")?;
    1581
    0
            }
    1582
    0
            if let Some(else_part) = &self.else_part {
    1583
    0
                write!(f, " else{else_part}")?;
    1584
    0
            }
    1585
    0
            return write!(f, "]");
    1586
    0
        }
    1587
    }
    1588
    1589
    impl Test {
    1590
    156k
        fn is_true(&self, context: &sxd_xpath::Context, mathml: Element) -> Result<bool> {
    1591
    156k
            return match self.condition.as_ref() {
    1592
    136
                None => Ok( false ),     // trivially false -- want to do else part
    1593
    156k
                Some(condition) => condition.is_true(context, mathml)
    1594
    156k
                                    .context("Failure in conditional test"),
    1595
            }
    1596
    156k
        }
    1597
    }
    1598
    1599
    // Used for speech rules with "variables: ..."
    1600
    #[derive(Debug, Clone)]
    1601
    struct VariableDefinition {
    1602
        name: String,     // name of variable
    1603
        value: MyXPath,   // xpath value, typically a constant like "true" or "0", but could be "*/*[1]" to store some nodes
    1604
    }
    1605
    1606
    impl fmt::Display for VariableDefinition {
    1607
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    1608
    0
            return write!(f, "[name: {}={}]", self.name, self.value);
    1609
    0
        }
    1610
    }
    1611
    1612
    // Used for speech rules with "variables: ..."
    1613
    #[derive(Debug)]
    1614
    struct VariableValue<'v> {
    1615
        name: String,       // name of variable
    1616
        value: Option<Value<'v>>,   // xpath value, typically a constant like "true" or "0", but could be "*/*[1]" to store some nodes
    1617
    }
    1618
    1619
    impl fmt::Display for VariableValue<'_> {
    1620
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    1621
    0
            let value = match &self.value {
    1622
    0
                None => "unset".to_string(),
    1623
    0
                Some(val) => format!("{val:?}")
    1624
            };
    1625
    0
            return write!(f, "[name: {}, value: {}]", self.name, value);
    1626
    0
        }
    1627
    }
    1628
    1629
    impl VariableDefinition {
    1630
    475k
        fn build(name_value_def: &Yaml) -> Result<VariableDefinition> {
    1631
    475k
            match name_value_def.as_hash() {
    1632
    475k
                Some(map) => {
    1633
    475k
                    if map.len() != 1 {
    1634
    0
                        bail!("definition is not a key/value pair. Found {}",
    1635
    0
                                yaml_to_string(name_value_def, 1) );
    1636
    475k
                    }
    1637
    475k
                    let (name, value) = map.iter().next().unwrap();
    1638
    475k
                    let name = as_str_checked( name)
    1639
    475k
                        .with_context(|| format!0( "definition name is not a string: {}",
    1640
    475k
                                yaml_to_string0(name0, 1) ))?0.to_string();
    1641
    475k
                    match value {
    1642
    475k
                        Yaml::Boolean(_) | Yaml::String(_)  | Yaml::Integer(_) | Yaml::Real(_) => (),
    1643
    0
                        _ => bail!("definition value is not a string, boolean, or number. Found {}",
    1644
    0
                                yaml_to_string(value, 1) )
    1645
                    };
    1646
                    return Ok(
    1647
                        VariableDefinition{
    1648
    475k
                            name,
    1649
    475k
                            value: MyXPath::build(value)?0
    1650
                        }
    1651
                    );
    1652
                },
    1653
    0
                None => bail!("definition is not a key/value pair. Found {}",
    1654
    0
                                yaml_to_string(name_value_def, 1) )
    1655
            }
    1656
    475k
        }
    1657
    }
    1658
    1659
    1660
    #[derive(Debug, Clone)]
    1661
    struct VariableDefinitions {
    1662
        defs: Vec<VariableDefinition>
    1663
    }
    1664
    1665
    impl fmt::Display for VariableDefinitions {
    1666
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    1667
    0
            for def in &self.defs {
    1668
    0
                write!(f, "{def},")?;
    1669
            }
    1670
    0
            return Ok( () );
    1671
    0
        }
    1672
    }
    1673
    1674
    struct VariableValues<'v> {
    1675
        defs: Vec<VariableValue<'v>>
    1676
    }
    1677
    1678
    impl fmt::Display for VariableValues<'_> {
    1679
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    1680
    0
            for value in &self.defs {
    1681
    0
                write!(f, "{value}")?;
    1682
            }
    1683
    0
            return writeln!(f);
    1684
    0
        }
    1685
    }
    1686
    1687
    impl VariableDefinitions {
    1688
    1.11M
        fn new(len: usize) -> VariableDefinitions {
    1689
    1.11M
            return VariableDefinitions{ defs: Vec::with_capacity(len) };
    1690
    1.11M
        }
    1691
    1692
    1.11M
        fn build(defs: &Yaml) -> Result<VariableDefinitions> {
    1693
    1.11M
            if defs.is_badvalue() {
    1694
    836k
                return Ok( VariableDefinitions::new(0) );
    1695
    277k
            };
    1696
    277k
            if defs.is_array() {
    1697
    277k
                let defs = defs.as_vec().unwrap();
    1698
    277k
                let mut definitions = VariableDefinitions::new(defs.len());
    1699
    475k
                for def in defs277k {
    1700
    475k
                    let variable_def = VariableDefinition::build(def)
    1701
    475k
                            .context("definition of 'variables'")?0;
    1702
    475k
                    definitions.push( variable_def);
    1703
                };
    1704
    277k
                return Ok (definitions );
    1705
    0
            }
    1706
    0
            bail!( "'variables' is not an array of {{name: xpath-value}} definitions. Found {}'",
    1707
    0
                    yaml_to_string(defs, 1) );
    1708
    1.11M
        }
    1709
    1710
    475k
        fn push(&mut self, var_def: VariableDefinition) {
    1711
    475k
            self.defs.push(var_def);
    1712
    475k
        }
    1713
    1714
    241k
        fn len(&self) -> usize {
    1715
    241k
            return self.defs.len();
    1716
    241k
        }
    1717
    }
    1718
    1719
    struct ContextStack<'c> {
    1720
        // Note: values are generated by calling value_of on an Evaluation -- that makes the two lifetimes the same
    1721
        old_values: Vec<VariableValues<'c>>,   // store old values so they can be set on pop
    1722
        base: sxd_xpath::Context<'c>                      // initial context -- contains all the function defs and pref variables
    1723
    }
    1724
    1725
    impl fmt::Display for ContextStack<'_> {
    1726
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    1727
    0
            writeln!(f, " {} old_values", self.old_values.len())?;
    1728
    0
            for values in &self.old_values {
    1729
    0
                writeln!(f, "  {values}")?;
    1730
            }
    1731
    0
            return writeln!(f);
    1732
    0
        }
    1733
    }
    1734
    1735
    impl<'c, 'r> ContextStack<'c> {
    1736
    22.7k
        fn new<'a,>(pref_manager: &'a PreferenceManager) -> ContextStack<'c> {
    1737
    22.7k
            let prefs = pref_manager.merge_prefs();
    1738
    22.7k
            let mut context_stack = ContextStack {
    1739
    22.7k
                base: ContextStack::base_context(prefs),
    1740
    22.7k
                old_values: Vec::with_capacity(31)      // should avoid allocations
    1741
    22.7k
            };
    1742
            // FIX: the list of variables to set should come from definitions.yaml
    1743
            // These can't be set on the <math> tag because of the "translate" command which starts speech at an 'id'
    1744
    22.7k
            context_stack.base.set_variable("MatchingPause", Value::Boolean(false));
    1745
    22.7k
            context_stack.base.set_variable("IsColumnSilent", Value::Boolean(false));
    1746
    1747
    1748
    22.7k
            return context_stack;
    1749
    22.7k
        }
    1750
    1751
    22.7k
        fn base_context(var_defs: PreferenceHashMap) -> sxd_xpath::Context<'c> {
    1752
    22.7k
            let mut context  = sxd_xpath::Context::new();
    1753
    22.7k
            context.set_namespace("m", "http://www.w3.org/1998/Math/MathML");
    1754
    22.7k
            crate::xpath_functions::add_builtin_functions(&mut context);
    1755
    1.88M
            for (key, value) in var_defs22.7k {
    1756
    1.88M
                context.set_variable(key.as_str(), yaml_to_value(&value));
    1757
    1.88M
                // if let Some(str_value) = value.as_str() {
    1758
    1.88M
                //     if str_value != "Auto" {
    1759
    1.88M
                //         debug!("Set {}='{}'", key.as_str(), str_value);
    1760
    1.88M
                //     }
    1761
    1.88M
                // }
    1762
    1.88M
            };
    1763
    22.7k
            return context;
    1764
    22.7k
        }
    1765
    1766
    3.78k
        fn set_globals(&'r mut self, new_vars: VariableDefinitions, mathml: Element<'c>) -> Result<()> {
    1767
            // for each var/value pair, evaluate the value and add the var/value to the base context
    1768
    4.84k
            for def in &new_vars.defs3.78k {
    1769
                // set the new value
    1770
    4.84k
                let new_value = match def.value.evaluate(&self.base, mathml) {
    1771
    4.84k
                    Ok(val) => val,
    1772
    0
                    Err(_) => bail!(format!("Can't evaluate variable def for {}", def)),
    1773
                };
    1774
    4.84k
                let qname = QName::new(def.name.as_str());
    1775
    4.84k
                self.base.set_variable(qname, new_value);
    1776
            }
    1777
    3.78k
            return Ok( () );
    1778
    3.78k
        }
    1779
    1780
    27.3k
        fn push(&'r mut self, new_vars: VariableDefinitions, mathml: Element<'c>) -> Result<()> {
    1781
            // store the old value and set the new one
    1782
    27.3k
            let mut old_values = VariableValues {defs: Vec::with_capacity(new_vars.defs.len()) };
    1783
    27.3k
            let evaluation = Evaluation::new(&self.base, Node::Element(mathml));
    1784
    66.9k
            for def in &new_vars.defs27.3k {
    1785
    66.9k
                // get the old value (might not be defined)
    1786
    66.9k
                let qname = QName::new(def.name.as_str());
    1787
    66.9k
                let old_value = evaluation.value_of(qname).cloned();
    1788
    66.9k
                old_values.defs.push( VariableValue{ name: def.name.clone(), value: old_value} );
    1789
    66.9k
            }
    1790
    1791
            // use a second loop because of borrow problem with self.base and 'evaluation'
    1792
    66.9k
            for def in &new_vars.defs27.3k {
    1793
                // set the new value
    1794
    66.9k
                let new_value = match def.value.evaluate(&self.base, mathml) {
    1795
    66.9k
                    Ok(val) => val,
    1796
    0
                    Err(_) => Value::Nodeset(sxd_xpath::nodeset::Nodeset::new()),
    1797
                };
    1798
    66.9k
                let qname = QName::new(def.name.as_str());
    1799
    66.9k
                self.base.set_variable(qname, new_value);
    1800
            }
    1801
    27.3k
            self.old_values.push(old_values);
    1802
    27.3k
            return Ok( () );
    1803
    27.3k
        }
    1804
    1805
    27.3k
        fn pop(&mut self) {
    1806
            const MISSING_VALUE: &str = "-- unset value --";     // can't remove a variable from context, so use this value
    1807
    27.3k
            let old_values = self.old_values.pop().unwrap();
    1808
    66.9k
            for variable in old_values.defs27.3k {
    1809
    66.9k
                let qname = QName::new(&variable.name);
    1810
    66.9k
                let old_value = match variable.value {
    1811
    22.8k
                    None => Value::String(MISSING_VALUE.to_string()),
    1812
    44.1k
                    Some(val) => val,
    1813
                };
    1814
    66.9k
                self.base.set_variable(qname, old_value);
    1815
            }
    1816
    27.3k
        }
    1817
    }
    1818
    1819
    1820
    1.88M
    fn yaml_to_value<'b>(yaml: &Yaml) -> Value<'b> {
    1821
    1.88M
        return match yaml {
    1822
    1.47M
            Yaml::String(s) => Value::String(s.clone()),
    1823
    295k
            Yaml::Boolean(b)  => Value::Boolean(*b),
    1824
    31.7k
            Yaml::Integer(i)   => Value::Number(*i as f64),
    1825
    91.0k
            Yaml::Real(s)   => Value::Number(s.parse::<f64>().unwrap()),
    1826
            _  => {
    1827
    0
                error!("yaml_to_value: illegal type found in Yaml value: {}", yaml_to_string(yaml, 1));
    1828
    0
                Value::String("".to_string())
    1829
            },
    1830
        }
    1831
    1.88M
    }
    1832
    1833
    1834
    // Information for matching a Unicode char (defined in unicode.yaml) and building its replacement
    1835
    struct UnicodeDef {
    1836
        ch: u32,
    1837
        speech: ReplacementArray
    1838
    }
    1839
    1840
    impl  fmt::Display for UnicodeDef {
    1841
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    1842
    0
            return write!(f, "UnicodeDef{{ch: {}, speech: {:?}}}", self.ch, self.speech);
    1843
    0
        }
    1844
    }
    1845
    1846
    impl UnicodeDef {
    1847
    2.23M
        fn build(unicode_def: &Yaml, file_name: &Path, speech_rules: &SpeechRules, use_short: bool) -> Result<Option<Vec<PathBuf>>> {
    1848
    2.23M
            if let Some(include_file_name3) = find_str(unicode_def, "include") {
    1849
    3
                let do_include_fn = |new_file: &Path| {
    1850
    3
                    speech_rules.read_unicode(Some(new_file.to_path_buf()), use_short)
    1851
    3
                };
    1852
    3
                return Ok( Some(process_include(file_name, include_file_name, do_include_fn)?0) );
    1853
    2.23M
            }
    1854
            // key: char, value is replacement or array of replacements
    1855
    2.23M
            let dictionary = unicode_def.as_hash();
    1856
    2.23M
            if dictionary.is_none() {
    1857
    0
                bail!("Expected a unicode definition (e.g, '+':[t: \"plus\"]'), found {}", yaml_to_string(unicode_def, 0));
    1858
    2.23M
            }
    1859
    1860
    2.23M
            let dictionary = dictionary.unwrap();
    1861
    2.23M
            if dictionary.len() != 1 {
    1862
    0
                bail!("Expected a unicode definition (e.g, '+':[t: \"plus\"]'), found {}", yaml_to_string(unicode_def, 0));
    1863
    2.23M
            }
    1864
    1865
    2.23M
            let (ch, replacements) = dictionary.iter().next().ok_or_else(|| anyhow!0("Expected a unicode definition (e.g, '+':[t: \"plus\"]'), found {}", yaml_to_string0(unicode_def0, 0)))?0;
    1866
    2.23M
            let mut unicode_table = if use_short {
    1867
    1.06M
                speech_rules.unicode_short.borrow_mut()
    1868
            } else {
    1869
    1.16M
                speech_rules.unicode_full.borrow_mut()
    1870
            };
    1871
    2.23M
            if let Some(str) = ch.as_str() {
    1872
    2.23M
                if str.is_empty() {
    1873
    0
                    bail!("Empty character definition. Replacement is {}", replacements.as_str().unwrap());
    1874
    2.23M
                }
    1875
    2.23M
                let mut chars = str.chars();
    1876
    2.23M
                let first_ch = chars.next().unwrap();       // non-empty string, so a char exists
    1877
    2.23M
                if chars.next().is_some() {                       // more than one char
    1878
    55.1k
                    if str.contains('-')  {
    1879
    38.7k
                        return process_range(str, replacements, unicode_table);
    1880
    16.3k
                    } else if first_ch != '0' {     // exclude 0xDDDD
    1881
    74.8k
                        for ch in str16.3k.chars16.3k() {     // restart the iterator
    1882
    74.8k
                            let ch_as_str = ch.to_string();
    1883
    74.8k
                            if unicode_table.insert(ch as u32, ReplacementArray::build(&substitute_ch(replacements, &ch_as_str))
    1884
    74.8k
                                                .with_context(|| format!0("In definition of char: '{str}'"))?0.replacements).is_some() {
    1885
    0
                                error!("*** Character '{}' (0x{:X}) is repeated", ch, ch as u32);
    1886
    74.8k
                            }
    1887
                        }
    1888
    16.3k
                        return Ok(None);
    1889
    0
                    }
    1890
    2.18M
                }
    1891
    0
            }
    1892
    1893
    2.18M
            let ch = UnicodeDef::get_unicode_char(ch)?0;
    1894
    2.18M
            if unicode_table.insert(ch, ReplacementArray::build(replacements)
    1895
    2.18M
                                            .with_context(|| format!0("In definition of char: '{}' (0x{})",
    1896
    2.18M
                                                                            char::from_u320(ch0).unwrap0(), ch))?0.replacements).is_some() {
    1897
    147
                error!("*** Character '{}' (0x{:X}) is repeated", char::from_u320(ch0).unwrap0(), ch);
    1898
    2.18M
            }
    1899
    2.18M
            return Ok(None);
    1900
    1901
    38.7k
            fn process_range(def_range: &str, replacements: &Yaml, mut unicode_table: RefMut<HashMap<u32,Vec<Replacement>>>) -> Result<Option<Vec<PathBuf>>> {
    1902
                // should be a character range (e.g., "A-Z")
    1903
                // iterate over that range and also substitute the char for '.' in the
    1904
    38.7k
                let mut range = def_range.split('-');
    1905
    38.7k
                let first = range.next().unwrap().chars().next().unwrap() as u32;
    1906
    38.7k
                let last = range.next().unwrap().chars().next().unwrap() as u32;
    1907
    38.7k
                if range.next().is_some() {
    1908
    0
                    bail!("Character range definition has more than one '-': '{}'", def_range);
    1909
    38.7k
                }
    1910
    1911
    897k
                for ch in first..last+138.7k {
    1912
    897k
                    let ch_as_str = char::from_u32(ch).unwrap().to_string();
    1913
    897k
                    unicode_table.insert(ch, ReplacementArray::build(&substitute_ch(replacements, &ch_as_str))
    1914
    897k
                                            .with_context(|| format!0("In definition of char: '{def_range}'"))?0.replacements);
    1915
                };
    1916
    1917
    38.7k
                return Ok(None)
    1918
    38.7k
            }
    1919
    1920
    10.3M
            fn substitute_ch(yaml: &Yaml, ch: &str) -> Yaml {
    1921
    10.3M
                return match yaml {
    1922
    2.35M
                    Yaml::Array(v) => {
    1923
                        Yaml::Array(
    1924
    2.35M
                            v.iter()
    1925
    3.05M
                             .map2.35M(|e| substitute_ch(e, ch))
    1926
    2.35M
                             .collect::<Vec<Yaml>>()
    1927
                        )
    1928
                    },
    1929
    4.54M
                    Yaml::Hash(h) => {
    1930
                        Yaml::Hash(
    1931
    4.54M
                            h.iter()
    1932
    6.36M
                             .map4.54M(|(key,val)| (key.clone(), substitute_ch(val, ch)) )
    1933
    4.54M
                             .collect::<Hash>()
    1934
                        )
    1935
                    },
    1936
    3.48M
                    Yaml::String(s) => Yaml::String( s.replace('.', ch) ),
    1937
    0
                    _ => yaml.clone(),
    1938
                }
    1939
    10.3M
            }
    1940
    2.23M
        }
    1941
    1942
    2.18M
        fn get_unicode_char(ch: &Yaml) -> Result<u32> {
    1943
            // either "a" or 0x1234 (number)
    1944
    2.18M
            if let Some(ch) = ch.as_str() {
    1945
    2.18M
                let mut ch_iter = ch.chars();
    1946
    2.18M
                let unicode_ch = ch_iter.next();
    1947
    2.18M
                if unicode_ch.is_none() || ch_iter.next().is_some() {
    1948
    0
                    bail!("Wanted unicode char, found string '{}')", ch);
    1949
    2.18M
                };
    1950
    2.18M
                return Ok( unicode_ch.unwrap() as u32 );
    1951
    0
            }
    1952
    1953
    0
            if let Some(num) = ch.as_i64() {
    1954
    0
                return Ok( num as u32 );
    1955
    0
            }
    1956
    0
            bail!("Unicode character '{}' can't be converted to an code point", yaml_to_string(ch, 0));
    1957
    2.18M
        }
    1958
    }
    1959
    1960
    // Fix: there should be a cache so subsequent library calls don't have to read in the same speech rules
    1961
    //   likely a cache of size 1 is fine
    1962
    // Fix: all statics should be gathered together into one structure that is a Mutex
    1963
    //   for each library call, we should grab a lock on the Mutex in case others try to call
    1964
    //   at the same time.
    1965
    //   If this turns out to be something that others actually do, then a cache > 1 would be good
    1966
    1967
     type RuleTable = HashMap<String, Vec<Box<SpeechPattern>>>;
    1968
     type UnicodeTable = Rc<RefCell<HashMap<u32,Vec<Replacement>>>>;
    1969
     type FilesAndTimesShared = Rc<RefCell<FilesAndTimes>>;
    1970
    1971
     #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    1972
     pub enum RulesFor {
    1973
         Intent,
    1974
         Speech,
    1975
         OverView,
    1976
         Navigation,
    1977
         Braille,
    1978
     }
    1979
    1980
     impl fmt::Display for RulesFor {
    1981
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    1982
    0
            let name = match self {
    1983
    0
                RulesFor::Intent => "Intent",
    1984
    0
                RulesFor::Speech => "Speech",
    1985
    0
                RulesFor::OverView => "OverView",
    1986
    0
                RulesFor::Navigation => "Navigation",
    1987
    0
                RulesFor::Braille => "Braille",
    1988
            };
    1989
    0
           return write!(f, "{name}");
    1990
    0
        }
    1991
     }
    1992
    1993
    1994
    #[derive(Debug, Clone)]
    1995
    pub struct FileAndTime {
    1996
        file: PathBuf,
    1997
        time: SystemTime,
    1998
    }
    1999
    2000
    impl FileAndTime {
    2001
    0
        fn new(file: PathBuf) -> FileAndTime {
    2002
    0
            return FileAndTime {
    2003
    0
                file,
    2004
    0
                time: SystemTime::UNIX_EPOCH,
    2005
    0
            }
    2006
    0
        }
    2007
    2008
        // used for debugging preference settings
    2009
    0
        pub fn debug_get_file(&self) -> Option<&str> {
    2010
    0
            return self.file.to_str();
    2011
    0
        }
    2012
    2013
    8.29k
        pub fn new_with_time(file: PathBuf) -> FileAndTime {
    2014
    8.29k
            return FileAndTime {
    2015
    8.29k
                time: FileAndTime::get_metadata(&file),
    2016
    8.29k
                file,
    2017
    8.29k
            }
    2018
    8.29k
        }
    2019
    2020
    33.7k
        pub fn is_up_to_date(&self) -> bool {
    2021
    33.7k
            let file_mod_time = FileAndTime::get_metadata(&self.file);
    2022
    33.7k
            return self.time >= file_mod_time;
    2023
    33.7k
        }
    2024
    2025
    140k
        fn get_metadata(path: &Path) -> SystemTime {
    2026
            use std::fs;
    2027
    140k
            if !cfg!(target_family = "wasm") {
    2028
    140k
                let metadata = fs::metadata(path);
    2029
    140k
                if let Ok(metadata120k) = metadata &&
    2030
    120k
                   let Ok(mod_time) = metadata.modified() {
    2031
    120k
                        return mod_time;
    2032
    20.4k
                    }
    2033
    0
            }
    2034
    20.4k
            return SystemTime::UNIX_EPOCH
    2035
    140k
        }
    2036
    2037
    }
    2038
    #[derive(Debug, Default)]
    2039
    pub struct FilesAndTimes {
    2040
        // ft[0] is the main file -- other files are included by it (or recursively)
    2041
        // We could be a little smarter about invalidation by tracking what file is the parent (including file),
    2042
        // but it seems more complicated than it is worth
    2043
        ft: Vec<FileAndTime>
    2044
    }
    2045
    2046
    impl FilesAndTimes {
    2047
    0
        pub fn new(start_path: PathBuf) -> FilesAndTimes {
    2048
    0
            let mut ft = Vec::with_capacity(8);
    2049
    0
            ft.push( FileAndTime::new(start_path) );
    2050
    0
            return FilesAndTimes{ ft };
    2051
    0
        }
    2052
    2053
        /// Returns true if the main file matches the corresponding preference location and files' times are all current
    2054
    33.4k
        pub fn is_file_up_to_date(&self, pref_path: &Path, should_ignore_file_time: bool) -> bool {
    2055
    2056
            // if the time isn't set or the path is different from the preference (which might have changed), return false
    2057
    33.4k
            if self.ft.is_empty() || self.as_path() != pref_path27.7k {
    2058
    5.75k
                return false;
    2059
    27.7k
            }
    2060
    27.7k
            if should_ignore_file_time || cfg!1.18k(target_family = "wasm") {
    2061
    26.5k
                return true;
    2062
    1.18k
            }
    2063
    1.18k
            if  self.ft[0].time == SystemTime::UNIX_EPOCH {
    2064
    0
                return false;
    2065
    1.18k
            }
    2066
    2067
    2068
            // check the time stamp on the included files -- if the head file hasn't changed, the paths for the included files will be the same
    2069
    1.19k
            for file in &self.ft1.18k {
    2070
    1.19k
                if !file.is_up_to_date() {
    2071
    1
                    return false;
    2072
    1.19k
                }
    2073
            }
    2074
    1.18k
            return true;
    2075
    33.4k
        }
    2076
    2077
    19.8k
        fn set_files_and_times(&mut self, new_files: Vec<PathBuf>)  {
    2078
    19.8k
            self.ft.clear();
    2079
    98.6k
            for path in new_files19.8k {
    2080
    98.6k
                let time = FileAndTime::get_metadata(&path);      // do before move below
    2081
    98.6k
                self.ft.push( FileAndTime{ file: path, time })
    2082
            }
    2083
    19.8k
        }
    2084
    2085
        /// Mark cached files as stale so the next `read_files()` reloads them.
    2086
    32.6k
        pub fn invalidate(&mut self) {
    2087
    32.6k
            self.ft.clear();
    2088
    32.6k
        }
    2089
    2090
    5
        pub fn is_valid(&self) -> bool {
    2091
    5
            self.ft.is_empty()
    2092
    5
        }
    2093
    2094
    27.7k
        pub fn as_path(&self) -> &Path {
    2095
    27.7k
            assert!(!self.ft.is_empty());
    2096
    27.7k
            return &self.ft[0].file;
    2097
    27.7k
        }
    2098
    2099
    0
        pub fn paths(&self) -> Vec<PathBuf> {
    2100
    0
            return self.ft.iter().map(|ft| ft.file.clone()).collect::<Vec<PathBuf>>();
    2101
    0
        }
    2102
    2103
    }
    2104
    2105
    2106
    /// `SpeechRulesWithContext` encapsulates a named group of speech rules (e.g, "ClearSpeak")
    2107
    /// along with the preferences to be used for speech.
    2108
    // Note: if we can't read the files, an error message is stored in the structure and needs to be checked.
    2109
    // I tried using Result<SpeechRules>, but it was a mess with all the unwrapping.
    2110
    // Important: the code needs to be careful to check this at the top level calls
    2111
    pub struct SpeechRules {
    2112
        error: String,
    2113
        name: RulesFor,
    2114
        pub pref_manager: Rc<RefCell<PreferenceManager>>,
    2115
        rules: RuleTable,                              // the speech rules used (partitioned into MathML tags in hashmap, then linearly searched)
    2116
        rule_files: FilesAndTimes,                     // files that were read
    2117
        translate_single_chars_only: bool,             // strings like "half" don't want 'a's translated, but braille does
    2118
        unicode_short: UnicodeTable,                   // the short list of rules used for Unicode characters
    2119
        unicode_short_files: FilesAndTimesShared,     // files that were read
    2120
        unicode_full:  UnicodeTable,                   // the long remaining rules used for Unicode characters
    2121
        unicode_full_files: FilesAndTimesShared,      // files that were read
    2122
        definitions_files: FilesAndTimesShared,       // files that were read
    2123
    }
    2124
    2125
    impl fmt::Display for SpeechRules {
    2126
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    2127
    0
            writeln!(f, "SpeechRules '{}'\n{})", self.name, self.pref_manager.borrow())?;
    2128
    0
            let mut rules_vec: Vec<(&String, &Vec<Box<SpeechPattern>>)> = self.rules.iter().collect();
    2129
    0
            rules_vec.sort_by_key(|(tag_name, _)| tag_name.as_str());
    2130
    0
            for (tag_name, rules) in rules_vec {
    2131
    0
                writeln!(f, "   {}: #patterns {}", tag_name, rules.len())?;
    2132
            };
    2133
    0
            return writeln!(f, "   {}+{} unicode entries", &self.unicode_short.borrow().len(), &self.unicode_full.borrow().len());
    2134
    0
        }
    2135
    }
    2136
    2137
    2138
    /// `SpeechRulesWithContext` encapsulates a named group of speech rules (e.g, "ClearSpeak")
    2139
    /// along with the preferences to be used for speech.
    2140
    /// Because speech rules can define variables, there is also a context that is carried with them
    2141
    pub struct SpeechRulesWithContext<'c, 's:'c, 'm:'c> {
    2142
        speech_rules: &'s SpeechRules,
    2143
        context_stack: ContextStack<'c>,   // current value of (context) variables
    2144
        doc: Document<'m>,
    2145
        nav_node_id: &'m str,
    2146
        nav_node_offset: usize,
    2147
        pub inside_spell: bool,     // hack to allow 'spell' to avoid infinite loop (see 'spell' implementation in tts.rs)
    2148
        pub translate_count: usize, // hack to avoid 'translate' infinite loop (see 'spell' implementation in tts.rs)
    2149
    }
    2150
    2151
    impl<'c, 's:'c, 'm:'c> fmt::Display for SpeechRulesWithContext<'c, 's,'m> {
    2152
    0
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    2153
    0
            writeln!(f, "SpeechRulesWithContext \n{})", self.speech_rules)?;
    2154
    0
            return writeln!(f, "   {} context entries, nav node id '({}, {})'", &self.context_stack, self.nav_node_id, self.nav_node_offset);
    2155
    0
        }
    2156
    }
    2157
    2158
    thread_local!{
    2159
        /// SPEECH_UNICODE_SHORT is shared among several rules, so "RC" is used
    2160
        static SPEECH_UNICODE_SHORT: UnicodeTable =
    2161
            Rc::new( RefCell::new( HashMap::with_capacity(500) ) );
    2162
    2163
        /// SPEECH_UNICODE_FULL is shared among several rules, so "RC" is used
    2164
        static SPEECH_UNICODE_FULL: UnicodeTable =
    2165
            Rc::new( RefCell::new( HashMap::with_capacity(6500) ) );
    2166
    2167
        /// BRAILLE_UNICODE_SHORT is shared among several rules, so "RC" is used
    2168
        static BRAILLE_UNICODE_SHORT: UnicodeTable =
    2169
            Rc::new( RefCell::new( HashMap::with_capacity(500) ) );
    2170
    2171
        /// BRAILLE_UNICODE_FULL is shared among several rules, so "RC" is used
    2172
        static BRAILLE_UNICODE_FULL: UnicodeTable =
    2173
            Rc::new( RefCell::new( HashMap::with_capacity(5000) ) );
    2174
    2175
        /// SPEECH_DEFINITION_FILES_AND_TIMES is shared among several rules, so "RC" is used
    2176
        static SPEECH_DEFINITION_FILES_AND_TIMES: FilesAndTimesShared =
    2177
            Rc::new( RefCell::new(FilesAndTimes::default()) );
    2178
    2179
        /// BRAILLE_DEFINITION_FILES_AND_TIMES is shared among several rules, so "RC" is used
    2180
        static BRAILLE_DEFINITION_FILES_AND_TIMES: FilesAndTimesShared =
    2181
            Rc::new( RefCell::new(FilesAndTimes::default()) );
    2182
    2183
        /// SPEECH_UNICODE_SHORT_FILES_AND_TIMES is shared among several rules, so "RC" is used
    2184
        static SPEECH_UNICODE_SHORT_FILES_AND_TIMES: FilesAndTimesShared =
    2185
            Rc::new( RefCell::new(FilesAndTimes::default()) );
    2186
    2187
        /// SPEECH_UNICODE_FULL_FILES_AND_TIMES is shared among several rules, so "RC" is used
    2188
        static SPEECH_UNICODE_FULL_FILES_AND_TIMES: FilesAndTimesShared =
    2189
            Rc::new( RefCell::new(FilesAndTimes::default()) );
    2190
    2191
        /// BRAILLE_UNICODE_SHORT_FILES_AND_TIMES is shared among several rules, so "RC" is used
    2192
        static BRAILLE_UNICODE_SHORT_FILES_AND_TIMES: FilesAndTimesShared =
    2193
            Rc::new( RefCell::new(FilesAndTimes::default()) );
    2194
    2195
        /// BRAILLE_UNICODE_FULL_FILES_AND_TIMES is shared among several rules, so "RC" is used
    2196
        static BRAILLE_UNICODE_FULL_FILES_AND_TIMES: FilesAndTimesShared =
    2197
            Rc::new( RefCell::new(FilesAndTimes::default()) );
    2198
    2199
        /// The current set of speech rules
    2200
        // maybe this should be a small cache of rules in case people switch rules/prefs?
    2201
        pub static INTENT_RULES: RefCell<SpeechRules> =
    2202
                RefCell::new( SpeechRules::new(RulesFor::Intent, true) );
    2203
    2204
        pub static SPEECH_RULES: RefCell<SpeechRules> =
    2205
                RefCell::new( SpeechRules::new(RulesFor::Speech, true) );
    2206
    2207
        pub static OVERVIEW_RULES: RefCell<SpeechRules> =
    2208
                RefCell::new( SpeechRules::new(RulesFor::OverView, true) );
    2209
    2210
        pub static NAVIGATION_RULES: RefCell<SpeechRules> =
    2211
                RefCell::new( SpeechRules::new(RulesFor::Navigation, true) );
    2212
    2213
        pub static BRAILLE_RULES: RefCell<SpeechRules> =
    2214
                RefCell::new( SpeechRules::new(RulesFor::Braille, false) );
    2215
    }
    2216
    2217
    /// Invalidate speech caches whose paths change when `Language` changes.
    2218
    4.10k
    pub fn invalidate_speech_language_caches() {
    2219
    4.10k
        SPEECH_DEFINITION_FILES_AND_TIMES.with(|files| files.borrow_mut().invalidate());
    2220
    4.10k
        SPEECH_UNICODE_SHORT_FILES_AND_TIMES.with(|files| files.borrow_mut().invalidate());
    2221
    4.10k
        SPEECH_UNICODE_FULL_FILES_AND_TIMES.with(|files| files.borrow_mut().invalidate());
    2222
    4.10k
        INTENT_RULES.with(|rules| rules.borrow_mut().rule_files.invalidate());
    2223
    4.10k
        SPEECH_RULES.with(|rules| rules.borrow_mut().rule_files.invalidate());
    2224
    4.10k
        OVERVIEW_RULES.with(|rules| rules.borrow_mut().rule_files.invalidate());
    2225
    4.10k
        NAVIGATION_RULES.with(|rules| rules.borrow_mut().rule_files.invalidate());
    2226
    4.10k
    }
    2227
    2228
    /// Invalidate caches whose paths change when `SpeechStyle` changes.
    2229
    1.51k
    pub fn invalidate_speech_style_caches() {
    2230
    1.51k
        SPEECH_RULES.with(|rules| rules.borrow_mut().rule_files.invalidate());
    2231
    1.51k
    }
    2232
    2233
    /// Invalidate braille caches whose paths change when `BrailleCode` changes.
    2234
    601
    pub fn invalidate_braille_caches() {
    2235
    601
        BRAILLE_DEFINITION_FILES_AND_TIMES.with(|files| files.borrow_mut().invalidate());
    2236
    601
        BRAILLE_UNICODE_SHORT_FILES_AND_TIMES.with(|files| files.borrow_mut().invalidate());
    2237
    601
        BRAILLE_UNICODE_FULL_FILES_AND_TIMES.with(|files| files.borrow_mut().invalidate());
    2238
    601
        BRAILLE_RULES.with(|rules| rules.borrow_mut().rule_files.invalidate());
    2239
    601
    }
    2240
    2241
    #[cfg(test)]
    2242
    // Used for testing the cache is invalidated when the language changes in prefs.rs
    2243
    impl SpeechRules {
    2244
    2
        pub(crate) fn rule_files_cache_is_empty(&self) -> bool {
    2245
    2
            self.rule_files.is_valid()
    2246
    2
        }
    2247
    2248
    3
        pub(crate) fn definitions_files_cache_is_empty(&self) -> bool {
    2249
    3
            self.definitions_files.borrow().is_valid()
    2250
    3
        }
    2251
    2252
    2
        pub(crate) fn definitions_files_cache_path(&self) -> PathBuf {
    2253
    2
            self.definitions_files.borrow().as_path().to_path_buf()
    2254
    2
        }
    2255
    }
    2256
    2257
    impl SpeechRules {
    2258
    17.7k
        pub fn new(name: RulesFor, translate_single_chars_only: bool) -> SpeechRules {
    2259
    17.7k
            let globals = if name == RulesFor::Braille {
    2260
    1.35k
                (
    2261
    1.35k
                    (BRAILLE_UNICODE_SHORT.with(Rc::clone), BRAILLE_UNICODE_SHORT_FILES_AND_TIMES.with(Rc::clone)),
    2262
    1.35k
                    (BRAILLE_UNICODE_FULL. with(Rc::clone), BRAILLE_UNICODE_FULL_FILES_AND_TIMES.with(Rc::clone)),
    2263
    1.35k
                    BRAILLE_DEFINITION_FILES_AND_TIMES.with(Rc::clone),
    2264
    1.35k
                )
    2265
            } else {
    2266
    16.4k
                (
    2267
    16.4k
                    (SPEECH_UNICODE_SHORT.with(Rc::clone), SPEECH_UNICODE_SHORT_FILES_AND_TIMES.with(Rc::clone)),
    2268
    16.4k
                    (SPEECH_UNICODE_FULL. with(Rc::clone), SPEECH_UNICODE_FULL_FILES_AND_TIMES.with(Rc::clone)),
    2269
    16.4k
                    SPEECH_DEFINITION_FILES_AND_TIMES.with(Rc::clone),
    2270
    16.4k
                )
    2271
            };
    2272
    2273
            return SpeechRules {
    2274
    17.7k
                error: Default::default(),
    2275
    17.7k
                name,
    2276
    17.7k
                rules: HashMap::with_capacity(if name == RulesFor::Intent || name == RulesFor::Speech13.6k {5008.28k} else {509.50k}),                       // lazy load them
    2277
    17.7k
                rule_files: FilesAndTimes::default(),
    2278
    17.7k
                unicode_short: globals.0.0,       // lazy load them
    2279
    17.7k
                unicode_short_files: globals.0.1,
    2280
    17.7k
                unicode_full: globals.1.0,        // lazy load them
    2281
    17.7k
                unicode_full_files: globals.1.1,
    2282
    17.7k
                definitions_files: globals.2,
    2283
    17.7k
                translate_single_chars_only,
    2284
    17.7k
                pref_manager: PreferenceManager::get(),
    2285
            };
    2286
    17.7k
    }
    2287
    2288
    17.7k
        pub fn get_error(&self) -> Option<&str> {
    2289
    17.7k
            return if self.error.is_empty() {
    2290
    17.7k
                 None
    2291
            } else {
    2292
    0
                Some(&self.error)
    2293
            }
    2294
    17.7k
        }
    2295
    2296
    15.3k
        pub fn read_files(&mut self) -> Result<()> {
    2297
    15.3k
            let check_rule_files = self.pref_manager.borrow().pref_to_string("CheckRuleFiles");
    2298
    15.3k
            if check_rule_files != "None" {  // "Prefs" or "All" are other values
    2299
    15.3k
                self.pref_manager.borrow_mut().set_preference_files()?0;
    2300
    2
            }
    2301
    15.3k
            let should_ignore_file_time = self.pref_manager.borrow().pref_to_string("CheckRuleFiles") != "All";     // ignore for "None", "Prefs"
    2302
    15.3k
            let rule_file = self.pref_manager.borrow().get_rule_file(&self.name).to_path_buf();     // need to create PathBuf to avoid a move/use problem
    2303
    15.3k
            if self.rules.is_empty() || !7.17kself.rule_files7.17k.is_file_up_to_date7.17k(&rule_file, should_ignore_file_time) {
    2304
    8.37k
                self.rules.clear();
    2305
    8.37k
                let files_read = self.read_patterns(&rule_file)?0;
    2306
    8.37k
                self.rule_files.set_files_and_times(files_read);
    2307
    6.93k
            }
    2308
    2309
    15.3k
            let pref_manager = self.pref_manager.borrow();
    2310
    15.3k
            let unicode_pref_files = if self.name == RulesFor::Braille {pref_manager.get_braille_unicode_file()1.82k} else {pref_manager.get_speech_unicode_file()13.4k};
    2311
    2312
    15.3k
            if !self.unicode_short_files.borrow().is_file_up_to_date(unicode_pref_files.0, should_ignore_file_time) {
    2313
    5.50k
                self.unicode_short.borrow_mut().clear();
    2314
    5.50k
                self.unicode_short_files.borrow_mut().set_files_and_times(self.read_unicode(None, true)?0);
    2315
    9.80k
            }
    2316
    2317
    15.3k
            if self.definitions_files.borrow().ft.is_empty() || !9.80kself.definitions_files.borrow()9.80k.is_file_up_to_date9.80k(
    2318
    9.80k
                                pref_manager.get_definitions_file(self.name != RulesFor::Braille),
    2319
    9.80k
                                should_ignore_file_time
    2320
    9.80k
            ) {
    2321
    5.50k
                self.definitions_files.borrow_mut().set_files_and_times(read_definitions_file(self.name != RulesFor::Braille)?0);
    2322
    9.80k
            }
    2323
    15.3k
            return Ok( () );
    2324
    15.3k
        }
    2325
    2326
    38.7k
        fn read_patterns(&mut self, path: &Path) -> Result<Vec<PathBuf>> {
    2327
            // info!("Reading rule file: {}", p.to_str().unwrap());
    2328
    38.7k
            let rule_file_contents = read_to_string_shim(path).with_context(|| format!0("cannot read file '{}'", path0.to_str0().unwrap0()))?0;
    2329
    38.7k
            let rules_build_fn = |pattern: &Yaml| {
    2330
    38.7k
                self.build_speech_patterns(pattern, path)
    2331
    38.7k
                    .with_context(||format!0("in file {:?}", path0.to_str0().unwrap0()))
    2332
    38.7k
            };
    2333
    38.7k
            return compile_rule(&rule_file_contents, rules_build_fn)
    2334
    38.7k
                    .with_context(||format!0("in file {:?}", path0.to_str0().unwrap0()));
    2335
    38.7k
        }
    2336
    2337
    38.7k
        fn build_speech_patterns(&mut self, patterns: &Yaml, file_name: &Path) -> Result<Vec<PathBuf>> {
    2338
            // Rule::SpeechPatternList
    2339
    38.7k
            let patterns_vec = patterns.as_vec();
    2340
    38.7k
            if patterns_vec.is_none() {
    2341
    0
                bail!(yaml_type_err(patterns, "array"));
    2342
    38.7k
            }
    2343
    38.7k
            let patterns_vec = patterns.as_vec().unwrap();
    2344
    38.7k
            let mut files_read = vec![file_name.to_path_buf()];
    2345
    896k
            for entry in patterns_vec.iter()38.7k {
    2346
    896k
                if let Some(mut added_files30.3k) = SpeechPattern::build(entry, file_name, self)?0 {
    2347
    30.3k
                    files_read.append(&mut added_files);
    2348
    866k
                }
    2349
            }
    2350
    38.7k
            return Ok(files_read)
    2351
    38.7k
        }
    2352
    2353
    5.97k
        fn read_unicode(&self, path: Option<PathBuf>, use_short: bool) -> Result<Vec<PathBuf>> {
    2354
    5.97k
            let path = match path {
    2355
    3
                Some(p) => p,
    2356
                None => {
    2357
                    // get the path to either the short or long unicode file
    2358
    5.97k
                    let pref_manager = self.pref_manager.borrow();
    2359
    5.97k
                    let unicode_files = if self.name == RulesFor::Braille {
    2360
    1.57k
                        pref_manager.get_braille_unicode_file()
    2361
                    } else {
    2362
    4.40k
                        pref_manager.get_speech_unicode_file()
    2363
                    };
    2364
    5.97k
                    let unicode_files = if use_short {unicode_files.05.50k} else {unicode_files.1468};
    2365
    5.97k
                    unicode_files.to_path_buf()
    2366
                }
    2367
            };
    2368
    2369
            // FIX: should read first (lang), then supplement with second (region)
    2370
            // info!("Reading unicode file {}", path.to_str().unwrap());
    2371
    5.97k
            let unicode_file_contents = read_to_string_shim(&path)?0;
    2372
    5.97k
            let unicode_build_fn = |unicode_def_list: &Yaml| {
    2373
    5.97k
                let unicode_defs = unicode_def_list.as_vec();
    2374
    5.97k
                if unicode_defs.is_none() {
    2375
    0
                    bail!("File '{}' does not begin with an array", yaml_to_type(unicode_def_list));
    2376
    5.97k
                };
    2377
    5.97k
                let mut files_read = vec![path.to_path_buf()];
    2378
    2.23M
                for unicode_def in unicode_defs5.97k.unwrap5.97k() {
    2379
    2.23M
                    if let Some(mut added_files3) = UnicodeDef::build(unicode_def, &path, self, use_short)
    2380
    2.23M
                                                                    .with_context(|| {format!0("In file {:?}", path.to_str()0)}0)?0 {
    2381
    3
                        files_read.append(&mut added_files);
    2382
    2.23M
                    }
    2383
                };
    2384
    5.97k
                return Ok(files_read)
    2385
    5.97k
            };
    2386
    2387
    5.97k
            return compile_rule(&unicode_file_contents, unicode_build_fn)
    2388
    5.97k
                        .with_context(||format!0("in file {:?}", path.to_str()0.unwrap0()));
    2389
    5.97k
        }
    2390
    2391
    0
        pub fn print_sizes() -> String {
    2392
            // let _ = &SPEECH_RULES.with_borrow(|rules| {
    2393
            //     debug!("SPEECH RULES entries\n");
    2394
            //     let rules = &rules.rules;
    2395
            //     for (key, _) in rules.iter() {
    2396
            //         debug!("key: {}", key);
    2397
            //     }
    2398
            // });
    2399
    0
            let mut answer = rule_size(&SPEECH_RULES, "SPEECH_RULES");
    2400
    0
            answer += &rule_size(&INTENT_RULES, "INTENT_RULES");
    2401
    0
            answer += &rule_size(&BRAILLE_RULES, "BRAILLE_RULES");
    2402
    0
            answer += &rule_size(&NAVIGATION_RULES, "NAVIGATION_RULES");
    2403
    0
            answer += &rule_size(&OVERVIEW_RULES, "OVERVIEW_RULES");
    2404
    0
            SPEECH_RULES.with_borrow(|rule| {
    2405
    0
                answer += &format!("Speech Unicode tables: short={}/{}, long={}/{}\n",
    2406
    0
                                    rule.unicode_short.borrow().len(), rule.unicode_short.borrow().capacity(),
    2407
    0
                                    rule.unicode_full.borrow().len(), rule.unicode_full.borrow().capacity());
    2408
    0
            });
    2409
    0
            BRAILLE_RULES.with_borrow(|rule| {
    2410
    0
                answer += &format!("Braille Unicode tables: short={}/{}, long={}/{}\n",
    2411
    0
                                    rule.unicode_short.borrow().len(), rule.unicode_short.borrow().capacity(),
    2412
    0
                                    rule.unicode_full.borrow().len(), rule.unicode_full.borrow().capacity());
    2413
    0
            });
    2414
    0
            return answer;
    2415
    2416
    0
            fn rule_size(rules: &'static std::thread::LocalKey<RefCell<SpeechRules>>, name: &str) -> String {
    2417
    0
                rules.with_borrow(|rule| {
    2418
    0
                    let hash_map = &rule.rules;
    2419
    0
                    return format!("{}: {}/{}\n", name, hash_map.len(), hash_map.capacity());
    2420
    0
                })
    2421
    0
            }
    2422
    0
        }
    2423
    }
    2424
    2425
    2426
    /// We track three different lifetimes:
    2427
    ///   'c -- the lifetime of the context and mathml
    2428
    ///   's -- the lifetime of the speech rules (which is static)
    2429
    ///   'r -- the lifetime of the reference (this seems to be key to keep the rust memory checker happy)
    2430
    impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> {
    2431
    22.7k
        pub fn new(speech_rules: &'s SpeechRules, doc: Document<'m>, nav_node_id: &'m str, nav_node_offset: usize) -> SpeechRulesWithContext<'c, 's, 'm> {
    2432
    22.7k
            return SpeechRulesWithContext {
    2433
    22.7k
                speech_rules,
    2434
    22.7k
                context_stack: ContextStack::new(&speech_rules.pref_manager.borrow()),
    2435
    22.7k
                doc,
    2436
    22.7k
                nav_node_id,
    2437
    22.7k
                nav_node_offset,
    2438
    22.7k
                inside_spell: false,
    2439
    22.7k
                translate_count: 0,
    2440
    22.7k
            }
    2441
    22.7k
        }
    2442
    2443
    1.84k
        pub fn get_rules(&mut self) -> &SpeechRules {
    2444
    1.84k
            return self.speech_rules;
    2445
    1.84k
        }
    2446
    2447
    45.5k
        pub fn get_context(&mut self) -> &mut sxd_xpath::Context<'c> {
    2448
    45.5k
            return &mut self.context_stack.base;
    2449
    45.5k
        }
    2450
    2451
    3.23k
        pub fn get_document(&mut self) -> Document<'m> {
    2452
    3.23k
            return self.doc;
    2453
    3.23k
        }
    2454
    2455
    1.13k
        pub fn set_nav_node_offset(&mut self, offset: usize) {
    2456
            // debug!("Setting nav node offset to {}", offset);
    2457
    1.13k
            self.nav_node_offset = offset;
    2458
    1.13k
        }
    2459
    2460
    121k
        pub fn match_pattern<T:TreeOrString<'c, 'm, T>>(&'r mut self, mathml: Element<'c>) -> Result<T> {
    2461
            // debug!("Looking for a match for: \n{}", mml_to_string(mathml));
    2462
    121k
            let tag_name = mathml.name().local_part();
    2463
    121k
            let rules = &self.speech_rules.rules;
    2464
    2465
            // start with priority rules that apply to any node (should be a very small number)
    2466
    121k
            if let Some(rule_vector95.8k) = rules.get("!*") &&
    2467
    95.8k
               let Some(result3.18k) = self.find_match(rule_vector, mathml)?9 {
    2468
    3.18k
                    return Ok(result);      // found a match
    2469
    118k
                }
    2470
    2471
    118k
            if let Some(rule_vector116k) = rules.get(tag_name) &&
    2472
    116k
               let Some(result82.1k) = self.find_match(rule_vector, mathml)?0 {
    2473
    82.1k
                    return Ok(result);      // found a match
    2474
    35.9k
                }
    2475
    2476
            // no rules for specific element, fall back to rules for "*" which *should* be present in all rule files as fallback
    2477
    35.9k
            if let Some(rule_vector) = rules.get("*") &&
    2478
    35.9k
               let Some(result) = self.find_match(rule_vector, mathml)?0 {
    2479
    35.9k
                    return Ok(result);      // found a match
    2480
    0
                }
    2481
    2482
            // no rules matched -- poorly written rule file -- let flow through to default error
    2483
            // report error message with file name
    2484
    0
            let speech_manager = self.speech_rules.pref_manager.borrow();
    2485
    0
            let file_name = speech_manager.get_rule_file(&self.speech_rules.name);
    2486
            // FIX: handle error appropriately
    2487
    0
            bail!("\nNo match found!\nMissing patterns in {} for MathML.\n{}", file_name.to_string_lossy(), mml_to_string(mathml));
    2488
    121k
        }
    2489
    2490
    248k
        fn find_match<T:TreeOrString<'c, 'm, T>>(&'r mut self, rule_vector: &[Box<SpeechPattern>], mathml: Element<'c>) -> Result<Option<T>> {
    2491
    870k
            for pattern in rule_vector248k {
    2492
                // debug!("Pattern name: {}", pattern.pattern_name);
    2493
                // always pushing and popping around the is_match would be a little cleaner, but push/pop is relatively expensive,
    2494
                //   so we optimize and only push first if the variables are needed to do the match
    2495
    870k
                if pattern.match_uses_var_defs {
    2496
    7.05k
                    self.context_stack.push(pattern.var_defs.clone(), mathml)?0;
    2497
    863k
                }
    2498
    870k
                if pattern.is_match(&self.context_stack.base, mathml)
    2499
    870k
                        .with_context(|| error_string0(pattern0, mathml0) )?0 {
    2500
                    // debug!("  find_match: FOUND!!!");
    2501
    121k
                    if !pattern.match_uses_var_defs && pattern.var_defs.len() > 0119k { // don't push them on twice
    2502
    13.0k
                        self.context_stack.push(pattern.var_defs.clone(), mathml)?0;
    2503
    108k
                    }
    2504
    121k
                    let result = if self.nav_node_offset > 0 &&
    2505
    47
                                self.nav_node_id == mathml.attribute_value("id").unwrap_or_default() && is_leaf7(mathml7) {
    2506
    7
                        let ch = crate::canonicalize::as_text(mathml).chars().nth(self.nav_node_offset-1).unwrap_or_default();
    2507
    7
                        let ch = self.replace_single_char(ch, mathml)?0;
    2508
                        // debug!("find_match: ch={} from '{}'; matched pattern name/tag: {}/{} with nav_node_offset={}",
    2509
                        //     ch, crate::canonicalize::as_text(mathml),
    2510
                        //     pattern.pattern_name, pattern.tag_name, self.nav_node_offset);
    2511
    7
                        T::from_string(ch.to_string(), self.doc)
    2512
                    } else {
    2513
    121k
                        pattern.replacements.replace(self, mathml)
    2514
                    };
    2515
    121k
                    if pattern.var_defs.len() > 0 {
    2516
    14.5k
                        self.context_stack.pop();
    2517
    106k
                    }
    2518
    121k
                    return match result {
    2519
    121k
                        Ok(s) => {
    2520
                            // for all except braille and navigation, nav_node_id will be an empty string and will not match
    2521
    121k
                            if self.nav_node_id.is_empty() {
    2522
    102k
                                Ok( Some(s) )
    2523
                            } else {
    2524
    18.5k
                                if self.nav_node_id == mathml.attribute_value("id").unwrap_or_default() {debug!990("Matched pattern name/tag: {}/{}", pattern.pattern_name, pattern.tag_name)}17.5k;
    2525
    18.5k
                                Ok ( Some(self.nav_node_adjust(s, mathml)) )
    2526
                            }
    2527
                        },
    2528
    9
                        Err(e) => Err( e.context(
    2529
    9
                            format!(
    2530
    9
                                "attempting replacement pattern: \"{}\" for \"{}\".\n\
    2531
    9
                                Replacement\n{}\n...due to matching the MathML\n{} with the pattern\n\
    2532
    9
                                {}\n\
    2533
    9
                                The patterns are in {}.\n",
    2534
    9
                                pattern.pattern_name, pattern.tag_name,
    2535
    9
                                pattern.replacements.pretty_print_replacements(),
    2536
    9
                                mml_to_string(mathml), pattern.pattern,
    2537
    9
                                pattern.file_name
    2538
    9
                            )
    2539
    9
                        ))
    2540
                    }
    2541
    749k
                } else if pattern.match_uses_var_defs {
    2542
    5.60k
                    self.context_stack.pop();
    2543
    743k
                }
    2544
            };
    2545
    127k
            return Ok(None);    // no matches
    2546
    2547
    0
            fn error_string(pattern: &SpeechPattern, mathml: Element) -> String {
    2548
    0
                return format!(
    2549
                    "error during pattern match using: \"{}\" for \"{}\".\n\
    2550
                    Pattern is \n{}\nMathML for the match:\n\
    2551
                    {}\
    2552
                    The patterns are in {}.\n",
    2553
                    pattern.pattern_name, pattern.tag_name,
    2554
                    pattern.pattern,
    2555
    0
                    mml_to_string(mathml),
    2556
                    pattern.file_name
    2557
                );
    2558
    0
            }
    2559
    2560
    248k
        }
    2561
    2562
    18.5k
        fn nav_node_adjust<T:TreeOrString<'c, 'm, T>>(&self, speech: T, mathml: Element<'c>) -> T {
    2563
    18.5k
          if let Some(id) = mathml.attribute_value("id") &&
    2564
    18.5k
             self.nav_node_id == id {
    2565
    990
            let offset = mathml.attribute_value(crate::navigate::ID_OFFSET).unwrap_or("0");
    2566
    990
            debug!("nav_node_adjust: id/name='{}/{}' offset?='{}'", id, name0(mathml0),
    2567
    0
                   self.nav_node_offset.to_string().as_str() == offset
    2568
            );
    2569
    990
            if is_leaf(mathml) || self.nav_node_offset.to_string().as_str() == offset527 {
    2570
    990
              if self.speech_rules.name == RulesFor::Braille {
    2571
    469
                let highlight_style =  self.speech_rules.pref_manager.borrow().pref_to_string("BrailleNavHighlight");
    2572
    469
                return T::highlight_braille(speech, highlight_style);
    2573
              } else {
    2574
    521
                debug!("nav_node_adjust: id='{}' offset='{}/{}'", id, self.nav_node_offset, offset);
    2575
    521
                return T::mark_nav_speech(speech)
    2576
              }
    2577
    0
            }
    2578
    17.5k
          }
    2579
    17.5k
          return speech;
    2580
    18.5k
        }
    2581
    2582
    469
        fn highlight_braille_string(braille: String, highlight_style: String) -> String {
    2583
            // add dots 7 & 8 to the Unicode braille (28xx)
    2584
    469
            if &highlight_style == "Off" || braille.is_empty() {
    2585
    6
                return braille;
    2586
    463
            }
    2587
    2588
            // FIX: this seems needlessly complex. It is much simpler if the char can be changed in place...
    2589
            // find first char that can get the dots and add them
    2590
    463
            let mut chars = braille.chars().collect::<Vec<char>>();
    2591
    2592
            // the 'b' for baseline indicator is really part of the previous token, so it needs to be highlighted but isn't because it is not Unicode braille
    2593
    463
            let baseline_indicator_hack = PreferenceManager::get().borrow().pref_to_string("BrailleCode") == "Nemeth";
    2594
            // debug!("highlight_braille_string: highlight_style={}\n braille={}", highlight_style, braille);
    2595
    463
            let mut i_first_modified = 0;
    2596
    760
            for (i, ch) in chars.iter_mut()463.enumerate463() {
    2597
    760
                let modified_ch = add_dots_to_braille_char(*ch, baseline_indicator_hack);
    2598
    760
                if *ch != modified_ch {
    2599
    463
                    *ch = modified_ch;
    2600
    463
                    i_first_modified = i;
    2601
    463
                    break;
    2602
    297
                };
    2603
            };
    2604
    2605
    463
            let mut i_last_modified = i_first_modified;
    2606
    463
            if &highlight_style != "FirstChar" {
    2607
                // find last char so that we know when to modify the char
    2608
    491
                for i in (i_first_modified463..chars.len()).rev463(){
    2609
    491
                    let ch = chars[i];
    2610
    491
                    let modified_ch = add_dots_to_braille_char(ch, baseline_indicator_hack);
    2611
    491
                    chars[i] = modified_ch;
    2612
    491
                    if ch !=  modified_ch {
    2613
    390
                        i_last_modified = i;
    2614
    390
                        break;
    2615
    101
                    }
    2616
                }
    2617
    0
            }
    2618
    2619
    463
            if &highlight_style == "All" {
    2620
                // finish going through the string
    2621
          #[allow(clippy::needless_range_loop)]  // I don't like enumerate/take/skip here
    2622
    4
                for i0 in i_first_modified+1..i_last_modified {
    2623
    0
                    chars[i] = add_dots_to_braille_char(chars[i], baseline_indicator_hack);
    2624
    0
                };
    2625
    459
            }
    2626
    2627
    463
            let result = chars.into_iter().collect::<String>();
    2628
            // debug!("    result={}", result);
    2629
    463
            return result;
    2630
    2631
    1.25k
            fn add_dots_to_braille_char(ch: char, baseline_indicator_hack: bool) -> char {
    2632
    1.25k
                let as_u32 = ch as u32;
    2633
    1.25k
                if (0x2800..0x28FF).contains(&as_u32) {
    2634
    919
                    return unsafe {char::from_u32_unchecked(as_u32 | 0xC0)};  // safe because we have checked the range
    2635
    332
                } else if baseline_indicator_hack && ch == 'b'89 {
    2636
    7
                    return '𝑏'
    2637
                } else {
    2638
    325
                    return ch;
    2639
                }
    2640
    1.25k
            }
    2641
    469
        }
    2642
    2643
    521
        fn mark_nav_speech(speech: String) -> String {
    2644
            // add unique markers (since speech is mostly ascii letters and digits, most any symbol will do)
    2645
            // it's a bug (but happened during intent generation), we might have identical id's, choose innermost one
    2646
    521
            debug!("mark_nav_speech: adding [[ {} ]] ", &speech0);
    2647
    521
            if !speech.contains("[[") {
    2648
    521
                return "[[".to_string() + &speech + "]]";
    2649
            } else {
    2650
    0
                return speech
    2651
            }
    2652
    521
        }
    2653
    2654
    456k
        fn replace<T:TreeOrString<'c, 'm, T>>(&'r mut self, replacement: &Replacement, mathml: Element<'c>) -> Result<T> {
    2655
            return Ok(
    2656
    456k
                match replacement {
    2657
    63.6k
                    Replacement::Text(t) => T::from_string(t.clone(), self.doc)?0,
    2658
    151k
                    Replacement::XPath(xpath) => xpath.replace(self, mathml)?9,
    2659
    60.7k
                    Replacement::TTS(tts) => {
    2660
    60.7k
                        T::from_string(
    2661
    60.7k
                            self.speech_rules.pref_manager.borrow().get_tts().replace(tts, &self.speech_rules.pref_manager.borrow(), self, mathml)?0,
    2662
    60.7k
                            self.doc
    2663
    0
                        )?
    2664
                    },
    2665
    45.5k
                    Replacement::Intent(intent) => {
    2666
    45.5k
                        intent.replace(self, mathml)?0
    2667
                    },
    2668
    115k
                    Replacement::Test(test) => {
    2669
    115k
                        test.replace(self, mathml)?0
    2670
                    },
    2671
    7.28k
                    Replacement::With(with) => {
    2672
    7.28k
                        with.replace(self, mathml)?0
    2673
                    },
    2674
    3.78k
                    Replacement::SetVariables(vars) => {
    2675
    3.78k
                        vars.replace(self, mathml)?0
    2676
                    },
    2677
    7.45k
                    Replacement::Insert(ic) => {
    2678
    7.45k
                        ic.replace(self, mathml)?0
    2679
                    },
    2680
    2
                    Replacement::Translate(id) => {
    2681
    2
                        id.replace(self, mathml)?0
    2682
                    },
    2683
                }
    2684
            )
    2685
    456k
        }
    2686
    2687
        /// Iterate over all the nodes, concatenating the result strings together with a ' ' between them
    2688
        /// If the node is an element, pattern match it
    2689
        /// For 'Text' and 'Attribute' nodes, convert them to strings
    2690
    121k
        fn replace_nodes<T:TreeOrString<'c, 'm, T>>(&'r mut self, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<T> {
    2691
    121k
            return T::replace_nodes(self, nodes, mathml);
    2692
    121k
        }
    2693
    2694
        /// Iterate over all the nodes finding matches for the elements
    2695
        /// For this case of returning MathML, everything else is an error
    2696
    48.6k
        fn replace_nodes_tree(&'r mut self, nodes: Vec<Node<'c>>, _mathml: Element<'c>) -> Result<Element<'m>> {
    2697
    48.6k
            let mut children = Vec::with_capacity(3*nodes.len());   // guess (2 chars/node + space)
    2698
    69.6k
            for node in nodes48.6k {
    2699
    69.6k
                let matched = match node {
    2700
    41.9k
                    Node::Element(n) => self.match_pattern::<Element<'m>>(n)?0,
    2701
    27.5k
                    Node::Text(t) =>  {
    2702
    27.5k
                        let leaf = create_mathml_element(&self.doc, "TEMP_NAME");
    2703
    27.5k
                        leaf.set_text(t.text());
    2704
    27.5k
                        leaf
    2705
                    },
    2706
    32
                    Node::Attribute(attr) => {
    2707
                        // debug!("  from attr with text '{}'", attr.value());
    2708
    32
                        let leaf = create_mathml_element(&self.doc, "TEMP_NAME");
    2709
    32
                        leaf.set_text(attr.value());
    2710
    32
                        leaf
    2711
                    },
    2712
                    _ => {
    2713
    0
                        bail!("replace_nodes: found unexpected node type!!!");
    2714
                    },
    2715
                };
    2716
    69.6k
                children.push(matched);
    2717
            }
    2718
    2719
    48.6k
            let result = create_mathml_element(&self.doc, "TEMP_NAME");    // FIX: what name should be used?
    2720
    48.6k
            result.append_children(children);
    2721
            // debug!("replace_nodes_tree\n{}\n====>>>>>\n", mml_to_string(result));
    2722
    48.6k
            return Ok( result );
    2723
    48.6k
        }
    2724
    2725
    72.9k
        fn replace_nodes_string(&'r mut self, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<String> {
    2726
            // debug!("replace_nodes: working on {} nodes", nodes.len());
    2727
    72.9k
            let mut result = String::with_capacity(3*nodes.len());   // guess (2 chars/node + space)
    2728
    72.9k
            let mut first_time = true;
    2729
    85.7k
            for node in nodes72.9k {
    2730
    85.7k
                if first_time {
    2731
    72.9k
                    first_time = false;
    2732
    72.9k
                } else {
    2733
    12.8k
                    result.push(' ');
    2734
    12.8k
                };
    2735
    85.7k
                let matched = match node {
    2736
    66.5k
                    Node::Element(n) => self.match_pattern::<String>(n)?0,
    2737
    19.2k
                    Node::Text(t) =>  self.replace_chars(t.text(), mathml)?0,
    2738
    14
                    Node::Attribute(attr) => self.replace_chars(attr.value(), mathml)?0,
    2739
    0
                    _ => bail!("replace_nodes: found unexpected node type!!!"),
    2740
                };
    2741
    85.7k
                result += &matched;
    2742
            }
    2743
    72.9k
            return Ok( result );
    2744
    72.9k
        }
    2745
    2746
        /// Lookup unicode "pronunciation" of char.
    2747
        /// Note: TTS is not supported here (not needed and a little less efficient)
    2748
    58.0k
        pub fn replace_chars(&'r mut self, str: &str, mathml: Element<'c>) -> Result<String> {
    2749
    58.0k
            let chars = str.chars().collect::<Vec<char>>();
    2750
    58.0k
            let rules = self.speech_rules;
    2751
            // handled in match_pattern -- temporarily leaving as comments in case something is missed and needed here
    2752
            // if self.nav_node_offset > 0 && chars.len() > 1 {
    2753
            //     if self.nav_node_offset > chars.len() {
    2754
            //         debug!("replace_chars: nav_node_offset {} is larger than string length {}", self.nav_node_offset, chars.len());
    2755
            //         self.nav_node_offset = chars.len();
    2756
            //     }
    2757
            //     let ch = chars[self.nav_node_offset-1];
    2758
            //     debug!("replace_chars: adjusted string to '{}' based on nav_node_offset {}", ch, self.nav_node_offset);
    2759
            //     if rules.translate_single_chars_only {
    2760
            //         return self.replace_single_char(ch, mathml);
    2761
            //     } else {
    2762
            //         return Ok( ch.to_string() );
    2763
            //     }
    2764
            // }
    2765
    58.0k
            if is_quoted_string(str) {  // quoted string -- already translated (set in get_braille_chars)
    2766
    12.5k
                return Ok(unquote_string(str).to_string());
    2767
    45.5k
            }
    2768
            // in a string, avoid "a" -> "eigh", "." -> "point", etc
    2769
    45.5k
            if rules.translate_single_chars_only {
    2770
    30.0k
                if chars.len() == 1 {
    2771
    27.3k
                    return self.replace_single_char(chars[0], mathml)
    2772
                } else {
    2773
                    // more than one char -- fix up non-breaking space
    2774
    2.69k
                    return Ok(str.replace('\u{00A0}', " ").replace(['\u{2061}', '\u{2062}', '\u{2063}', '\u{2064}'], ""))
    2775
                }
    2776
    15.5k
            };
    2777
    2778
    15.5k
            let result = chars.iter()
    2779
    18.2k
                .map15.5k(|&ch| self.replace_single_char(ch, mathml))
    2780
    15.5k
                .collect::<Result<Vec<String>>>()?0
    2781
    15.5k
                .join("");
    2782
    15.5k
            return Ok( result );
    2783
    58.0k
        }
    2784
    2785
    45.6k
        fn replace_single_char(&'r mut self, ch: char, mathml: Element<'c>) -> Result<String> {
    2786
    45.6k
            let ch_as_u32 = ch as u32;
    2787
    45.6k
            let rules =  self.speech_rules;
    2788
    45.6k
            let mut unicode = rules.unicode_short.borrow();
    2789
    45.6k
            let mut replacements = unicode.get( &ch_as_u32 );
    2790
            // debug!("replace_single_char: looking for unicode {} for char '{}'/{:#06x}, found: {:?}", rules.name, ch, ch_as_u32, replacements);
    2791
    45.6k
            if replacements.is_none() {
    2792
                // see if it in the full unicode table (if it isn't loaded already)
    2793
    1.64k
                let pref_manager = rules.pref_manager.borrow();
    2794
    1.64k
                let unicode_pref_files = if rules.name == RulesFor::Braille {pref_manager.get_braille_unicode_file()525} else {pref_manager.get_speech_unicode_file()1.12k};
    2795
    1.64k
                let should_ignore_file_time = pref_manager.pref_to_string("CheckRuleFiles") == "All";
    2796
    1.64k
                if rules.unicode_full.borrow().is_empty() || !1.18krules.unicode_full_files.borrow()1.18k.is_file_up_to_date1.18k(unicode_pref_files.1, should_ignore_file_time) {
    2797
    468
                    info!("*** Loading full unicode {} for char '{}'/{:#06x}", rules.name, ch, ch_as_u32);
    2798
    468
                    rules.unicode_full.borrow_mut().clear();
    2799
    468
                    rules.unicode_full_files.borrow_mut().set_files_and_times(rules.read_unicode(None, false)?0);
    2800
    468
                    info!("# Unicode defs = {}/{}", rules.unicode_short.borrow().len()0, rules.unicode_full.borrow().len()0);
    2801
    1.18k
                }
    2802
    1.64k
                unicode = rules.unicode_full.borrow();
    2803
    1.64k
                replacements = unicode.get( &ch_as_u32 );
    2804
    1.64k
                if replacements.is_none() {
    2805
    269
                  self.translate_count = 0;     // not in loop
    2806
                  // debug!("*** Did not find unicode {} for char '{}'/{:#06x}", rules.name, ch, ch_as_u32);
    2807
    269
                  if rules.translate_single_chars_only || ch247.is_ascii247() {  // speech or if braille, avoid loop (ASCII remains ASCII if not found)
    2808
    269
                    return Ok(String::from(ch));   // no replacement, so just return the char and hope for the best
    2809
                  } else { // braille -- must turn into braille dots
    2810
                    // Emulate what NVDA does: generate (including single quotes) '\xhhhh' or '\yhhhhhh'
    2811
    0
                    let ch_as_int = ch as u32;
    2812
    0
                    let prefix_indicator = if ch_as_int < 1<<16 {'x'} else {'y'};
    2813
    0
                    return self.replace_chars( &format!("'\\{prefix_indicator}{:06x}'", ch_as_int), mathml);
    2814
                  }
    2815
    1.37k
                }
    2816
    43.9k
            };
    2817
    2818
            // map across all the parts of the replacement, collect them up into a Vec, and then concat them together
    2819
    45.3k
            let result = replacements.unwrap()
    2820
    45.3k
                        .iter()
    2821
    45.3k
                        .map(|replacement|
    2822
    49.1k
                             self.replace(replacement, mathml)
    2823
    49.1k
                                    .with_context(|| format!0("Unicode replacement error: {replacement}")) )
    2824
    45.3k
                        .collect::<Result<Vec<String>>>()?0
    2825
    45.3k
                        .join(" ");
    2826
    45.3k
             self.translate_count = 0;     // found a replacement, so not in a loop
    2827
    45.3k
            return Ok(result);
    2828
    45.6k
        }
    2829
    }
    2830
    2831
    /// Hack to allow replacement of `str` with braille chars.
    2832
    12.5k
    pub fn braille_replace_chars(str: &str, mathml: Element) -> Result<String> {
    2833
    12.5k
        return BRAILLE_RULES.with(|rules| {
    2834
    12.5k
            let rules = rules.borrow();
    2835
    12.5k
            let new_package = Package::new();
    2836
    12.5k
            let mut rules_with_context = SpeechRulesWithContext::new(&rules, new_package.as_document(), "", 0);
    2837
    12.5k
            return match rules_with_context.replace_chars(str, mathml) {
    2838
    12.5k
                Ok(s) => Ok(
    2839
    12.5k
                    s.replace(CONCAT_STRING, "")
    2840
    12.5k
                     .replace(CONCAT_INDICATOR, "")
    2841
    12.5k
                     .replace(POSTFIX_CONCAT_STRING, "")
    2842
    12.5k
                     .replace(POSTFIX_CONCAT_INDICATOR, "")
    2843
    12.5k
                ),
    2844
    0
                Err(e) => Err(e),
    2845
            }
    2846
    2847
    2848
    12.5k
        })
    2849
    12.5k
    }
    2850
    2851
    2852
    2853
    #[cfg(test)]
    2854
    mod tests {
    2855
        #[allow(unused_imports)]
    2856
        use crate::init_logger;
    2857
    2858
        use super::*;
    2859
    2860
        #[test]
    2861
    1
        fn test_read_statement() {
    2862
    1
            let str = r#"---
    2863
    1
            {name: default, tag: math, match: ".", replace: [x: "./*"] }"#;
    2864
    1
            let doc = YamlLoader::load_from_str(str).unwrap();
    2865
    1
            assert_eq!(doc.len(), 1);
    2866
    1
            let mut rules = SpeechRules::new(RulesFor::Speech, true);
    2867
    2868
    1
            SpeechPattern::build(&doc[0], Path::new("testing"), &mut rules).unwrap();
    2869
    1
            assert_eq!(rules.rules["math"].len(), 1, "\nshould only be one rule");
    2870
    2871
    1
            let speech_pattern = &rules.rules["math"][0];
    2872
    1
            assert_eq!(speech_pattern.pattern_name, "default", "\npattern name failure");
    2873
    1
            assert_eq!(speech_pattern.tag_name, "math", "\ntag name failure");
    2874
    1
            assert_eq!(speech_pattern.pattern.rc.string, ".", "\npattern failure");
    2875
    1
            assert_eq!(speech_pattern.replacements.replacements.len(), 1, "\nreplacement failure");
    2876
    1
            assert_eq!(speech_pattern.replacements.replacements[0].to_string(), r#""./*""#, "\nreplacement failure");
    2877
    1
        }
    2878
    2879
        #[test]
    2880
    1
        fn test_read_statements_with_replace() {
    2881
    1
            let str = r#"---
    2882
    1
            {name: default, tag: math, match: ".", replace: [x: "./*"] }"#;
    2883
    1
            let doc = YamlLoader::load_from_str(str).unwrap();
    2884
    1
            assert_eq!(doc.len(), 1);
    2885
    1
            let mut rules = SpeechRules::new(RulesFor::Speech, true);
    2886
    1
            SpeechPattern::build(&doc[0], Path::new("testing"), &mut rules).unwrap();
    2887
    2888
    1
            let str = r#"---
    2889
    1
            {name: default, tag: math, match: ".", replace: [t: "test", x: "./*"] }"#;
    2890
    1
            let doc2 = YamlLoader::load_from_str(str).unwrap();
    2891
    1
            assert_eq!(doc2.len(), 1);
    2892
    1
            SpeechPattern::build(&doc2[0], Path::new("testing"), &mut rules).unwrap();
    2893
    1
            assert_eq!(rules.rules["math"].len(), 1, "\nfirst rule not replaced");
    2894
    2895
    1
            let speech_pattern = &rules.rules["math"][0];
    2896
    1
            assert_eq!(speech_pattern.pattern_name, "default", "\npattern name failure");
    2897
    1
            assert_eq!(speech_pattern.tag_name, "math", "\ntag name failure");
    2898
    1
            assert_eq!(speech_pattern.pattern.rc.string, ".", "\npattern failure");
    2899
    1
            assert_eq!(speech_pattern.replacements.replacements.len(), 2, "\nreplacement failure");
    2900
    1
        }
    2901
    2902
        #[test]
    2903
    1
        fn test_read_statements_with_add() {
    2904
    1
            let str = r#"---
    2905
    1
            {name: default, tag: math, match: ".", replace: [x: "./*"] }"#;
    2906
    1
            let doc = YamlLoader::load_from_str(str).unwrap();
    2907
    1
            assert_eq!(doc.len(), 1);
    2908
    1
            let mut rules = SpeechRules::new(RulesFor::Speech, true);
    2909
    1
            SpeechPattern::build(&doc[0], Path::new("testing"), &mut rules).unwrap();
    2910
    2911
    1
            let str = r#"---
    2912
    1
            {name: another-rule, tag: math, match: ".", replace: [t: "test", x: "./*"] }"#;
    2913
    1
            let doc2 = YamlLoader::load_from_str(str).unwrap();
    2914
    1
            assert_eq!(doc2.len(), 1);
    2915
    1
            SpeechPattern::build(&doc2[0], Path::new("testing"), &mut rules).unwrap();
    2916
    1
            assert_eq!(rules.rules["math"].len(), 2, "\nsecond rule not added");
    2917
    2918
    1
            let speech_pattern = &rules.rules["math"][0];
    2919
    1
            assert_eq!(speech_pattern.pattern_name, "default", "\npattern name failure");
    2920
    1
            assert_eq!(speech_pattern.tag_name, "math", "\ntag name failure");
    2921
    1
            assert_eq!(speech_pattern.pattern.rc.string, ".", "\npattern failure");
    2922
    1
            assert_eq!(speech_pattern.replacements.replacements.len(), 1, "\nreplacement failure");
    2923
    1
        }
    2924
    2925
        #[test]
    2926
    1
        fn test_debug_no_debug() {
    2927
    1
            let str = r#"*[2]/*[3][text()='3']"#;
    2928
    1
            let result = MyXPath::add_debug_string_arg(str);
    2929
    1
            assert!(result.is_ok());
    2930
    1
            assert_eq!(result.unwrap(), str);
    2931
    1
        }
    2932
    2933
        #[test]
    2934
    1
        fn test_debug_no_debug_with_quote() {
    2935
    1
            let str = r#"*[2]/*[3][text()='(']"#;
    2936
    1
            let result = MyXPath::add_debug_string_arg(str);
    2937
    1
            assert!(result.is_ok());
    2938
    1
            assert_eq!(result.unwrap(), str);
    2939
    1
        }
    2940
    2941
        #[test]
    2942
    1
        fn test_debug_no_quoted_paren() {
    2943
    1
            let str = r#"DEBUG(*[2]/*[3][text()='3'])"#;
    2944
    1
            let result = MyXPath::add_debug_string_arg(str);
    2945
    1
            assert!(result.is_ok());
    2946
    1
            assert_eq!(result.unwrap(), r#"DEBUG(*[2]/*[3][text()='3'], "*[2]/*[3][text()='3']")"#);
    2947
    1
        }
    2948
    2949
        #[test]
    2950
    1
        fn test_debug_quoted_paren() {
    2951
    1
            let str = r#"DEBUG(*[2]/*[3][text()='('])"#;
    2952
    1
            let result = MyXPath::add_debug_string_arg(str);
    2953
    1
            assert!(result.is_ok());
    2954
    1
            assert_eq!(result.unwrap(), r#"DEBUG(*[2]/*[3][text()='('], "*[2]/*[3][text()='(']")"#);
    2955
    1
        }
    2956
    2957
        #[test]
    2958
    1
        fn test_debug_quoted_paren_before_paren() {
    2959
    1
            let str = r#"DEBUG(ClearSpeak_Matrix = 'Combinatorics') and IsBracketed(., '(', ')')"#;
    2960
    1
            let result = MyXPath::add_debug_string_arg(str);
    2961
    1
            assert!(result.is_ok());
    2962
    1
            assert_eq!(result.unwrap(), r#"DEBUG(ClearSpeak_Matrix = 'Combinatorics', "ClearSpeak_Matrix = 'Combinatorics'") and IsBracketed(., '(', ')')"#);
    2963
    1
        }
    2964
    2965
    2966
    // zipped files do NOT include "zz", hence we need to exclude this test
    2967
    cfg_if::cfg_if! {if #[cfg(not(feature = "include-zip"))] {
    2968
        #[test]
    2969
    1
        fn test_up_to_date() {
    2970
            use crate::interface::*;
    2971
            // initialize and move to a directory where making a time change doesn't really matter
    2972
    1
            set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
    2973
    1
            set_preference("Language", "zz-aa").unwrap();
    2974
            // not much is support in zz
    2975
    1
            if let Err(e0) = set_mathml("<math><mi>x</mi></math>") {
    2976
    0
                error!("{}", crate::errors_to_string(&e));
    2977
    0
                panic!("Should not be an error in setting MathML")
    2978
    1
            }
    2979
    2980
    1
            set_preference("CheckRuleFiles", "All").unwrap();
    2981
    1
            assert!(!is_file_time_same(), "file's time did not get updated");
    2982
    1
            set_preference("CheckRuleFiles", "None").unwrap();
    2983
    1
            assert!(is_file_time_same(), "file's time was wrongly updated (preference 'CheckRuleFiles' should have prevented updating)");
    2984
    2985
            // change a file, cause read_files to be called, and return if MathCAT noticed the change and updated its time
    2986
    2
            fn is_file_time_same() -> bool {
    2987
                // read and write a unicode file in a test dir
    2988
                // files are read in due to setting the MathML
    2989
    2990
                use std::time::Duration;
    2991
    2
                return SPEECH_RULES.with(|rules| {
    2992
    2
                    let start_main_file = rules.borrow().unicode_short_files.borrow().ft[0].clone();
    2993
    2994
                    // open the file, read all the contents, then write them back so the time changes
    2995
    2
                    let contents = std::fs::read(&start_main_file.file).expect(&format!("Failed to read file {} during test", &start_main_file.file.to_string_lossy()));
    2996
    2
                    std::fs::write(start_main_file.file, contents).unwrap();
    2997
    2
                    std::thread::sleep(Duration::from_millis(5));       // pause a little to make sure the time changes
    2998
    2999
                    // speak should cause the file stored to have a new time
    3000
    2
                    if let Err(e0) = get_spoken_text() {
    3001
    0
                        error!("{}", crate::errors_to_string(&e));
    3002
    0
                        panic!("Should not be an error in speech")
    3003
    2
                    }
    3004
    2
                    return rules.borrow().unicode_short_files.borrow().ft[0].time == start_main_file.time;
    3005
    2
                });
    3006
    2
            }
    3007
    1
        }
    3008
    }}
    3009
    3010
        // #[test]
    3011
        // fn test_nested_debug_quoted_paren() {
    3012
        //     let str = r#"DEBUG(*[2]/*[3][DEBUG(text()='(')])"#;
    3013
        //     let result = MyXPath::add_debug_string_arg(str);
    3014
        //     assert!(result.is_ok());
    3015
        //     assert_eq!(result.unwrap(), r#"DEBUG(*[2]/*[3][DEBUG(text()='(')], "DEBUG(*[2]/*[3][DEBUG(text()='(')], \"text()='(')]\")"#);
    3016
        // }
    3017
    3018
    }