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.17k
self.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.80k
self.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.18k
rules.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
}