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