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