Skip to main content

libmathcat/
speech.rs

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