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