1#![allow(clippy::needless_return)]
6use std::path::PathBuf;
7use std::collections::HashMap;
8use std::cell::{RefCell, RefMut};
9use std::sync::LazyLock;
10use std::fmt::Debug;
11use sxd_document_no_unsafe::dom::{ChildOfElement, Document, Element};
12use sxd_document_no_unsafe::{Package, QName};
13use sxd_document_no_unsafe::{as_str, as_qname};
14use sxd_xpath_no_unsafe::context::Evaluation;
15use sxd_xpath_no_unsafe::{Factory, Value, XPath};
16use sxd_xpath_no_unsafe::nodeset::Node;
17use std::fmt;
18use std::time::SystemTime;
19use crate::definitions::read_definitions_file;
20use crate::errors::*;
21use crate::prefs::*;
22use crate::xpath_functions::is_leaf;
23use yaml_rust::{YamlLoader, Yaml, yaml::Hash};
24use crate::tts::*;
25use crate::infer_intent::*;
26use crate::pretty_print::{mml_to_string, yaml_to_string};
27use std::path::Path;
28use std::rc::Rc;
29use crate::shim_filesystem::{read_to_string_shim, canonicalize_shim};
30use crate::canonicalize::{as_element, create_mathml_element, set_mathml_name, name, MATHML_FROM_NAME_ATTR};
31use regex::Regex;
32use log::{debug, error, info};
33
34
35pub const NAV_NODE_SPEECH_NOT_FOUND: &str = "NAV_NODE_NOT_FOUND";
36
37const NO_EVAL_QUOTE_CHAR: char = '\u{efff}'; const NO_EVAL_QUOTE_CHAR_AS_BYTES: [u8;3] = [0xee,0xbf,0xbf];
43const N_BYTES_NO_EVAL_QUOTE_CHAR: usize = NO_EVAL_QUOTE_CHAR.len_utf8();
44
45pub fn make_quoted_string(mut string: String) -> String {
47 string.push(NO_EVAL_QUOTE_CHAR);
48 return string;
49}
50
51pub fn is_quoted_string(str: &str) -> bool {
53 if str.len() < N_BYTES_NO_EVAL_QUOTE_CHAR {
54 return false;
55 }
56 let bytes = str.as_bytes();
57 return bytes[bytes.len()-N_BYTES_NO_EVAL_QUOTE_CHAR..] == NO_EVAL_QUOTE_CHAR_AS_BYTES;
58}
59
60pub fn unquote_string(str: &str) -> &str {
63 return &str[..str.len()-N_BYTES_NO_EVAL_QUOTE_CHAR];
64}
65
66
67pub fn intent_from_mathml<'m>(mathml: Element, doc: Document<'m>) -> Result<Element<'m>> {
78 let intent_tree = intent_rules(&INTENT_RULES, doc, mathml, "")?;
79 doc.root().append_child(intent_tree);
80 return Ok(intent_tree);
81}
82
83pub fn speak_mathml(mathml: Element, nav_node_id: &str, nav_node_offset: usize) -> Result<String> {
84 return speak_rules(&SPEECH_RULES, mathml, nav_node_id, nav_node_offset);
85}
86
87pub fn overview_mathml(mathml: Element, nav_node_id: &str, nav_node_offset: usize) -> Result<String> {
88 return speak_rules(&OVERVIEW_RULES, mathml, nav_node_id, nav_node_offset);
89}
90
91
92fn intent_rules<'m>(rules: &'static std::thread::LocalKey<RefCell<SpeechRules>>, doc: Document<'m>, mathml: Element, nav_node_id: &'m str) -> Result<Element<'m>> {
93 rules.with(|rules| {
94 rules.borrow_mut().read_files()?;
95 let rules = rules.borrow();
96 let should_set_literal_intent = rules.pref_manager.borrow().pref_to_string("SpeechStyle").as_str() == "LiteralSpeak";
98 let original_intent = mathml.attribute_value("intent");
99 if should_set_literal_intent {
100 if let Some(ref intent) = original_intent {
101 let intent = if intent.contains('(') {intent.replace('(', ":literal(")} else {intent.to_string() + ":literal"};
102 mathml.set_attribute_value("intent", &intent);
103 } else {
104 mathml.set_attribute_value("intent", ":literal");
105 };
106 }
107 let mut rules_with_context = SpeechRulesWithContext::new(&rules, doc, nav_node_id, 0);
108 let intent = rules_with_context.match_pattern::<Element<'m>>(mathml)
109 .context("Pattern match/replacement failure!")?;
110 let answer = if name(intent) == "TEMP_NAME" { assert_eq!(intent.children().len(), 1);
112 as_element(intent.children()[0])
113 } else {
114 intent
115 };
116 if should_set_literal_intent {
117 if let Some(original_intent) = original_intent {
118 mathml.set_attribute_value("intent", as_str!(original_intent));
119 } else {
120 mathml.remove_attribute("intent");
121 }
122 }
123 return Ok(answer);
124 })
125}
126
127fn speak_rules(rules: &'static std::thread::LocalKey<RefCell<SpeechRules>>, mathml: Element, nav_node_id: &str, nav_node_offset: usize) -> Result<String> {
130 return rules.with(|rules| {
131 rules.borrow_mut().read_files()?;
132 let rules = rules.borrow();
133 let new_package = Package::new();
135 let mut rules_with_context = SpeechRulesWithContext::new(&rules, new_package.as_document(), nav_node_id, nav_node_offset);
136 let speech_string = nestable_speak_rules(& mut rules_with_context, mathml)?;
137
138 return Ok( rules.pref_manager.borrow().get_tts()
139 .merge_pauses(remove_optional_indicators(
140 &speech_string.replace(CONCAT_STRING, "")
141 .replace(CONCAT_INDICATOR, "")
142 .replace(POSTFIX_CONCAT_STRING, "")
143 .replace(POSTFIX_CONCAT_INDICATOR, "")
144 )
145 .trim_start().trim_end_matches([' ', ',', ';'])) );
146 });
147
148 fn nestable_speak_rules<'c, 's:'c, 'm:'c>(rules_with_context: &mut SpeechRulesWithContext<'c, 's, 'm>, mathml: Element<'c>) -> Result<String> {
149 let mut speech_string = rules_with_context.match_pattern::<String>(mathml)
150 .context("Pattern match/replacement failure!")?;
151 if !rules_with_context.nav_node_id.is_empty() {
154 let raw_intent_attr = mathml.attribute_value("data-intent-property");
156 let intent_attr = raw_intent_attr.as_deref().unwrap_or_default();
157 if let Some(start) = speech_string.find("[[") {
158 match speech_string[start+2..].find("]]") {
159 None => bail!("Internal error: looking for '[[...]]' during navigation -- only found '[[' in '{}'", speech_string),
160 Some(end) => speech_string = speech_string[start+2..start+2+end].to_string(),
161 }
162 } else if !intent_attr.contains(":literal:") {
163 mathml.set_attribute_value("data-intent-property", (":literal:".to_string() + intent_attr).as_str());
165 let speech = nestable_speak_rules(rules_with_context, mathml);
166 mathml.set_attribute_value("data-intent-property", intent_attr);
167 return speech;
168 } else {
169 bail!(NAV_NODE_SPEECH_NOT_FOUND); }
171 }
172 return Ok(speech_string);
173 }
174}
175
176pub fn yaml_to_type(yaml: &Yaml) -> String {
178 return match yaml {
179 Yaml::Real(v)=> format!("real='{v:#}'"),
180 Yaml::Integer(v)=> format!("integer='{v:#}'"),
181 Yaml::String(v)=> format!("string='{v:#}'"),
182 Yaml::Boolean(v)=> format!("boolean='{v:#}'"),
183 Yaml::Array(v)=> match v.len() {
184 0 => "array with no entries".to_string(),
185 1 => format!("array with the entry: {}", yaml_to_type(&v[0])),
186 _ => format!("array with {} entries. First entry: {}", v.len(), yaml_to_type(&v[0])),
187 }
188 Yaml::Hash(h)=> {
189 let first_pair =
190 if h.is_empty() {
191 "no pairs".to_string()
192 } else {
193 let (key, val) = h.iter().next().unwrap();
194 format!("({}, {})", yaml_to_type(key), yaml_to_type(val))
195 };
196 format!("dictionary with {} pair{}. A pair: {}", h.len(), if h.len()==1 {""} else {"s"}, first_pair)
197 }
198 Yaml::Alias(_)=> "Alias".to_string(),
199 Yaml::Null=> "Null".to_string(),
200 Yaml::BadValue=> "BadValue".to_string(),
201 }
202}
203
204fn yaml_type_err(yaml: &Yaml, str: &str) -> Error {
205 anyhow!("Expected {}, found {}", str, yaml_to_type(yaml))
206}
207
208fn find_str<'a>(dict: &'a Yaml, key: &'a str) -> Option<&'a str> {
221 return dict[key].as_str();
222}
223
224pub fn as_hash_checked(value: &Yaml) -> Result<&Hash> {
226 let result = value.as_hash();
227 let result = result.ok_or_else(|| yaml_type_err(value, "hashmap"))?;
228 return Ok( result );
229}
230
231pub fn as_vec_checked(value: &Yaml) -> Result<&Vec<Yaml>> {
233 let result = value.as_vec();
234 let result = result.ok_or_else(|| yaml_type_err(value, "array"))?;
235 return Ok( result );
236}
237
238pub fn as_str_checked(yaml: &Yaml) -> Result<&str> {
240 return yaml.as_str().ok_or_else(|| yaml_type_err(yaml, "string"));
241}
242
243
244pub const CONCAT_INDICATOR: &str = "\u{F8FE}";
248
249pub const CONCAT_STRING: &str = " \u{F8FE}";
251
252pub const POSTFIX_CONCAT_INDICATOR: &str = "\u{F8FF}";
254
255pub const POSTFIX_CONCAT_STRING: &str = "\u{F8FF} ";
257
258const OPTIONAL_INDICATOR: &str = "\u{F8FD}";
261const OPTIONAL_INDICATOR_LEN: usize = OPTIONAL_INDICATOR.len();
262
263pub fn remove_optional_indicators(str: &str) -> String {
264 return str.replace(OPTIONAL_INDICATOR, "");
265}
266
267pub fn compile_rule<F>(str: &str, mut build_fn: F) -> Result<Vec<PathBuf>> where
271 F: FnMut(&Yaml) -> Result<Vec<PathBuf>> {
272 let docs = YamlLoader::load_from_str(str);
273 match docs {
274 Err(e) => {
275 bail!("Parse error!!: {}", e);
276 },
277 Ok(docs) => {
278 if docs.len() != 1 {
279 bail!("Didn't find rules!");
280 }
281 return build_fn(&docs[0]);
282 }
283 }
284}
285
286pub fn process_include<F>(current_file: &Path, new_file_name: &str, mut read_new_file: F) -> Result<Vec<PathBuf>>
287 where F: FnMut(&Path) -> Result<Vec<PathBuf>> {
288 let parent_path = current_file.parent();
289 if parent_path.is_none() {
290 bail!("Internal error: {:?} is not a valid file name", current_file);
291 }
292 let mut new_file = match canonicalize_shim(parent_path.unwrap()) {
293 Ok(path) => path,
294 Err(e) => bail!("process_include: canonicalize failed for {} with message {}", parent_path.unwrap().display(), e),
295 };
296
297 for unzip_dir in new_file.ancestors() {
299 if unzip_dir.ends_with("Rules") {
300 break; }
302 if unzip_dir.ends_with("Languages") || unzip_dir.ends_with("Braille") {
303 if let Some(subdir) = new_file.strip_prefix(unzip_dir).unwrap().iter().next() {
306 let default_lang = if unzip_dir.ends_with("Languages") {"en"} else {"UEB;"};
307 PreferenceManager::unzip_files(unzip_dir, subdir.to_str().unwrap(), Some(default_lang)).unwrap_or_default();
308 }
309 }
310 }
311 new_file.push(new_file_name);
312 info!("...processing include: {new_file_name}...");
313 let new_file = match crate::shim_filesystem::canonicalize_shim(new_file.as_path()) {
314 Ok(buf) => buf,
315 Err(msg) => bail!("-include: constructed file name '{}' causes error '{}'",
316 new_file.to_str().unwrap(), msg),
317 };
318
319 let mut included_files = read_new_file(new_file.as_path())?;
320 let mut files_read = vec![new_file];
321 files_read.append(&mut included_files);
322 return Ok(files_read);
323}
324
325pub trait TreeOrString<'c, 'm:'c, T: Debug> : Debug {
328 fn from_element(e: Element<'m>) -> Result<T>;
329 fn from_string(s: String, doc: Document<'m>) -> Result<T>;
330 fn replace_tts<'s:'c, 'r>(tts: &TTS, command: &TTSCommandRule, prefs: &PreferenceManager, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T>;
331 fn replace<'s:'c, 'r>(ra: &ReplacementArray, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T>;
332 fn replace_nodes<'s:'c, 'r>(rules: &'r mut SpeechRulesWithContext<'c, 's,'m>, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<T>;
333 fn highlight_braille(braille: T, highlight_style: String) -> T;
334 fn mark_nav_speech(speech: T) -> T;
335 fn sanitize_xpath_string(s: String, _rules_with_context: &SpeechRulesWithContext<'c, '_, 'm>) -> String {
337 return s;
338 }
339}
340
341impl<'c, 'm:'c> TreeOrString<'c, 'm, String> for String {
342 fn from_element(_e: Element<'m>) -> Result<String> {
343 bail!("from_element not allowed for strings");
344 }
345
346 fn from_string(s: String, _doc: Document<'m>) -> Result<String> {
347 return Ok(s);
348 }
349
350 fn replace_tts<'s:'c, 'r>(tts: &TTS, command: &TTSCommandRule, prefs: &PreferenceManager, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<String> {
351 return tts.replace_string(command, prefs, rules_with_context, mathml);
352 }
353
354 fn replace<'s:'c, 'r>(ra: &ReplacementArray, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<String> {
355 return ra.replace_array_string(rules_with_context, mathml);
356 }
357
358 fn replace_nodes<'s:'c, 'r>(rules: &'r mut SpeechRulesWithContext<'c, 's,'m>, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<String> {
359 return rules.replace_nodes_string(nodes, mathml);
360 }
361
362 fn highlight_braille(braille: String, highlight_style: String) -> String {
363 return SpeechRulesWithContext::highlight_braille_string(braille, highlight_style);
364 }
365
366 fn mark_nav_speech(speech: String) -> String {
367 return SpeechRulesWithContext::mark_nav_speech(speech);
368 }
369
370 }
372
373impl<'c, 'm:'c> TreeOrString<'c, 'm, Element<'m>> for Element<'m> {
374 fn from_element(e: Element<'m>) -> Result<Element<'m>> {
375 return Ok(e);
376 }
377
378 fn from_string(s: String, doc: Document<'m>) -> Result<Element<'m>> {
379 let leaf = create_mathml_element(&doc, "mi");
381 leaf.set_text(&s);
382 return Ok(leaf);
383}
384
385 fn replace_tts<'s:'c, 'r>(_tts: &TTS, _command: &TTSCommandRule, _prefs: &PreferenceManager, _rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, _mathml: Element<'c>) -> Result<Element<'m>> {
386 bail!("Internal error: applying a TTS rule to a tree");
387 }
388
389 fn replace<'s:'c, 'r>(ra: &ReplacementArray, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<Element<'m>> {
390 return ra.replace_array_tree(rules_with_context, mathml);
391 }
392
393 fn replace_nodes<'s:'c, 'r>(rules: &'r mut SpeechRulesWithContext<'c, 's,'m>, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<Element<'m>> {
394 return rules.replace_nodes_tree(nodes, mathml);
395 }
396
397 fn highlight_braille(_braille: Element<'c>, _highlight_style: String) -> Element<'m> {
398 panic!("Internal error: highlight_braille called on a tree");
399 }
400
401 fn mark_nav_speech(_speech: Element<'c>) -> Element<'m> {
402 panic!("Internal error: mark_nav_speech called on a tree");
403 }
404}
405
406#[derive(Debug, Clone)]
409#[allow(clippy::upper_case_acronyms)]
410enum Replacement {
411 Text(String),
413 XPath(MyXPath),
414 Intent(Box<Intent>),
415 Test(Box<TestArray>),
416 TTS(Box<TTSCommandRule>),
417 With(Box<With>),
418 SetVariables(Box<SetVariables>),
419 Insert(Box<InsertChildren>),
420 Translate(TranslateExpression),
421}
422
423impl fmt::Display for Replacement {
424 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
425 return write!(f, "{}",
426 match self {
427 Replacement::Test(c) => c.to_string(),
428 Replacement::Text(t) => format!("t: \"{t}\""),
429 Replacement::XPath(x) => x.to_string(),
430 Replacement::Intent(i) => i.to_string(),
431 Replacement::TTS(t) => t.to_string(),
432 Replacement::With(w) => w.to_string(),
433 Replacement::SetVariables(v) => v.to_string(),
434 Replacement::Insert(ic) => ic.to_string(),
435 Replacement::Translate(x) => x.to_string(),
436 }
437 );
438 }
439}
440
441impl Replacement {
442 fn build(replacement: &Yaml) -> Result<Replacement> {
443 let dictionary = replacement.as_hash();
445 if dictionary.is_none() {
446 bail!(" expected a key/value pair. Found {}.", yaml_to_string(replacement, 0));
447 };
448 let dictionary = dictionary.unwrap();
449 if dictionary.is_empty() {
450 bail!("No key/value pairs found for key 'replace'.\n\
451 Suggestion: are the following lines indented properly?");
452 }
453 if dictionary.len() > 1 {
454 bail!("Should only be one key/value pair for the replacement.\n \
455 Suggestion: are the following lines indented properly?\n \
456 The key/value pairs found are\n{}", yaml_to_string(replacement, 2));
457 }
458
459 let (key, value) = dictionary.iter().next().unwrap();
461 let key = key.as_str().ok_or_else(|| anyhow!("replacement key(e.g, 't') is not a string"))?;
462 match key {
463 "t" | "T" => {
464 return Ok( Replacement::Text( as_str_checked(value)?.to_string() ) );
465 },
466 "ct" | "CT" => {
467 return Ok( Replacement::Text( CONCAT_INDICATOR.to_string() + as_str_checked(value)? ) );
468 },
469 "tc" | "TC" => {
470 return Ok( Replacement::Text( as_str_checked(value)?.to_string() + POSTFIX_CONCAT_INDICATOR ) );
471 },
472 "ot" | "OT" => {
473 return Ok( Replacement::Text( OPTIONAL_INDICATOR.to_string() + as_str_checked(value)? + OPTIONAL_INDICATOR ) );
474 },
475 "x" => {
476 return Ok( Replacement::XPath( MyXPath::build(value)
477 .context("while trying to evaluate value of 'x:'")? ) );
478 },
479 "pause" | "rate" | "pitch" | "volume" | "audio" | "gender" | "voice" | "spell" | "SPELL" | "bookmark" | "pronounce" | "PRONOUNCE" => {
480 return Ok( Replacement::TTS( TTS::build(&key.to_ascii_lowercase(), value)? ) );
481 },
482 "intent" => {
483 return Ok( Replacement::Intent( Intent::build(value)? ) );
484 },
485 "test" => {
486 return Ok( Replacement::Test( Box::new( TestArray::build(value)? ) ) );
487 },
488 "with" => {
489 return Ok( Replacement::With( With::build(value)? ) );
490 },
491 "set_variables" => {
492 return Ok( Replacement::SetVariables( SetVariables::build(value)? ) );
493 },
494 "insert" => {
495 return Ok( Replacement::Insert( InsertChildren::build(value)? ) );
496 },
497 "translate" => {
498 return Ok( Replacement::Translate( TranslateExpression::build(value)
499 .context("while trying to evaluate value of 'speak:'")? ) );
500 },
501 _ => {
502 bail!("Unknown 'replace' command ({}) with value: {}", key, yaml_to_string(value, 0));
503 }
504 }
505 }
506}
507
508#[derive(Debug, Clone)]
511struct InsertChildren {
512 xpath: MyXPath, replacements: ReplacementArray, }
515
516#[cfg_attr(coverage, coverage(off))]
517impl fmt::Display for InsertChildren {
518 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
519 return write!(f, "InsertChildren:\n nodes {}\n replacements {}", self.xpath, self.replacements);
520 }
521}
522
523
524impl InsertChildren {
525 fn build(insert: &Yaml) -> Result<Box<InsertChildren>> {
526 if insert.as_hash().is_none() {
528 bail!("")
529 }
530 let nodes = &insert["nodes"];
531 if nodes.is_badvalue() {
532 bail!("Missing 'nodes' as part of 'insert'.\n \
533 Suggestion: add 'nodes:' or if present, indent so it is contained in 'insert'");
534 }
535 let nodes = as_str_checked(nodes)?;
536 let replace = &insert["replace"];
537 if replace.is_badvalue() {
538 bail!("Missing 'replace' as part of 'insert'.\n \
539 Suggestion: add 'replace:' or if present, indent so it is contained in 'insert'");
540 }
541 return Ok( Box::new( InsertChildren {
542 xpath: MyXPath::new(nodes.to_string())?,
543 replacements: ReplacementArray::build(replace).context("'replace:'")?,
544 } ) );
545 }
546
547 fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
556 let result = self.xpath.evaluate(&rules_with_context.context_stack.base, mathml)
557 .with_context(||format!("in '{}' replacing after pattern match", self.xpath.rc.string) )?;
558 match result {
559 Value::Nodeset(nodes) => {
560 if nodes.size() == 0 {
561 bail!("During replacement, no matching element found");
562 };
563 let nodes = nodes.document_order();
564 let n_nodes = nodes.len();
565 let mut expanded_result = Vec::with_capacity(n_nodes + (n_nodes+1)*self.replacements.replacements.len());
566 expanded_result.push(
567 Replacement::XPath(
568 MyXPath::new(format!("{}[{}]", self.xpath.rc.string , 1))?
569 )
570 );
571 for i in 2..n_nodes+1 {
572 expanded_result.extend_from_slice(&self.replacements.replacements);
573 expanded_result.push(
574 Replacement::XPath(
575 MyXPath::new(format!("{}[{}]", self.xpath.rc.string , i))?
576 )
577 );
578 }
579 let replacements = ReplacementArray{ replacements: expanded_result };
580 return replacements.replace(rules_with_context, mathml);
581 },
582
583 Value::String(t) => { return T::from_string(rules_with_context.replace_chars(&t, mathml)?, rules_with_context.doc); },
585 Value::Number(num) => { return T::from_string( num.to_string(), rules_with_context.doc ); },
586 Value::Boolean(b) => { return T::from_string( b.to_string(), rules_with_context.doc ); }, }
588
589 }
590}
591
592
593static ATTR_NAME_VALUE: LazyLock<Regex> = LazyLock::new(|| {
594 Regex::new(
595 r#"(?P<name>[^\s\u{0}-\u{40}\[\\\]^`\u{7B}-\u{BF}][^\s\u{0}-\u{2C}/:;<=>?@\[\\\]^`\u{7B}-\u{BF}]*)\s*=\s*('(?P<value>[^']+)'|"(?P<dqvalue>[^"]+)")"#
598 ).unwrap()
599});
600
601#[derive(Debug, Clone)]
604struct Intent {
605 name: Option<String>, xpath: Option<MyXPath>, attrs: String, children: ReplacementArray, }
610
611impl fmt::Display for Intent {
612 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
613 let name = if let Some(name) = &self.name {
614 name.to_string()
615 } else {
616 self.xpath.as_ref().unwrap().to_string()
617 };
618 return write!(f, "intent: {}: {}, attrs='{}'>\n children: {}",
619 if self.name.is_some() {"name"} else {"xpath-name"}, name,
620 self.attrs,
621 self.children);
622 }
623}
624
625impl Intent {
626 fn build(yaml_dict: &Yaml) -> Result<Box<Intent>> {
627 if yaml_dict.as_hash().is_none() {
629 bail!("Array found for contents of 'intent' -- should be dictionary with keys 'name' and 'children'")
630 }
631 let name = &yaml_dict["name"];
632 let xpath_name = &yaml_dict["xpath-name"];
633 if name.is_badvalue() && xpath_name.is_badvalue(){
634 bail!("Missing 'name' or 'xpath-name' as part of 'intent'.\n \
635 Suggestion: add 'name:' or if present, indent so it is contained in 'intent'");
636 }
637 let attrs = &yaml_dict["attrs"];
638 let replace = &yaml_dict["children"];
639 if replace.is_badvalue() {
640 bail!("Missing 'children' as part of 'intent'.\n \
641 Suggestion: add 'children:' or if present, indent so it is contained in 'intent'");
642 }
643 return Ok( Box::new( Intent {
644 name: if name.is_badvalue() {None} else {Some(as_str_checked(name).context("'name'")?.to_string())},
645 xpath: if xpath_name.is_badvalue() {None} else {Some(MyXPath::build(xpath_name).context("'intent'")?)},
646 attrs: if attrs.is_badvalue() {"".to_string()} else {as_str_checked(attrs).context("'attrs'")?.to_string()},
647 children: ReplacementArray::build(replace).context("'children:'")?,
648 } ) );
649 }
650
651 fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
652 let result = self.children.replace::<Element<'m>>(rules_with_context, mathml)
653 .context("replacing inside 'intent'")?;
654 let mut result = lift_children(result);
655 if name(result) != "TEMP_NAME" && name(result) != "Unknown" {
656 let temp = create_mathml_element(&result.document(), "TEMP_NAME");
658 temp.append_child(result);
659 result = temp;
660 }
661 if let Some(intent_name) = &self.name {
662 result.set_attribute_value(MATHML_FROM_NAME_ATTR, as_str!(name(mathml)));
663 set_mathml_name(result, intent_name.as_str());
664 }
665 if let Some(my_xpath) = &self.xpath{ let xpath_value = my_xpath.evaluate(rules_with_context.get_context(), mathml)?;
667 match xpath_value {
668 Value::String(intent_name) => {
669 result.set_attribute_value(MATHML_FROM_NAME_ATTR, as_str!(name(mathml)));
670 set_mathml_name(result, intent_name.as_str())
671 },
672 _ => bail!("'xpath-name' value '{}' was not a string", my_xpath),
673 }
674 }
675 if self.name.is_none() && self.xpath.is_none() {
676 bail!("Intent::replace: internal error -- neither 'name' nor 'xpath' is set");
677 };
678
679 for attr in mathml.attributes() {
680 result.set_attribute_value(as_qname!(attr.name()), as_str!(attr.value()));
681 }
682
683 if mathml.parent().is_some() && mathml.parent().unwrap().element().is_some() &&
685 result.attribute_value("id") == crate::canonicalize::get_parent(mathml).attribute_value("id") {
686 result.remove_attribute("id");
688 }
689
690 if !self.attrs.is_empty() {
691 for cap in ATTR_NAME_VALUE.captures_iter(&self.attrs) {
695 let matched_value = if cap["value"].is_empty() {&cap["dqvalue"]} else {&cap["value"]};
696 let value_as_xpath = MyXPath::new(matched_value.to_string()).context("attr value inside 'intent'")?;
697 let value = value_as_xpath.evaluate(rules_with_context.get_context(), result)
698 .context("attr xpath evaluation value inside 'intent'")?;
699 let mut value = value.into_string();
700 if &cap["name"] == INTENT_PROPERTY {
701 value = simplify_fixity_properties(&value);
702 }
703 if &cap["name"] == INTENT_PROPERTY && value == ":" {
705 result.remove_attribute(INTENT_PROPERTY);
707 } else {
708 result.set_attribute_value(&cap["name"], &value);
709 }
710 };
711 }
712
713 return T::from_element(result);
715
716
717 fn lift_children(result: Element) -> Element {
719 let mut new_children = Vec::with_capacity(2*result.children().len());
722 for child_of_element in result.children() {
723 match child_of_element {
724 ChildOfElement::Element(child) => {
725 if name(child) == "TEMP_NAME" {
726 new_children.append(&mut child.children()); } else {
728 new_children.push(child_of_element);
729 }
730 },
731 _ => new_children.push(child_of_element), }
733 }
734 result.replace_children(new_children);
735 return result;
736 }
737 }
738}
739
740#[derive(Debug, Clone)]
743struct With {
744 variables: VariableDefinitions, replacements: ReplacementArray, }
747
748#[cfg_attr(coverage, coverage(off))]
749impl fmt::Display for With {
750 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
751 return write!(f, "with:\n variables: {}\n replace: {}", self.variables, self.replacements);
752 }
753}
754
755
756impl With {
757 fn build(vars_replacements: &Yaml) -> Result<Box<With>> {
758 if vars_replacements.as_hash().is_none() {
760 bail!("Array found for contents of 'with' -- should be dictionary with keys 'variables' and 'replace'")
761 }
762 let var_defs = &vars_replacements["variables"];
763 if var_defs.is_badvalue() {
764 bail!("Missing 'variables' as part of 'with'.\n \
765 Suggestion: add 'variables:' or if present, indent so it is contained in 'with'");
766 }
767 let replace = &vars_replacements["replace"];
768 if replace.is_badvalue() {
769 bail!("Missing 'replace' as part of 'with'.\n \
770 Suggestion: add 'replace:' or if present, indent so it is contained in 'with'");
771 }
772 return Ok( Box::new( With {
773 variables: VariableDefinitions::build(var_defs).context("'variables'")?,
774 replacements: ReplacementArray::build(replace).context("'replace:'")?,
775 } ) );
776 }
777
778 fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
779 rules_with_context.context_stack.push(self.variables.clone(), mathml)?;
780 let result = self.replacements.replace(rules_with_context, mathml)
781 .context("replacing inside 'with'")?;
782 rules_with_context.context_stack.pop();
783 return Ok( result );
784 }
785}
786
787#[derive(Debug, Clone)]
790struct SetVariables {
791 variables: VariableDefinitions, }
793
794#[cfg_attr(coverage, coverage(off))]
795impl fmt::Display for SetVariables {
796 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
797 return write!(f, "SetVariables: variables {}", self.variables);
798 }
799}
800
801
802impl SetVariables {
803 fn build(vars: &Yaml) -> Result<Box<SetVariables>> {
804 if vars.as_vec().is_none() {
806 bail!("'set_variables' -- should be an array of variable name, xpath value");
807 }
808 return Ok( Box::new( SetVariables {
809 variables: VariableDefinitions::build(vars).context("'set_variables'")?
810 } ) );
811 }
812
813 fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
814 rules_with_context.context_stack.set_globals(self.variables.clone(), mathml)?;
815 return T::from_string( "".to_string(), rules_with_context.doc );
816 }
817}
818
819
820#[derive(Debug, Clone)]
822struct TranslateExpression {
823 xpath: MyXPath, }
825
826#[cfg_attr(coverage, coverage(off))]
827impl fmt::Display for TranslateExpression {
828 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
829 return write!(f, "speak: {}", self.xpath);
830 }
831}
832
833
834impl TranslateExpression {
835 fn build(vars: &Yaml) -> Result<TranslateExpression> {
836 return Ok( TranslateExpression { xpath: MyXPath::build(vars).context("'translate'")? } );
838 }
839
840 fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
841 if self.xpath.rc.string.starts_with('@') {
842 let xpath_value = self.xpath.evaluate(rules_with_context.get_context(), mathml)?;
843 let id = match xpath_value {
844 Value::String(s) => Some(s),
845 Value::Nodeset(nodes) => {
846 if nodes.size() == 1 {
847 nodes.document_order_first().unwrap().attribute().map(|attr| attr.value().to_string())
848 } else {
849 None
850 }
851 },
852 _ => None,
853 };
854 match id {
855 None => bail!("'translate' value '{}' is not a string or an attribute value (correct by using '@id'??):\n", self.xpath),
856 Some(id) => {
857 let speech = speak_mathml(mathml, &id, 0)?;
858 return T::from_string(speech, rules_with_context.doc);
859 }
860 }
861 } else {
862 return T::from_string(
863 self.xpath.replace(rules_with_context, mathml).context("'translate'")?,
864 rules_with_context.doc
865 );
866 }
867 }
868}
869
870
871#[derive(Debug, Clone)]
873pub struct ReplacementArray {
874 replacements: Vec<Replacement>
875}
876
877impl fmt::Display for ReplacementArray {
878 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
879 return write!(f, "{}", self.pretty_print_replacements());
880 }
881}
882
883impl ReplacementArray {
884 pub fn build_empty() -> ReplacementArray {
886 return ReplacementArray {
887 replacements: vec![]
888 }
889 }
890
891 pub fn build(replacements: &Yaml) -> Result<ReplacementArray> {
894 let result= if replacements.is_array() {
896 let replacements = replacements.as_vec().unwrap();
897 replacements
898 .iter()
899 .enumerate() .map(|(i, r)| Replacement::build(r)
901 .with_context(|| format!("replacement #{} of {}", i+1, replacements.len())))
902 .collect::<Result<Vec<Replacement>>>()?
903 } else {
904 vec![ Replacement::build(replacements)?]
905 };
906
907 return Ok( ReplacementArray{ replacements: result } );
908 }
909
910 pub fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
912 return T::replace(self, rules_with_context, mathml);
913 }
914
915 pub fn replace_array_string<'c, 's:'c, 'm:'c>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<String> {
916 let mut replacement_strings = Vec::with_capacity(self.replacements.len()); for replacement in self.replacements.iter() {
922 let string: String = rules_with_context.replace(replacement, mathml)?;
923 if !string.is_empty() {
924 replacement_strings.push(string);
925 }
926 }
927
928 if replacement_strings.is_empty() {
929 return Ok( "".to_string() );
930 }
931 for i in 1..replacement_strings.len()-1 {
939 if let Some(bytes) = is_repetitive(&replacement_strings[i-1], &replacement_strings[i]) {
940 replacement_strings[i] = bytes.to_string();
941 }
942 }
943
944 for i in 0..replacement_strings.len() {
945 if replacement_strings[i].contains(PAUSE_AUTO_STR) {
946 let before = if i == 0 {""} else {&replacement_strings[i-1]};
947 let after = if i+1 == replacement_strings.len() {""} else {&replacement_strings[i+1]};
948 replacement_strings[i] = replacement_strings[i].replace(
949 PAUSE_AUTO_STR,
950 &rules_with_context.speech_rules.pref_manager.borrow().get_tts().compute_auto_pause(&rules_with_context.speech_rules.pref_manager.borrow(), before, after)?);
951 }
952 }
953
954 return Ok( replacement_strings.join(" ") );
957
958 fn is_repetitive<'a>(prev: &str, next: &'a str) -> Option<&'a str> {
962 if next.len() <= 2 * OPTIONAL_INDICATOR_LEN {
966 return None;
967 }
968
969 let i_start = next.find(OPTIONAL_INDICATOR)?;
971 let start_repeat_word_in_next = &next[i_start + OPTIONAL_INDICATOR_LEN..];
972 let i_end = start_repeat_word_in_next.find(OPTIONAL_INDICATOR)
973 .unwrap_or_else(|| panic!("Internal error: missing end optional char -- text handling is corrupted!"));
974 let repeat_word = &start_repeat_word_in_next[..i_end];
975 let prev_trimmed = prev.trim_end();
979 let ends_with_word = prev_trimmed.len() > repeat_word.len() && prev_trimmed.ends_with(repeat_word);
980 let ends_with_wrapped_word =
981 prev_trimmed
982 .strip_suffix(OPTIONAL_INDICATOR)
983 .and_then(|s| s.strip_suffix(repeat_word))
984 .and_then(|s| s.strip_suffix(OPTIONAL_INDICATOR))
985 .is_some();
986 if ends_with_word || ends_with_wrapped_word {
987 Some(start_repeat_word_in_next[i_end + OPTIONAL_INDICATOR_LEN..].trim_start()) } else {
990 None
991 }
992 }
993 }
994
995 pub fn replace_array_tree<'c, 's:'c, 'm:'c>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<Element<'m>> {
996 if self.replacements.len() == 1 {
998 return rules_with_context.replace::<Element<'m>>(&self.replacements[0], mathml);
999 }
1000
1001 let new_element = create_mathml_element(&rules_with_context.doc, "Unknown"); let mut new_children = Vec::with_capacity(self.replacements.len());
1003 for child in self.replacements.iter() {
1004 let child = rules_with_context.replace::<Element<'m>>(child, mathml)?;
1005 new_children.push(ChildOfElement::Element(child));
1006 };
1007 new_element.append_children(new_children);
1008 return Ok(new_element);
1009 }
1010
1011
1012 pub fn is_empty(&self) -> bool {
1014 return self.replacements.is_empty();
1015 }
1016
1017 fn pretty_print_replacements(&self) -> String {
1018 let mut group_string = String::with_capacity(128);
1019 if self.replacements.len() == 1 {
1020 group_string += &format!("[{}]", self.replacements[0]);
1021 } else {
1022 group_string += &self.replacements.iter()
1023 .map(|replacement| format!("\n - {replacement}"))
1024 .collect::<Vec<String>>()
1025 .join("");
1026 group_string += "\n";
1027 }
1028 return group_string;
1029 }
1030}
1031
1032
1033
1034#[derive(Debug)]
1038struct RCMyXPath {
1039 xpath: XPath,
1040 string: String, }
1042
1043#[derive(Debug, Clone)]
1044pub struct MyXPath {
1045 rc: Rc<RCMyXPath> }
1047
1048
1049impl fmt::Display for MyXPath {
1050 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1051 return write!(f, "\"{}\"", self.rc.string);
1052 }
1053}
1054
1055thread_local!{
1059 static XPATH_CACHE: RefCell<HashMap<String, MyXPath>> = RefCell::new( HashMap::with_capacity(2047) );
1060}
1061impl MyXPath {
1064 fn new(xpath: String) -> Result<MyXPath> {
1065 return XPATH_CACHE.with( |cache| {
1066 let mut cache = cache.borrow_mut();
1067 return Ok(
1068 match cache.get(&xpath) {
1069 Some(compiled_xpath) => {
1070 compiled_xpath.clone()
1072 },
1073 None => {
1074 let new_xpath = MyXPath {
1075 rc: Rc::new( RCMyXPath {
1076 xpath: MyXPath::compile_xpath(&xpath)?,
1077 string: xpath.clone()
1078 })};
1079 cache.insert(xpath.clone(), new_xpath.clone());
1080 new_xpath
1081 },
1082 }
1083 )
1084 });
1085 }
1086
1087 pub fn build(xpath: &Yaml) -> Result<MyXPath> {
1088 let xpath = match xpath {
1089 Yaml::String(s) => s.to_string(),
1090 Yaml::Integer(i) => i.to_string(),
1091 Yaml::Real(s) => s.to_string(),
1092 Yaml::Boolean(s) => s.to_string(),
1093 Yaml::Array(v) =>
1094 v.iter()
1096 .map(as_str_checked)
1097 .collect::<Result<Vec<&str>>>()?
1098 .join(" "),
1099 _ => bail!("Bad value when trying to create an xpath: {}", yaml_to_string(xpath, 1)),
1100 };
1101 return MyXPath::new(xpath);
1102 }
1103
1104 fn compile_xpath(xpath: &str) -> Result<XPath> {
1105 let factory = Factory::new();
1106 let xpath_with_debug_info = MyXPath::add_debug_string_arg(xpath)?;
1107 let compiled_xpath = factory.build(&xpath_with_debug_info)
1108 .with_context(|| format!(
1109 "Could not compile XPath for pattern:\n{}{}",
1110 xpath, more_details(xpath)))?;
1111 return Ok(compiled_xpath);
1112
1113
1114 fn more_details(xpath: &str) -> String {
1115 if xpath.is_empty() {
1117 return "xpath is empty string".to_string();
1118 }
1119 let as_bytes = xpath.trim().as_bytes();
1120 if as_bytes[0] == b'\'' && as_bytes[as_bytes.len()-1] != b'\'' {
1121 return "\nmissing \"'\"".to_string();
1122 }
1123 if (as_bytes[0] == b'"' && as_bytes[as_bytes.len()-1] != b'"') ||
1124 (as_bytes[0] != b'"' && as_bytes[as_bytes.len()-1] == b'"'){
1125 return "\nmissing '\"'".to_string();
1126 }
1127
1128 let mut i_bytes = 0; let mut paren_count = 0; let mut i_paren = 0; let mut bracket_count = 0;
1132 let mut i_bracket = 0;
1133 for ch in xpath.chars() {
1134 if ch == '(' {
1135 if paren_count == 0 {
1136 i_paren = i_bytes;
1137 }
1138 paren_count += 1;
1139 } else if ch == '[' {
1140 if bracket_count == 0 {
1141 i_bracket = i_bytes;
1142 }
1143 bracket_count += 1;
1144 } else if ch == ')' {
1145 if paren_count == 0 {
1146 return format!("\nExtra ')' found after '{}'", &xpath[i_paren..i_bytes]);
1147 }
1148 paren_count -= 1;
1149 if paren_count == 0 && bracket_count > 0 && i_bracket > i_paren {
1150 return format!("\nUnclosed brackets found at '{}'", &xpath[i_paren..i_bytes]);
1151 }
1152 } else if ch == ']' {
1153 if bracket_count == 0 {
1154 return format!("\nExtra ']' found after '{}'", &xpath[i_bracket..i_bytes]);
1155 }
1156 bracket_count -= 1;
1157 if bracket_count == 0 && paren_count > 0 && i_paren > i_bracket {
1158 return format!("\nUnclosed parens found at '{}'", &xpath[i_bracket..i_bytes]);
1159 }
1160 }
1161 i_bytes += ch.len_utf8();
1162 }
1163 return "".to_string();
1164 }
1165 }
1166
1167 fn add_debug_string_arg(xpath: &str) -> Result<String> {
1169 let debug_start = xpath.find("DEBUG(");
1171 if debug_start.is_none() {
1172 return Ok( xpath.to_string() );
1173 }
1174
1175 let debug_start = debug_start.unwrap();
1176 let mut before_paren = xpath[..debug_start+5].to_string(); let chars = xpath[debug_start+5..].chars().collect::<Vec<char>>(); before_paren.push_str(&chars_add_debug_string_arg(&chars).with_context(|| format!("In xpath='{xpath}'"))?);
1179 return Ok(before_paren);
1181
1182 fn chars_add_debug_string_arg(chars: &[char]) -> Result<String> {
1183 assert_eq!(chars[0], '(', "{} does not start with ')'", chars.iter().collect::<String>());
1188 let mut count = 1; let mut i = 1;
1190 let mut inside_quote = false;
1191 while i < chars.len() {
1192 let ch = chars[i];
1193 match ch {
1194 '\\' => {
1195 if i+1 == chars.len() {
1196 bail!("Syntax error in DEBUG: last char is escape char\nDebug string: '{}'", chars.iter().collect::<String>());
1197 }
1198 i += 1;
1199 },
1200 '\'' => inside_quote = !inside_quote,
1201 '(' if !inside_quote => {
1202 count += 1;
1203 },
1205 '(' => (),
1206 ')' if !inside_quote => {
1207 count -= 1;
1208 if count == 0 {
1209 let arg = &chars[1..i].iter().collect::<String>();
1210 let escaped_arg = arg.replace('"', "\\\"");
1211 let processed_arg = MyXPath::add_debug_string_arg(arg)?;
1213
1214 let processed_rest = MyXPath::add_debug_string_arg(&chars[i+1..].iter().collect::<String>())?;
1216 return Ok( format!("({processed_arg}, \"{escaped_arg}\"){processed_rest}") );
1217 }
1218 },
1219 ')' => (),
1220 _ => (),
1221 }
1222 i += 1;
1223 }
1224 bail!("Syntax error in DEBUG: didn't find matching closing paren\nDEBUG{}", chars.iter().collect::<String>());
1225 }
1226 }
1227
1228 fn is_true(&self, context: &sxd_xpath_no_unsafe::Context, mathml: Element) -> Result<bool> {
1229 return Ok(
1231 match self.evaluate(context, mathml)? {
1232 Value::Boolean(b) => b,
1233 Value::Nodeset(nodes) => nodes.size() > 0,
1234 _ => false,
1235 }
1236 )
1237 }
1238
1239 pub fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
1240 if self.rc.string == "process-intent(.)" {
1241 return T::from_element( infer_intent(rules_with_context, mathml)? );
1242 }
1243
1244 let result = self.evaluate(&rules_with_context.context_stack.base, mathml)
1245 .with_context(|| format!("in '{}' replacing after pattern match", self.rc.string) )?;
1246 let string = match result {
1247 Value::Nodeset(nodes) => {
1248 if nodes.size() == 0 {
1249 bail!("During replacement, no matching element found");
1250 }
1251 return rules_with_context.replace_nodes(nodes.document_order(), mathml);
1252 },
1253 Value::String(s) => s,
1254 Value::Number(num) => num.to_string(),
1255 Value::Boolean(b) => b.to_string(), };
1257 let result = if self.rc.string.starts_with('$') {string} else {rules_with_context.replace_chars(&string, mathml)?};
1260 return T::from_string(result, rules_with_context.doc );
1261 }
1262
1263 pub fn evaluate<'c>(&self, context: &sxd_xpath_no_unsafe::Context<'c>, mathml: Element<'c>) -> Result<Value<'c>> {
1264 let result = self.rc.xpath.evaluate(context, mathml);
1266 return match result {
1267 Ok(val) => Ok( val ),
1268 Err(e) => {
1269 bail!( "{}\n\n",
1271 e.to_string().replace("OwnedPrefixedName { prefix: None, local_part:", "").replace(" }", "") );
1273 }
1274 };
1275 }
1276
1277 pub fn test_input<F>(self, f: F) -> bool where F: Fn(&str) -> bool {
1278 return f(self.rc.string.as_ref());
1279 }
1280}
1281
1282#[derive(Debug)]
1287struct SpeechPattern {
1288 pattern_name: String,
1289 tag_name: String,
1290 file_name: String,
1291 pattern: MyXPath, match_uses_var_defs: bool, var_defs: VariableDefinitions, replacements: ReplacementArray, }
1296
1297impl fmt::Display for SpeechPattern {
1298 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1299 return write!(f, "[name: {}, tag: {},\n variables: {:?}, pattern: {},\n replacement: {}]",
1300 self.pattern_name, self.tag_name, self.var_defs, self.pattern,
1301 self.replacements.pretty_print_replacements());
1302 }
1303}
1304
1305impl SpeechPattern {
1306 fn build(dict: &Yaml, file: &Path, rules: &mut SpeechRules) -> Result<Option<Vec<PathBuf>>> {
1307 if let Some(include_file_name) = find_str(dict, "include") {
1313 let do_include_fn = |new_file: &Path| {
1314 rules.read_patterns(new_file)
1315 };
1316
1317 return Ok( Some(process_include(file, include_file_name, do_include_fn)?) );
1318 }
1319
1320 let pattern_name = find_str(dict, "name");
1321
1322 let mut tag_names: Vec<&str> = Vec::new();
1324 match find_str(dict, "tag") {
1325 Some(str) => tag_names.push(str),
1326 None => {
1327 let tag_array = &dict["tag"];
1329 tag_names = vec![];
1330 if tag_array.is_array() {
1331 for (i, name) in tag_array.as_vec().unwrap().iter().enumerate() {
1332 match as_str_checked(name) {
1333 Err(e) => return Err(
1334 e.context(
1335 format!("tag name '{}' is not a string in:\n{}",
1336 yaml_to_string(&tag_array.as_vec().unwrap()[i], 0),
1337 yaml_to_string(dict, 1)))
1338 ),
1339 Ok(str) => tag_names.push(str),
1340 };
1341 }
1342 } else {
1343 bail!("Errors trying to find 'tag' in:\n{}", yaml_to_string(dict, 1));
1344 }
1345 }
1346 }
1347
1348 if pattern_name.is_none() {
1349 if dict.is_null() {
1350 bail!("Error trying to find 'name': empty value (two consecutive '-'s?");
1351 } else {
1352 bail!("Errors trying to find 'name' in:\n{}", yaml_to_string(dict, 1));
1353 };
1354 };
1355 let pattern_name = pattern_name.unwrap().to_string();
1356
1357 if dict["match"].is_badvalue() {
1359 bail!("Did not find 'match' in\n{}", yaml_to_string(dict, 1));
1360 }
1361 if dict["replace"].is_badvalue() {
1362 bail!("Did not find 'replace' in\n{}", yaml_to_string(dict, 1));
1363 }
1364
1365 for tag_name in tag_names {
1367 let tag_name = tag_name.to_string();
1368 let pattern_xpath = MyXPath::build(&dict["match"])
1369 .with_context(|| {
1370 format!("value for 'match' in rule ({}: {}):\n{}",
1371 tag_name, pattern_name, yaml_to_string(dict, 1))
1372 })?;
1373 let speech_pattern =
1374 Box::new( SpeechPattern{
1375 pattern_name: pattern_name.clone(),
1376 tag_name: tag_name.clone(),
1377 file_name: file.to_str().unwrap().to_string(),
1378 match_uses_var_defs: dict["variables"].is_array() && pattern_xpath.rc.string.contains('$'), pattern: pattern_xpath,
1380 var_defs: VariableDefinitions::build(&dict["variables"])
1381 .with_context(|| {
1382 format!("value for 'variables' in rule ({}: {}):\n{}",
1383 tag_name, pattern_name, yaml_to_string(dict, 1))
1384 })?,
1385 replacements: ReplacementArray::build(&dict["replace"])
1386 .with_context(|| {
1387 format!("value for 'replace' in rule ({}: {}). Replacements:\n{}",
1388 tag_name, pattern_name, yaml_to_string(&dict["replace"], 1))
1389 })?
1390 } );
1391 let rule_value = rules.rules.entry(tag_name).or_default();
1393
1394 match rule_value.iter().enumerate().find(|&pattern| pattern.1.pattern_name == speech_pattern.pattern_name) {
1396 None => rule_value.push(speech_pattern),
1397 Some((i, _old_pattern)) => {
1398 let old_rule = &rule_value[i];
1399 info!("\n\n***WARNING***: replacing {}/'{}' in {} with rule from {}\n",
1400 old_rule.tag_name, old_rule.pattern_name, old_rule.file_name, speech_pattern.file_name);
1401 rule_value[i] = speech_pattern;
1402 },
1403 }
1404 }
1405
1406 return Ok(None);
1407 }
1408
1409 fn is_match(&self, context: &sxd_xpath_no_unsafe::Context, mathml: Element) -> Result<bool> {
1410 if self.tag_name != as_qname!(mathml.name()).local_part() && self.tag_name != "*" && self.tag_name != "!*" {
1411 return Ok( false );
1412 }
1413
1414 return Ok(
1418 match self.pattern.evaluate(context, mathml)? {
1419 Value::Boolean(b) => b,
1420 Value::Nodeset(nodes) => nodes.size() > 0,
1421 _ => false,
1422 }
1423 );
1424 }
1425}
1426
1427
1428#[derive(Debug, Clone)]
1432struct TestArray {
1433 tests: Vec<Test>
1434}
1435
1436impl fmt::Display for TestArray {
1437 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1438 for test in &self.tests {
1439 writeln!(f, "{test}")?;
1440 }
1441 return Ok( () );
1442 }
1443}
1444
1445impl TestArray {
1446 fn build(test: &Yaml) -> Result<TestArray> {
1447 let tests = if test.as_hash().is_some() {
1452 vec![test]
1453 } else if let Some(vec) = test.as_vec() {
1454 vec.iter().collect()
1455 } else {
1456 bail!("Value for 'test:' is neither a dictionary or an array.")
1457 };
1458
1459 let mut test_array = vec![];
1465 for test in tests {
1466 if test.as_hash().is_none() {
1467 bail!("Value for array entry in 'test:' must be a dictionary/contain keys");
1468 }
1469 let if_part = &test[if test_array.is_empty() {"if"} else {"else_if"}];
1470 if !if_part.is_badvalue() {
1471 let condition = Some( MyXPath::build(if_part)? );
1473 let then_part = TestOrReplacements::build(test, "then", "then_test", true)?;
1474 let else_part = TestOrReplacements::build(test, "else", "else_test", false)?;
1475 let n_keys = if else_part.is_none() {2} else {3};
1476 if test.as_hash().unwrap().len() > n_keys {
1477 bail!("A key other than 'if', 'else_if', 'then', 'then_test', 'else', or 'else_test' was found in the 'then' clause of 'test'");
1478 };
1479 test_array.push(
1480 Test { condition, then_part, else_part }
1481 );
1482 } else {
1483 let else_part = TestOrReplacements::build(test, "else", "else_test", true)?;
1485 if test.as_hash().unwrap().len() > 1 {
1486 bail!("A key other than 'if', 'else_if', 'then', 'then_test', 'else', or 'else_test' was found the 'else' clause of 'test'");
1487 };
1488 test_array.push(
1489 Test { condition: None, then_part: None, else_part }
1490 );
1491
1492 if test_array.len() < test.as_hash().unwrap().len() {
1494 bail!("'else'/'else_test' key is not last key in 'test:'");
1495 }
1496 }
1497 };
1498
1499 if test_array.is_empty() {
1500 bail!("No entries for 'test:'");
1501 }
1502
1503 return Ok( TestArray { tests: test_array } );
1504 }
1505
1506 fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
1507 for test in &self.tests {
1508 if test.is_true(&rules_with_context.context_stack.base, mathml)? {
1509 assert!(test.then_part.is_some());
1510 return test.then_part.as_ref().unwrap().replace(rules_with_context, mathml);
1511 } else if let Some(else_part) = test.else_part.as_ref() {
1512 return else_part.replace(rules_with_context, mathml);
1513 }
1514 }
1515 return T::from_string("".to_string(), rules_with_context.doc);
1516 }
1517}
1518
1519#[derive(Debug, Clone)]
1520enum TestOrReplacements {
1522 Replacements(ReplacementArray), Test(TestArray), }
1525
1526impl fmt::Display for TestOrReplacements {
1527 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1528 if let TestOrReplacements::Test(_) = self {
1529 write!(f, " _test")?;
1530 }
1531 write!(f, ":")?;
1532 return match self {
1533 TestOrReplacements::Test(t) => write!(f, "{t}"),
1534 TestOrReplacements::Replacements(r) => write!(f, "{r}"),
1535 };
1536 }
1537}
1538
1539impl TestOrReplacements {
1540 fn build(test: &Yaml, replace_key: &str, test_key: &str, key_required: bool) -> Result<Option<TestOrReplacements>> {
1541 let part = &test[replace_key];
1542 let test_part = &test[test_key];
1543 if !part.is_badvalue() && !test_part.is_badvalue() {
1544 bail!(format!("Only one of '{}' or '{}' is allowed as part of 'test'.\n{}\n \
1545 Suggestion: delete one or adjust indentation",
1546 replace_key, test_key, yaml_to_string(test, 2)));
1547 }
1548 if part.is_badvalue() && test_part.is_badvalue() {
1549 if key_required {
1550 bail!(format!("Missing one of '{}'/'{}:' as part of 'test:'\n{}\n \
1551 Suggestion: add the missing key or indent so it is contained in 'test'",
1552 replace_key, test_key, yaml_to_string(test, 2)))
1553 } else {
1554 return Ok( None );
1555 }
1556 }
1557 if test_part.is_badvalue() {
1559 return Ok( Some( TestOrReplacements::Replacements( ReplacementArray::build(part)? ) ) );
1560 } else {
1561 return Ok( Some( TestOrReplacements::Test( TestArray::build(test_part)? ) ) );
1562 }
1563 }
1564
1565 fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
1566 return match self {
1567 TestOrReplacements::Replacements(r) => r.replace(rules_with_context, mathml),
1568 TestOrReplacements::Test(t) => t.replace(rules_with_context, mathml),
1569 }
1570 }
1571}
1572
1573#[derive(Debug, Clone)]
1574struct Test {
1575 condition: Option<MyXPath>,
1576 then_part: Option<TestOrReplacements>,
1577 else_part: Option<TestOrReplacements>,
1578}
1579impl fmt::Display for Test {
1580 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1581 write!(f, "test: [ ")?;
1582 if let Some(if_part) = &self.condition {
1583 write!(f, " if: '{if_part}'")?;
1584 }
1585 if let Some(then_part) = &self.then_part {
1586 write!(f, " then{then_part}")?;
1587 }
1588 if let Some(else_part) = &self.else_part {
1589 write!(f, " else{else_part}")?;
1590 }
1591 return write!(f, "]");
1592 }
1593}
1594
1595impl Test {
1596 fn is_true(&self, context: &sxd_xpath_no_unsafe::Context, mathml: Element) -> Result<bool> {
1597 return match self.condition.as_ref() {
1598 None => Ok( false ), Some(condition) => condition.is_true(context, mathml)
1600 .context("Failure in conditional test"),
1601 }
1602 }
1603}
1604
1605#[derive(Debug, Clone)]
1607struct VariableDefinition {
1608 name: String, value: MyXPath, }
1611
1612impl fmt::Display for VariableDefinition {
1613 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1614 return write!(f, "[name: {}={}]", self.name, self.value);
1615 }
1616}
1617
1618#[derive(Debug)]
1620struct VariableValue<'v> {
1621 name: String, value: Option<Value<'v>>, }
1624
1625impl fmt::Display for VariableValue<'_> {
1626 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1627 let value = match &self.value {
1628 None => "unset".to_string(),
1629 Some(val) => format!("{val:?}")
1630 };
1631 return write!(f, "[name: {}, value: {}]", self.name, value);
1632 }
1633}
1634
1635impl VariableDefinition {
1636 fn build(name_value_def: &Yaml) -> Result<VariableDefinition> {
1637 match name_value_def.as_hash() {
1638 Some(map) => {
1639 if map.len() != 1 {
1640 bail!("definition is not a key/value pair. Found {}",
1641 yaml_to_string(name_value_def, 1) );
1642 }
1643 let (name, value) = map.iter().next().unwrap();
1644 let name = as_str_checked( name)
1645 .with_context(|| format!( "definition name is not a string: {}",
1646 yaml_to_string(name, 1) ))?.to_string();
1647 match value {
1648 Yaml::Boolean(_) | Yaml::String(_) | Yaml::Integer(_) | Yaml::Real(_) => (),
1649 _ => bail!("definition value is not a string, boolean, or number. Found {}",
1650 yaml_to_string(value, 1) )
1651 };
1652 return Ok(
1653 VariableDefinition{
1654 name,
1655 value: MyXPath::build(value)?
1656 }
1657 );
1658 },
1659 None => bail!("definition is not a key/value pair. Found {}",
1660 yaml_to_string(name_value_def, 1) )
1661 }
1662 }
1663}
1664
1665
1666#[derive(Debug, Clone)]
1667struct VariableDefinitions {
1668 defs: Vec<VariableDefinition>
1669}
1670
1671impl fmt::Display for VariableDefinitions {
1672 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1673 for def in &self.defs {
1674 write!(f, "{def},")?;
1675 }
1676 return Ok( () );
1677 }
1678}
1679
1680struct VariableValues<'v> {
1681 defs: Vec<VariableValue<'v>>
1682}
1683
1684impl fmt::Display for VariableValues<'_> {
1685 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1686 for value in &self.defs {
1687 write!(f, "{value}")?;
1688 }
1689 return writeln!(f);
1690 }
1691}
1692
1693impl VariableDefinitions {
1694 fn new(len: usize) -> VariableDefinitions {
1695 return VariableDefinitions{ defs: Vec::with_capacity(len) };
1696 }
1697
1698 fn build(defs: &Yaml) -> Result<VariableDefinitions> {
1699 if defs.is_badvalue() {
1700 return Ok( VariableDefinitions::new(0) );
1701 };
1702 if defs.is_array() {
1703 let defs = defs.as_vec().unwrap();
1704 let mut definitions = VariableDefinitions::new(defs.len());
1705 for def in defs {
1706 let variable_def = VariableDefinition::build(def)
1707 .context("definition of 'variables'")?;
1708 definitions.push( variable_def);
1709 };
1710 return Ok (definitions );
1711 }
1712 bail!( "'variables' is not an array of {{name: xpath-value}} definitions. Found {}'",
1713 yaml_to_string(defs, 1) );
1714 }
1715
1716 fn push(&mut self, var_def: VariableDefinition) {
1717 self.defs.push(var_def);
1718 }
1719
1720 fn len(&self) -> usize {
1721 return self.defs.len();
1722 }
1723}
1724
1725struct ContextStack<'c> {
1726 old_values: Vec<VariableValues<'c>>, base: sxd_xpath_no_unsafe::Context<'c> }
1730
1731impl fmt::Display for ContextStack<'_> {
1732 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1733 writeln!(f, " {} old_values", self.old_values.len())?;
1734 for values in &self.old_values {
1735 writeln!(f, " {values}")?;
1736 }
1737 return writeln!(f);
1738 }
1739}
1740
1741impl<'c, 'r> ContextStack<'c> {
1742 fn new<'a,>(pref_manager: &'a PreferenceManager) -> ContextStack<'c> {
1743 let prefs = pref_manager.merge_prefs();
1744 let mut context_stack = ContextStack {
1745 base: ContextStack::base_context(prefs),
1746 old_values: Vec::with_capacity(31) };
1748 context_stack.base.set_variable("MatchingPause", Value::Boolean(false));
1751 context_stack.base.set_variable("IsColumnSilent", Value::Boolean(false));
1752
1753
1754 return context_stack;
1755 }
1756
1757 fn base_context(var_defs: PreferenceHashMap) -> sxd_xpath_no_unsafe::Context<'c> {
1758 let mut context = sxd_xpath_no_unsafe::Context::new();
1759 context.set_namespace("m", "http://www.w3.org/1998/Math/MathML");
1760 crate::xpath_functions::add_builtin_functions(&mut context);
1761 for (key, value) in var_defs {
1762 context.set_variable(key.as_str(), yaml_to_value(&value));
1763 };
1769 return context;
1770 }
1771
1772 fn set_globals(&'r mut self, new_vars: VariableDefinitions, mathml: Element<'c>) -> Result<()> {
1773 for def in &new_vars.defs {
1775 let new_value = match def.value.evaluate(&self.base, mathml) {
1777 Ok(val) => val,
1778 Err(_) => bail!(format!("Can't evaluate variable def for {}", def)),
1779 };
1780 let qname = QName::new(def.name.as_str());
1781 self.base.set_variable(qname, new_value);
1782 }
1783 return Ok( () );
1784 }
1785
1786 fn push(&'r mut self, new_vars: VariableDefinitions, mathml: Element<'c>) -> Result<()> {
1787 let mut old_values = VariableValues {defs: Vec::with_capacity(new_vars.defs.len()) };
1789 let evaluation = Evaluation::new(&self.base, Node::Element(mathml));
1790 for def in &new_vars.defs {
1791 let qname = QName::new(def.name.as_str());
1793 let old_value = evaluation.value_of(qname).cloned();
1794 old_values.defs.push( VariableValue{ name: def.name.clone(), value: old_value} );
1795 }
1796
1797 for def in &new_vars.defs {
1799 let new_value = match def.value.evaluate(&self.base, mathml) {
1801 Ok(val) => val,
1802 Err(_) => Value::Nodeset(sxd_xpath_no_unsafe::nodeset::Nodeset::new()),
1803 };
1804 let qname = QName::new(def.name.as_str());
1805 self.base.set_variable(qname, new_value);
1806 }
1807 self.old_values.push(old_values);
1808 return Ok( () );
1809 }
1810
1811 fn pop(&mut self) {
1812 const MISSING_VALUE: &str = "-- unset value --"; let old_values = self.old_values.pop().unwrap();
1814 for variable in old_values.defs {
1815 let qname = QName::new(&variable.name);
1816 let old_value = match variable.value {
1817 None => Value::String(MISSING_VALUE.to_string()),
1818 Some(val) => val,
1819 };
1820 self.base.set_variable(qname, old_value);
1821 }
1822 }
1823}
1824
1825
1826fn yaml_to_value<'b>(yaml: &Yaml) -> Value<'b> {
1827 return match yaml {
1828 Yaml::String(s) => Value::String(s.clone()),
1829 Yaml::Boolean(b) => Value::Boolean(*b),
1830 Yaml::Integer(i) => Value::Number(*i as f64),
1831 Yaml::Real(s) => Value::Number(s.parse::<f64>().unwrap()),
1832 _ => {
1833 error!("yaml_to_value: illegal type found in Yaml value: {}", yaml_to_string(yaml, 1));
1834 Value::String("".to_string())
1835 },
1836 }
1837}
1838
1839
1840struct UnicodeDef {
1842 ch: u32,
1843 speech: ReplacementArray
1844}
1845
1846impl fmt::Display for UnicodeDef {
1847 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1848 return write!(f, "UnicodeDef{{ch: {}, speech: {:?}}}", self.ch, self.speech);
1849 }
1850}
1851
1852impl UnicodeDef {
1853 fn build(unicode_def: &Yaml, file_name: &Path, speech_rules: &SpeechRules, use_short: bool) -> Result<Option<Vec<PathBuf>>> {
1854 if let Some(include_file_name) = find_str(unicode_def, "include") {
1855 let do_include_fn = |new_file: &Path| {
1856 speech_rules.read_unicode(Some(new_file.to_path_buf()), use_short)
1857 };
1858 return Ok( Some(process_include(file_name, include_file_name, do_include_fn)?) );
1859 }
1860 let dictionary = unicode_def.as_hash();
1862 if dictionary.is_none() {
1863 bail!("Expected a unicode definition (e.g, '+':[t: \"plus\"]'), found {}", yaml_to_string(unicode_def, 0));
1864 }
1865
1866 let dictionary = dictionary.unwrap();
1867 if dictionary.len() != 1 {
1868 bail!("Expected a unicode definition (e.g, '+':[t: \"plus\"]'), found {}", yaml_to_string(unicode_def, 0));
1869 }
1870
1871 let (ch, replacements) = dictionary.iter().next().ok_or_else(|| anyhow!("Expected a unicode definition (e.g, '+':[t: \"plus\"]'), found {}", yaml_to_string(unicode_def, 0)))?;
1872 let mut unicode_table = if use_short {
1873 speech_rules.unicode_short.borrow_mut()
1874 } else {
1875 speech_rules.unicode_full.borrow_mut()
1876 };
1877 if let Some(str) = ch.as_str() {
1878 if str.is_empty() {
1879 bail!("Empty character definition. Replacement is {}", replacements.as_str().unwrap());
1880 }
1881 let mut chars = str.chars();
1882 let first_ch = chars.next().unwrap(); if chars.next().is_some() { if str.contains('-') {
1885 return process_range(str, replacements, unicode_table);
1886 } else if first_ch != '0' { for ch in str.chars() { let ch_as_str = ch.to_string();
1889 if unicode_table.insert(ch as u32, ReplacementArray::build(&substitute_ch(replacements, &ch_as_str))
1890 .with_context(|| format!("In definition of char: '{str}'"))?.replacements).is_some() {
1891 error!("*** Character '{}' (0x{:X}) is repeated", ch, ch as u32);
1892 }
1893 }
1894 return Ok(None);
1895 }
1896 }
1897 }
1898
1899 let ch = UnicodeDef::get_unicode_char(ch)?;
1900 if unicode_table.insert(ch, ReplacementArray::build(replacements)
1901 .with_context(|| format!("In definition of char: '{}' (0x{})",
1902 char::from_u32(ch).unwrap(), ch))?.replacements).is_some() {
1903 error!("*** Character '{}' (0x{:X}) is repeated", char::from_u32(ch).unwrap(), ch);
1904 }
1905 return Ok(None);
1906
1907 fn process_range(def_range: &str, replacements: &Yaml, mut unicode_table: RefMut<HashMap<u32,Vec<Replacement>>>) -> Result<Option<Vec<PathBuf>>> {
1908 let mut range = def_range.split('-');
1911 let first = range.next().unwrap().chars().next().unwrap() as u32;
1912 let last = range.next().unwrap().chars().next().unwrap() as u32;
1913 if range.next().is_some() {
1914 bail!("Character range definition has more than one '-': '{}'", def_range);
1915 }
1916
1917 for ch in first..last+1 {
1918 let ch_as_str = char::from_u32(ch).unwrap().to_string();
1919 if unicode_table.insert(ch, ReplacementArray::build(&substitute_ch(replacements, &ch_as_str))
1920 .with_context(|| format!("In definition of char: '{def_range}'"))?.replacements).is_some() {
1921 error!("*** Character '{}' (0x{:X}) is repeated", char::from_u32(ch).unwrap(), ch);
1922 }
1923 };
1924
1925 return Ok(None)
1926 }
1927
1928 fn substitute_ch(yaml: &Yaml, ch: &str) -> Yaml {
1929 return match yaml {
1930 Yaml::Array(v) => {
1931 Yaml::Array(
1932 v.iter()
1933 .map(|e| substitute_ch(e, ch))
1934 .collect::<Vec<Yaml>>()
1935 )
1936 },
1937 Yaml::Hash(h) => {
1938 Yaml::Hash(
1939 h.iter()
1940 .map(|(key,val)| (key.clone(), substitute_ch(val, ch)) )
1941 .collect::<Hash>()
1942 )
1943 },
1944 Yaml::String(s) => Yaml::String( s.replace('.', ch) ),
1945 _ => yaml.clone(),
1946 }
1947 }
1948 }
1949
1950 fn get_unicode_char(ch: &Yaml) -> Result<u32> {
1951 if let Some(ch) = ch.as_str() {
1953 let mut ch_iter = ch.chars();
1954 let unicode_ch = ch_iter.next();
1955 if unicode_ch.is_none() || ch_iter.next().is_some() {
1956 bail!("Wanted unicode char, found string '{}')", ch);
1957 };
1958 return Ok( unicode_ch.unwrap() as u32 );
1959 }
1960
1961 if let Some(num) = ch.as_i64() {
1962 return Ok( num as u32 );
1963 }
1964 bail!("Unicode character '{}' can't be converted to an code point", yaml_to_string(ch, 0));
1965 }
1966}
1967
1968type RuleTable = HashMap<String, Vec<Box<SpeechPattern>>>;
1976 type UnicodeTable = Rc<RefCell<HashMap<u32,Vec<Replacement>>>>;
1977 type FilesAndTimesShared = Rc<RefCell<FilesAndTimes>>;
1978
1979 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1980 pub enum RulesFor {
1981 Intent,
1982 Speech,
1983 OverView,
1984 Navigation,
1985 Braille,
1986 }
1987
1988 impl fmt::Display for RulesFor {
1989 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1990 let name = match self {
1991 RulesFor::Intent => "Intent",
1992 RulesFor::Speech => "Speech",
1993 RulesFor::OverView => "OverView",
1994 RulesFor::Navigation => "Navigation",
1995 RulesFor::Braille => "Braille",
1996 };
1997 return write!(f, "{name}");
1998 }
1999 }
2000
2001
2002#[derive(Debug, Clone)]
2003pub struct FileAndTime {
2004 file: PathBuf,
2005 time: SystemTime,
2006}
2007
2008impl FileAndTime {
2009 fn new(file: PathBuf) -> FileAndTime {
2010 return FileAndTime {
2011 file,
2012 time: SystemTime::UNIX_EPOCH,
2013 }
2014 }
2015
2016 pub fn debug_get_file(&self) -> Option<&str> {
2018 return self.file.to_str();
2019 }
2020
2021 pub fn new_with_time(file: PathBuf) -> FileAndTime {
2022 return FileAndTime {
2023 time: FileAndTime::get_metadata(&file),
2024 file,
2025 }
2026 }
2027
2028 pub fn is_up_to_date(&self) -> bool {
2029 let file_mod_time = FileAndTime::get_metadata(&self.file);
2030 return self.time >= file_mod_time;
2031 }
2032
2033 fn get_metadata(path: &Path) -> SystemTime {
2034 use std::fs;
2035 if !cfg!(target_family = "wasm") {
2036 let metadata = fs::metadata(path);
2037 if let Ok(metadata) = metadata &&
2038 let Ok(mod_time) = metadata.modified() {
2039 return mod_time;
2040 }
2041 }
2042 return SystemTime::UNIX_EPOCH
2043 }
2044
2045}
2046#[derive(Debug, Default)]
2047pub struct FilesAndTimes {
2048 ft: Vec<FileAndTime>
2052}
2053
2054impl FilesAndTimes {
2055 pub fn new(start_path: PathBuf) -> FilesAndTimes {
2056 let mut ft = Vec::with_capacity(8);
2057 ft.push( FileAndTime::new(start_path) );
2058 return FilesAndTimes{ ft };
2059 }
2060
2061 pub fn is_file_up_to_date(&self, pref_path: &Path, should_ignore_file_time: bool) -> bool {
2063
2064 if self.ft.is_empty() || self.as_path() != pref_path {
2066 return false;
2067 }
2068 if should_ignore_file_time || cfg!(target_family = "wasm") {
2069 return true;
2070 }
2071 if self.ft[0].time == SystemTime::UNIX_EPOCH {
2072 return false;
2073 }
2074
2075
2076 for file in &self.ft {
2078 if !file.is_up_to_date() {
2079 return false;
2080 }
2081 }
2082 return true;
2083 }
2084
2085 fn set_files_and_times(&mut self, new_files: Vec<PathBuf>) {
2086 self.ft.clear();
2087 for path in new_files {
2088 let time = FileAndTime::get_metadata(&path); self.ft.push( FileAndTime{ file: path, time })
2090 }
2091 }
2092
2093 pub fn invalidate(&mut self) {
2095 self.ft.clear();
2096 }
2097
2098 pub fn is_valid(&self) -> bool {
2099 self.ft.is_empty()
2100 }
2101
2102 pub fn as_path(&self) -> &Path {
2103 assert!(!self.ft.is_empty());
2104 return &self.ft[0].file;
2105 }
2106
2107 pub fn paths(&self) -> Vec<PathBuf> {
2108 return self.ft.iter().map(|ft| ft.file.clone()).collect::<Vec<PathBuf>>();
2109 }
2110
2111}
2112
2113
2114pub struct SpeechRules {
2120 error: String,
2121 name: RulesFor,
2122 pub pref_manager: Rc<RefCell<PreferenceManager>>,
2123 rules: RuleTable, rule_files: FilesAndTimes, translate_single_chars_only: bool, unicode_short: UnicodeTable, unicode_short_files: FilesAndTimesShared, unicode_full: UnicodeTable, unicode_full_files: FilesAndTimesShared, definitions_files: FilesAndTimesShared, }
2132
2133impl fmt::Display for SpeechRules {
2134 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2135 writeln!(f, "SpeechRules '{}'\n{})", self.name, self.pref_manager.borrow())?;
2136 let mut rules_vec: Vec<(&String, &Vec<Box<SpeechPattern>>)> = self.rules.iter().collect();
2137 rules_vec.sort_by_key(|(tag_name, _)| *tag_name);
2138 for (tag_name, rules) in rules_vec {
2139 writeln!(f, " {}: #patterns {}", tag_name, rules.len())?;
2140 };
2141 return writeln!(f, " {}+{} unicode entries", self.unicode_short.borrow().len(), self.unicode_full.borrow().len());
2142 }
2143}
2144
2145
2146pub struct SpeechRulesWithContext<'c, 's:'c, 'm:'c> {
2150 speech_rules: &'s SpeechRules,
2151 context_stack: ContextStack<'c>, doc: Document<'m>,
2153 nav_node_id: &'m str,
2154 nav_node_offset: usize,
2155 pub inside_spell: bool, pub translate_count: usize, }
2158
2159impl<'c, 's:'c, 'm:'c> fmt::Display for SpeechRulesWithContext<'c, 's,'m> {
2160 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2161 writeln!(f, "SpeechRulesWithContext \n{})", self.speech_rules)?;
2162 return writeln!(f, " {} context entries, nav node id '({}, {})'", self.context_stack, self.nav_node_id, self.nav_node_offset);
2163 }
2164}
2165
2166thread_local!{
2167 static SPEECH_UNICODE_SHORT: UnicodeTable =
2169 Rc::new( RefCell::new( HashMap::with_capacity(700) ) );
2170
2171 static SPEECH_UNICODE_FULL: UnicodeTable =
2173 Rc::new( RefCell::new( HashMap::with_capacity(6500) ) );
2174
2175 static BRAILLE_UNICODE_SHORT: UnicodeTable =
2177 Rc::new( RefCell::new( HashMap::with_capacity(500) ) );
2178
2179 static BRAILLE_UNICODE_FULL: UnicodeTable =
2181 Rc::new( RefCell::new( HashMap::with_capacity(4000) ) );
2182
2183 static SPEECH_DEFINITION_FILES_AND_TIMES: FilesAndTimesShared =
2185 Rc::new( RefCell::new(FilesAndTimes::default()) );
2186
2187 static BRAILLE_DEFINITION_FILES_AND_TIMES: FilesAndTimesShared =
2189 Rc::new( RefCell::new(FilesAndTimes::default()) );
2190
2191 static SPEECH_UNICODE_SHORT_FILES_AND_TIMES: FilesAndTimesShared =
2193 Rc::new( RefCell::new(FilesAndTimes::default()) );
2194
2195 static SPEECH_UNICODE_FULL_FILES_AND_TIMES: FilesAndTimesShared =
2197 Rc::new( RefCell::new(FilesAndTimes::default()) );
2198
2199 static BRAILLE_UNICODE_SHORT_FILES_AND_TIMES: FilesAndTimesShared =
2201 Rc::new( RefCell::new(FilesAndTimes::default()) );
2202
2203 static BRAILLE_UNICODE_FULL_FILES_AND_TIMES: FilesAndTimesShared =
2205 Rc::new( RefCell::new(FilesAndTimes::default()) );
2206
2207 pub static INTENT_RULES: RefCell<SpeechRules> =
2210 RefCell::new( SpeechRules::new(RulesFor::Intent, true) );
2211
2212 pub static SPEECH_RULES: RefCell<SpeechRules> =
2213 RefCell::new( SpeechRules::new(RulesFor::Speech, true) );
2214
2215 pub static OVERVIEW_RULES: RefCell<SpeechRules> =
2216 RefCell::new( SpeechRules::new(RulesFor::OverView, true) );
2217
2218 pub static NAVIGATION_RULES: RefCell<SpeechRules> =
2219 RefCell::new( SpeechRules::new(RulesFor::Navigation, true) );
2220
2221 pub static BRAILLE_RULES: RefCell<SpeechRules> =
2222 RefCell::new( SpeechRules::new(RulesFor::Braille, false) );
2223}
2224
2225pub fn invalidate_speech_language_caches() {
2227 SPEECH_DEFINITION_FILES_AND_TIMES.with(|files| files.borrow_mut().invalidate());
2228 SPEECH_UNICODE_SHORT_FILES_AND_TIMES.with(|files| files.borrow_mut().invalidate());
2229 SPEECH_UNICODE_FULL_FILES_AND_TIMES.with(|files| files.borrow_mut().invalidate());
2230 INTENT_RULES.with(|rules| rules.borrow_mut().rule_files.invalidate());
2231 SPEECH_RULES.with(|rules| rules.borrow_mut().rule_files.invalidate());
2232 OVERVIEW_RULES.with(|rules| rules.borrow_mut().rule_files.invalidate());
2233 NAVIGATION_RULES.with(|rules| rules.borrow_mut().rule_files.invalidate());
2234}
2235
2236pub fn invalidate_speech_style_caches() {
2238 SPEECH_RULES.with(|rules| rules.borrow_mut().rule_files.invalidate());
2239}
2240
2241pub fn invalidate_braille_caches() {
2243 BRAILLE_DEFINITION_FILES_AND_TIMES.with(|files| files.borrow_mut().invalidate());
2244 BRAILLE_UNICODE_SHORT_FILES_AND_TIMES.with(|files| files.borrow_mut().invalidate());
2245 BRAILLE_UNICODE_FULL_FILES_AND_TIMES.with(|files| files.borrow_mut().invalidate());
2246 BRAILLE_RULES.with(|rules| rules.borrow_mut().rule_files.invalidate());
2247}
2248
2249#[cfg(test)]
2250impl SpeechRules {
2252 pub(crate) fn rule_files_cache_is_empty(&self) -> bool {
2253 self.rule_files.is_valid()
2254 }
2255
2256 pub(crate) fn definitions_files_cache_is_empty(&self) -> bool {
2257 self.definitions_files.borrow().is_valid()
2258 }
2259
2260 pub(crate) fn definitions_files_cache_path(&self) -> PathBuf {
2261 self.definitions_files.borrow().as_path().to_path_buf()
2262 }
2263}
2264
2265impl SpeechRules {
2266 pub fn new(name: RulesFor, translate_single_chars_only: bool) -> SpeechRules {
2267 let globals = if name == RulesFor::Braille {
2268 (
2269 (BRAILLE_UNICODE_SHORT.with(Rc::clone), BRAILLE_UNICODE_SHORT_FILES_AND_TIMES.with(Rc::clone)),
2270 (BRAILLE_UNICODE_FULL. with(Rc::clone), BRAILLE_UNICODE_FULL_FILES_AND_TIMES.with(Rc::clone)),
2271 BRAILLE_DEFINITION_FILES_AND_TIMES.with(Rc::clone),
2272 )
2273 } else {
2274 (
2275 (SPEECH_UNICODE_SHORT.with(Rc::clone), SPEECH_UNICODE_SHORT_FILES_AND_TIMES.with(Rc::clone)),
2276 (SPEECH_UNICODE_FULL. with(Rc::clone), SPEECH_UNICODE_FULL_FILES_AND_TIMES.with(Rc::clone)),
2277 SPEECH_DEFINITION_FILES_AND_TIMES.with(Rc::clone),
2278 )
2279 };
2280
2281 return SpeechRules {
2282 error: Default::default(),
2283 name,
2284 rules: HashMap::with_capacity(if name == RulesFor::Intent || name == RulesFor::Speech {500} else {50}), rule_files: FilesAndTimes::default(),
2286 unicode_short: globals.0.0, unicode_short_files: globals.0.1,
2288 unicode_full: globals.1.0, unicode_full_files: globals.1.1,
2290 definitions_files: globals.2,
2291 translate_single_chars_only,
2292 pref_manager: PreferenceManager::get(),
2293 };
2294}
2295
2296 pub fn get_error(&self) -> Option<&str> {
2297 return if self.error.is_empty() {
2298 None
2299 } else {
2300 Some(&self.error)
2301 }
2302 }
2303
2304 pub fn read_files(&mut self) -> Result<()> {
2305 let check_rule_files = self.pref_manager.borrow().pref_to_string("CheckRuleFiles");
2306 if check_rule_files != "None" { self.pref_manager.borrow_mut().set_preference_files()?;
2308 }
2309 let should_ignore_file_time = self.pref_manager.borrow().pref_to_string("CheckRuleFiles") != "All"; let rule_file = self.pref_manager.borrow().get_rule_file(&self.name).to_path_buf(); if self.rules.is_empty() || !self.rule_files.is_file_up_to_date(&rule_file, should_ignore_file_time) {
2312 self.rules.clear();
2313 let files_read = self.read_patterns(&rule_file)?;
2314 self.rule_files.set_files_and_times(files_read);
2315 }
2316
2317 let pref_manager = self.pref_manager.borrow();
2318 let unicode_pref_files = if self.name == RulesFor::Braille {pref_manager.get_braille_unicode_file()} else {pref_manager.get_speech_unicode_file()};
2319
2320 if !self.unicode_short_files.borrow().is_file_up_to_date(unicode_pref_files.0, should_ignore_file_time) {
2321 self.unicode_short.borrow_mut().clear();
2322 self.unicode_short_files.borrow_mut().set_files_and_times(self.read_unicode(None, true)?);
2323 }
2324
2325 if self.definitions_files.borrow().ft.is_empty() || !self.definitions_files.borrow().is_file_up_to_date(
2326 pref_manager.get_definitions_file(self.name != RulesFor::Braille),
2327 should_ignore_file_time
2328 ) {
2329 self.definitions_files.borrow_mut().set_files_and_times(read_definitions_file(self.name != RulesFor::Braille)?);
2330 }
2331 return Ok( () );
2332 }
2333
2334 fn read_patterns(&mut self, path: &Path) -> Result<Vec<PathBuf>> {
2335 let rule_file_contents = read_to_string_shim(path).with_context(|| format!("cannot read file '{}'", path.to_str().unwrap()))?;
2337 let rules_build_fn = |pattern: &Yaml| {
2338 self.build_speech_patterns(pattern, path)
2339 .with_context(||format!("in file {:?}", path.to_str().unwrap()))
2340 };
2341 return compile_rule(&rule_file_contents, rules_build_fn)
2342 .with_context(||format!("in file {:?}", path.to_str().unwrap()));
2343 }
2344
2345 fn build_speech_patterns(&mut self, patterns: &Yaml, file_name: &Path) -> Result<Vec<PathBuf>> {
2346 let patterns_vec = patterns.as_vec();
2348 if patterns_vec.is_none() {
2349 bail!(yaml_type_err(patterns, "array"));
2350 }
2351 let patterns_vec = patterns.as_vec().unwrap();
2352 let mut files_read = vec![file_name.to_path_buf()];
2353 for entry in patterns_vec.iter() {
2354 if let Some(mut added_files) = SpeechPattern::build(entry, file_name, self)? {
2355 files_read.append(&mut added_files);
2356 }
2357 }
2358 return Ok(files_read)
2359 }
2360
2361 fn read_unicode(&self, path: Option<PathBuf>, use_short: bool) -> Result<Vec<PathBuf>> {
2362 let path = match path {
2363 Some(p) => p,
2364 None => {
2365 let pref_manager = self.pref_manager.borrow();
2367 let unicode_files = if self.name == RulesFor::Braille {
2368 pref_manager.get_braille_unicode_file()
2369 } else {
2370 pref_manager.get_speech_unicode_file()
2371 };
2372 let unicode_files = if use_short {unicode_files.0} else {unicode_files.1};
2373 unicode_files.to_path_buf()
2374 }
2375 };
2376
2377 let unicode_file_contents = read_to_string_shim(&path)?;
2380 let unicode_build_fn = |unicode_def_list: &Yaml| {
2381 let unicode_defs = unicode_def_list.as_vec();
2382 if unicode_defs.is_none() {
2383 bail!("File '{}' does not begin with an array", yaml_to_type(unicode_def_list));
2384 };
2385 let mut files_read = vec![path.to_path_buf()];
2386 for unicode_def in unicode_defs.unwrap() {
2387 if let Some(mut added_files) = UnicodeDef::build(unicode_def, &path, self, use_short)
2388 .with_context(|| {format!("In file {:?}", path.to_str())})? {
2389 files_read.append(&mut added_files);
2390 }
2391 };
2392 return Ok(files_read)
2393 };
2394
2395 return compile_rule(&unicode_file_contents, unicode_build_fn)
2396 .with_context(||format!("in file {:?}", path.to_str().unwrap()));
2397 }
2398
2399 pub fn print_sizes() -> String {
2400 let mut answer = rule_size(&SPEECH_RULES, "SPEECH_RULES");
2408 answer += &rule_size(&INTENT_RULES, "INTENT_RULES");
2409 answer += &rule_size(&BRAILLE_RULES, "BRAILLE_RULES");
2410 answer += &rule_size(&NAVIGATION_RULES, "NAVIGATION_RULES");
2411 answer += &rule_size(&OVERVIEW_RULES, "OVERVIEW_RULES");
2412 SPEECH_RULES.with_borrow(|rule| {
2413 answer += &format!("Speech Unicode tables: short={}/{}, long={}/{}\n",
2414 rule.unicode_short.borrow().len(), rule.unicode_short.borrow().capacity(),
2415 rule.unicode_full.borrow().len(), rule.unicode_full.borrow().capacity());
2416 });
2417 BRAILLE_RULES.with_borrow(|rule| {
2418 answer += &format!("Braille Unicode tables: short={}/{}, long={}/{}\n",
2419 rule.unicode_short.borrow().len(), rule.unicode_short.borrow().capacity(),
2420 rule.unicode_full.borrow().len(), rule.unicode_full.borrow().capacity());
2421 });
2422 return answer;
2423
2424 fn rule_size(rules: &'static std::thread::LocalKey<RefCell<SpeechRules>>, name: &str) -> String {
2425 rules.with_borrow(|rule| {
2426 let hash_map = &rule.rules;
2427 return format!("{}: {}/{}\n", name, hash_map.len(), hash_map.capacity());
2428 })
2429 }
2430 }
2431}
2432
2433
2434impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> {
2439 pub fn new(speech_rules: &'s SpeechRules, doc: Document<'m>, nav_node_id: &'m str, nav_node_offset: usize) -> SpeechRulesWithContext<'c, 's, 'm> {
2440 return SpeechRulesWithContext {
2441 speech_rules,
2442 context_stack: ContextStack::new(&speech_rules.pref_manager.borrow()),
2443 doc,
2444 nav_node_id,
2445 nav_node_offset,
2446 inside_spell: false,
2447 translate_count: 0,
2448 }
2449 }
2450
2451 pub fn get_rules(&mut self) -> &SpeechRules {
2452 return self.speech_rules;
2453 }
2454
2455 pub fn escape_string_for_safety(&self, s: String) -> String {
2456 return crate::tts::escape_string_for_safety(
2457 s,
2458 self.speech_rules.name,
2459 &self.speech_rules.pref_manager.borrow().get_tts(),
2460 );
2461 }
2462
2463 pub fn get_context(&mut self) -> &mut sxd_xpath_no_unsafe::Context<'c> {
2464 return &mut self.context_stack.base;
2465 }
2466
2467 pub fn get_document(&mut self) -> Document<'m> {
2468 return self.doc;
2469 }
2470
2471 pub fn set_nav_node_offset(&mut self, offset: usize) {
2472 self.nav_node_offset = offset;
2474 }
2475
2476 pub fn match_pattern<T:TreeOrString<'c, 'm, T>>(&'r mut self, mathml: Element<'c>) -> Result<T> {
2477 let raw_name = mathml.name();
2479 let tag_name = as_qname!(raw_name).local_part();
2480 let rules = &self.speech_rules.rules;
2481
2482 if let Some(rule_vector) = rules.get("!*") &&
2484 let Some(result) = self.find_match(rule_vector, mathml)? {
2485 return Ok(result); }
2487
2488 if let Some(rule_vector) = rules.get(tag_name) &&
2489 let Some(result) = self.find_match(rule_vector, mathml)? {
2490 return Ok(result); }
2492
2493 if let Some(rule_vector) = rules.get("*") &&
2495 let Some(result) = self.find_match(rule_vector, mathml)? {
2496 return Ok(result); }
2498
2499 let speech_manager = self.speech_rules.pref_manager.borrow();
2502 let file_name = speech_manager.get_rule_file(&self.speech_rules.name);
2503 bail!("\nNo match found!\nMissing patterns in {} for MathML.\n{}", file_name.to_string_lossy(), mml_to_string(mathml));
2505 }
2506
2507 fn find_match<T:TreeOrString<'c, 'm, T>>(&'r mut self, rule_vector: &[Box<SpeechPattern>], mathml: Element<'c>) -> Result<Option<T>> {
2508 for pattern in rule_vector {
2509 if pattern.match_uses_var_defs {
2513 self.context_stack.push(pattern.var_defs.clone(), mathml)?;
2514 }
2515 if pattern.is_match(&self.context_stack.base, mathml)
2516 .with_context(|| error_string(pattern, mathml) )? {
2517 if !pattern.match_uses_var_defs && pattern.var_defs.len() > 0 { self.context_stack.push(pattern.var_defs.clone(), mathml)?;
2520 }
2521 let result = if self.nav_node_offset > 0 &&
2522 self.nav_node_id == mathml.attribute_value("id").as_deref().unwrap_or_default() && is_leaf(mathml) {
2523 let ch = crate::canonicalize::as_text(mathml).chars().nth(self.nav_node_offset-1).unwrap_or_default();
2524 let ch = self.replace_single_char(ch, mathml)?;
2525 T::from_string(ch.to_string(), self.doc)
2529 } else {
2530 pattern.replacements.replace(self, mathml)
2531 };
2532 if pattern.var_defs.len() > 0 {
2533 self.context_stack.pop();
2534 }
2535 return match result {
2536 Ok(s) => {
2537 if self.nav_node_id.is_empty() {
2539 Ok( Some(s) )
2540 } else {
2541 if self.nav_node_id == mathml.attribute_value("id").as_deref().unwrap_or_default() {debug!("Matched pattern name/tag: {}/{}", pattern.pattern_name, pattern.tag_name)};
2542 Ok ( Some(self.nav_node_adjust(s, mathml)) )
2543 }
2544 },
2545 Err(e) => Err( e.context(
2546 format!(
2547 "attempting replacement pattern: \"{}\" for \"{}\".\n\
2548 Replacement\n{}\n...due to matching the MathML\n{} with the pattern\n\
2549 {}\n\
2550 The patterns are in {}.\n",
2551 pattern.pattern_name, pattern.tag_name,
2552 pattern.replacements.pretty_print_replacements(),
2553 mml_to_string(mathml), pattern.pattern,
2554 pattern.file_name
2555 )
2556 ))
2557 }
2558 } else if pattern.match_uses_var_defs {
2559 self.context_stack.pop();
2560 }
2561 };
2562 return Ok(None); fn error_string(pattern: &SpeechPattern, mathml: Element) -> String {
2565 return format!(
2566 "error during pattern match using: \"{}\" for \"{}\".\n\
2567 Pattern is \n{}\nMathML for the match:\n\
2568 {}\
2569 The patterns are in {}.\n",
2570 pattern.pattern_name, pattern.tag_name,
2571 pattern.pattern,
2572 mml_to_string(mathml),
2573 pattern.file_name
2574 );
2575 }
2576
2577 }
2578
2579 fn nav_node_adjust<T:TreeOrString<'c, 'm, T>>(&self, speech: T, mathml: Element<'c>) -> T {
2580 if let Some(id) = mathml.attribute_value("id") &&
2581 self.nav_node_id == id {
2582 let raw_offset = mathml.attribute_value(crate::navigate::ID_OFFSET);
2583 let offset = raw_offset.as_deref().unwrap_or("0");
2584 debug!("nav_node_adjust: id/name='{}/{}' offset?='{}'", id, name(mathml),
2585 self.nav_node_offset.to_string().as_str() == offset
2586 );
2587 if is_leaf(mathml) || self.nav_node_offset.to_string().as_str() == offset {
2588 if self.speech_rules.name == RulesFor::Braille {
2589 let highlight_style = self.speech_rules.pref_manager.borrow().pref_to_string("BrailleNavHighlight");
2590 return T::highlight_braille(speech, highlight_style);
2591 } else {
2592 return T::mark_nav_speech(speech)
2594 }
2595 }
2596 }
2597 return speech;
2598 }
2599
2600 fn highlight_braille_string(braille: String, highlight_style: String) -> String {
2601 if &highlight_style == "Off" || braille.is_empty() {
2603 return braille;
2604 }
2605
2606 let mut chars = braille.chars().collect::<Vec<char>>();
2609
2610 let baseline_indicator_hack = PreferenceManager::get().borrow().pref_to_string("BrailleCode") == "Nemeth";
2612 let mut i_first_modified = 0;
2614 for (i, ch) in chars.iter_mut().enumerate() {
2615 let modified_ch = add_dots_to_braille_char(*ch, baseline_indicator_hack);
2616 if *ch != modified_ch {
2617 *ch = modified_ch;
2618 i_first_modified = i;
2619 break;
2620 };
2621 };
2622
2623 let mut i_last_modified = i_first_modified;
2624 if &highlight_style != "FirstChar" {
2625 for i in (i_first_modified..chars.len()).rev(){
2627 let ch = chars[i];
2628 let modified_ch = add_dots_to_braille_char(ch, baseline_indicator_hack);
2629 chars[i] = modified_ch;
2630 if ch != modified_ch {
2631 i_last_modified = i;
2632 break;
2633 }
2634 }
2635 }
2636
2637 if &highlight_style == "All" {
2638 #[allow(clippy::needless_range_loop)] for i in i_first_modified+1..i_last_modified {
2641 chars[i] = add_dots_to_braille_char(chars[i], baseline_indicator_hack);
2642 };
2643 }
2644
2645 let result = chars.into_iter().collect::<String>();
2646 return result;
2648
2649 fn add_dots_to_braille_char(ch: char, baseline_indicator_hack: bool) -> char {
2650 let as_u32 = ch as u32;
2651 if (0x2800..0x28FF).contains(&as_u32) {
2652 return unsafe {char::from_u32_unchecked(as_u32 | 0xC0)}; } else if baseline_indicator_hack && ch == 'b' {
2654 return '𝑏'
2655 } else {
2656 return ch;
2657 }
2658 }
2659 }
2660
2661 fn mark_nav_speech(speech: String) -> String {
2662 if !speech.contains("[[") {
2666 return "[[".to_string() + &speech + "]]";
2667 } else {
2668 return speech
2669 }
2670 }
2671
2672 fn replace<T:TreeOrString<'c, 'm, T>>(&'r mut self, replacement: &Replacement, mathml: Element<'c>) -> Result<T> {
2673 return Ok(
2674 match replacement {
2675 Replacement::Text(t) => T::from_string(t.clone(), self.doc)?,
2676 Replacement::XPath(xpath) => xpath.replace(self, mathml)?,
2677 Replacement::TTS(tts) => {
2678 T::from_string(
2679 self.speech_rules.pref_manager.borrow().get_tts().replace(tts, &self.speech_rules.pref_manager.borrow(), self, mathml)?,
2680 self.doc
2681 )?
2682 },
2683 Replacement::Intent(intent) => {
2684 intent.replace(self, mathml)?
2685 },
2686 Replacement::Test(test) => {
2687 test.replace(self, mathml)?
2688 },
2689 Replacement::With(with) => {
2690 with.replace(self, mathml)?
2691 },
2692 Replacement::SetVariables(vars) => {
2693 vars.replace(self, mathml)?
2694 },
2695 Replacement::Insert(ic) => {
2696 ic.replace(self, mathml)?
2697 },
2698 Replacement::Translate(id) => {
2699 id.replace(self, mathml)?
2700 },
2701 }
2702 )
2703 }
2704
2705 fn replace_nodes<T:TreeOrString<'c, 'm, T>>(&'r mut self, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<T> {
2709 return T::replace_nodes(self, nodes, mathml);
2710 }
2711
2712 fn replace_nodes_tree(&'r mut self, nodes: Vec<Node<'c>>, _mathml: Element<'c>) -> Result<Element<'m>> {
2715 let mut children = Vec::with_capacity(3*nodes.len()); for node in nodes {
2717 let matched = match node {
2718 Node::Element(n) => self.match_pattern::<Element<'m>>(n)?,
2719 Node::Text(t) => {
2720 let leaf = create_mathml_element(&self.doc, "TEMP_NAME");
2721 leaf.set_text(as_str!(t.text()));
2722 leaf
2723 },
2724 Node::Attribute(attr) => {
2725 let leaf = create_mathml_element(&self.doc, "TEMP_NAME");
2727 leaf.set_text(as_str!(attr.value()));
2728 leaf
2729 },
2730 _ => {
2731 bail!("replace_nodes: found unexpected node type!!!");
2732 },
2733 };
2734 children.push(matched);
2735 }
2736
2737 let result = create_mathml_element(&self.doc, "TEMP_NAME"); result.append_children(children);
2739 return Ok( result );
2741 }
2742
2743 fn replace_nodes_string(&'r mut self, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<String> {
2744 let mut result = String::with_capacity(3*nodes.len()); let mut first_time = true;
2747 for node in nodes {
2748 if first_time {
2749 first_time = false;
2750 } else {
2751 result.push(' ');
2752 };
2753 let matched = match node {
2754 Node::Element(n) => self.match_pattern::<String>(n)?,
2755 Node::Text(t) => self.replace_chars(as_str!(t.text()), mathml)?,
2756 Node::Attribute(attr) => self.replace_chars(as_str!(attr.value()), mathml)?,
2757 _ => bail!("replace_nodes: found unexpected node type!!!"),
2758 };
2759 result += &matched;
2760 }
2761 return Ok( result );
2762 }
2763
2764 pub fn replace_chars(&'r mut self, str: &str, mathml: Element<'c>) -> Result<String> {
2767 if is_quoted_string(str) { return Ok(unquote_string(str).to_string());
2769 }
2770 self.replace_chars_escaping_xml_chars(str, mathml)
2771 }
2772
2773 fn replace_chars_escaping_xml_chars(&'r mut self, str: &str, mathml: Element<'c>) -> Result<String> {
2774 let chars = str.chars().collect::<Vec<char>>();
2775 let rules = self.speech_rules;
2776 if rules.translate_single_chars_only {
2792 if chars.len() == 1 {
2793 return self.replace_single_char(chars[0], mathml);
2794 } else {
2795 let s = str.replace('\u{00A0}', " ").replace(['\u{2061}', '\u{2062}', '\u{2063}', '\u{2064}'], "");
2797 return Ok(self.escape_string_for_safety(s));
2798 }
2799 }
2800
2801 let result = chars.iter()
2802 .map(|&ch| self.replace_single_char(ch, mathml))
2803 .collect::<Result<Vec<String>>>()?
2804 .join("");
2805 return Ok(result);
2806 }
2807
2808 fn replace_single_char(&'r mut self, ch: char, mathml: Element<'c>) -> Result<String> {
2809 let ch_as_u32 = ch as u32;
2810 let rules = self.speech_rules;
2811 let mut unicode = rules.unicode_short.borrow();
2812 let mut replacements = unicode.get( &ch_as_u32 );
2813 if replacements.is_none() {
2815 let pref_manager = rules.pref_manager.borrow();
2817 let unicode_pref_files = if rules.name == RulesFor::Braille {pref_manager.get_braille_unicode_file()} else {pref_manager.get_speech_unicode_file()};
2818 let should_ignore_file_time = pref_manager.pref_to_string("CheckRuleFiles") == "All";
2819 if rules.unicode_full.borrow().is_empty() || !rules.unicode_full_files.borrow().is_file_up_to_date(unicode_pref_files.1, should_ignore_file_time) {
2820 info!("*** Loading full unicode {} for char '{}'/{:#06x}", rules.name, ch, ch_as_u32);
2821 rules.unicode_full.borrow_mut().clear();
2822 rules.unicode_full_files.borrow_mut().set_files_and_times(rules.read_unicode(None, false)?);
2823 if cfg!(debug_assertions) {
2825 let unicode_full = rules.unicode_full.borrow();
2826 for ch in unicode.keys() {
2827 if unicode_full.get(ch).is_some() {
2828 error!("*** Character '{}' (0x{:X}) is repeated in both short and full unicode tables", *ch, *ch);
2829 }
2830 }
2831 }
2832 info!("# Unicode defs = {}/{}", rules.unicode_short.borrow().len(), rules.unicode_full.borrow().len());
2833 }
2834 unicode = rules.unicode_full.borrow();
2835 replacements = unicode.get( &ch_as_u32 );
2836 if replacements.is_none() {
2837 self.translate_count = 0; if rules.translate_single_chars_only || ch.is_ascii() { return Ok(self.escape_string_for_safety(String::from(ch)));
2841 } else {
2842 let ch_as_int = ch as u32;
2843 if ('\u{2800}'..='\u{28ff}').contains(&ch) { return Ok(self.escape_string_for_safety(String::from(ch)));
2845 } else { let prefix_indicator = if ch_as_int < 1<<16 {'x'} else {'y'};
2847 return self.replace_chars( &format!("'\\{prefix_indicator}{:06x}'", ch_as_int), mathml);
2848 }
2849 }
2850 }
2851 };
2852
2853 let result = replacements.unwrap()
2855 .iter()
2856 .map(|replacement|
2857 self.replace(replacement, mathml)
2858 .with_context(|| format!("Unicode replacement error: {replacement}")) )
2859 .collect::<Result<Vec<String>>>()?
2860 .join(" ");
2861 self.translate_count = 0; return Ok(result);
2863 }
2864}
2865
2866pub fn braille_replace_chars(str: &str, mathml: Element) -> Result<String> {
2868 return BRAILLE_RULES.with(|rules| {
2869 let rules = rules.borrow();
2870 let new_package = Package::new();
2871 let mut rules_with_context = SpeechRulesWithContext::new(&rules, new_package.as_document(), "", 0);
2872 return match rules_with_context.replace_chars(str, mathml) {
2873 Ok(s) => Ok(
2874 s.replace(CONCAT_STRING, "")
2875 .replace(CONCAT_INDICATOR, "")
2876 .replace(POSTFIX_CONCAT_STRING, "")
2877 .replace(POSTFIX_CONCAT_INDICATOR, "")
2878 ),
2879 Err(e) => Err(e),
2880 }
2881
2882
2883 })
2884}
2885
2886
2887
2888#[cfg(test)]
2889mod tests {
2890 #[allow(unused_imports)]
2891 use crate::init_logger;
2892
2893 use super::*;
2894
2895 #[test]
2896 fn test_read_statement() {
2897 let str = r#"---
2898 {name: default, tag: math, match: ".", replace: [x: "./*"] }"#;
2899 let doc = YamlLoader::load_from_str(str).unwrap();
2900 assert_eq!(doc.len(), 1);
2901 let mut rules = SpeechRules::new(RulesFor::Speech, true);
2902
2903 SpeechPattern::build(&doc[0], Path::new("testing"), &mut rules).unwrap();
2904 assert_eq!(rules.rules["math"].len(), 1, "\nshould only be one rule");
2905
2906 let speech_pattern = &rules.rules["math"][0];
2907 assert_eq!(speech_pattern.pattern_name, "default", "\npattern name failure");
2908 assert_eq!(speech_pattern.tag_name, "math", "\ntag name failure");
2909 assert_eq!(speech_pattern.pattern.rc.string, ".", "\npattern failure");
2910 assert_eq!(speech_pattern.replacements.replacements.len(), 1, "\nreplacement failure");
2911 assert_eq!(speech_pattern.replacements.replacements[0].to_string(), r#""./*""#, "\nreplacement failure");
2912 }
2913
2914 #[test]
2915 fn test_read_statements_with_replace() {
2916 let str = r#"---
2917 {name: default, tag: math, match: ".", replace: [x: "./*"] }"#;
2918 let doc = YamlLoader::load_from_str(str).unwrap();
2919 assert_eq!(doc.len(), 1);
2920 let mut rules = SpeechRules::new(RulesFor::Speech, true);
2921 SpeechPattern::build(&doc[0], Path::new("testing"), &mut rules).unwrap();
2922
2923 let str = r#"---
2924 {name: default, tag: math, match: ".", replace: [t: "test", x: "./*"] }"#;
2925 let doc2 = YamlLoader::load_from_str(str).unwrap();
2926 assert_eq!(doc2.len(), 1);
2927 SpeechPattern::build(&doc2[0], Path::new("testing"), &mut rules).unwrap();
2928 assert_eq!(rules.rules["math"].len(), 1, "\nfirst rule not replaced");
2929
2930 let speech_pattern = &rules.rules["math"][0];
2931 assert_eq!(speech_pattern.pattern_name, "default", "\npattern name failure");
2932 assert_eq!(speech_pattern.tag_name, "math", "\ntag name failure");
2933 assert_eq!(speech_pattern.pattern.rc.string, ".", "\npattern failure");
2934 assert_eq!(speech_pattern.replacements.replacements.len(), 2, "\nreplacement failure");
2935 }
2936
2937 #[test]
2938 fn test_read_statements_with_add() {
2939 let str = r#"---
2940 {name: default, tag: math, match: ".", replace: [x: "./*"] }"#;
2941 let doc = YamlLoader::load_from_str(str).unwrap();
2942 assert_eq!(doc.len(), 1);
2943 let mut rules = SpeechRules::new(RulesFor::Speech, true);
2944 SpeechPattern::build(&doc[0], Path::new("testing"), &mut rules).unwrap();
2945
2946 let str = r#"---
2947 {name: another-rule, tag: math, match: ".", replace: [t: "test", x: "./*"] }"#;
2948 let doc2 = YamlLoader::load_from_str(str).unwrap();
2949 assert_eq!(doc2.len(), 1);
2950 SpeechPattern::build(&doc2[0], Path::new("testing"), &mut rules).unwrap();
2951 assert_eq!(rules.rules["math"].len(), 2, "\nsecond rule not added");
2952
2953 let speech_pattern = &rules.rules["math"][0];
2954 assert_eq!(speech_pattern.pattern_name, "default", "\npattern name failure");
2955 assert_eq!(speech_pattern.tag_name, "math", "\ntag name failure");
2956 assert_eq!(speech_pattern.pattern.rc.string, ".", "\npattern failure");
2957 assert_eq!(speech_pattern.replacements.replacements.len(), 1, "\nreplacement failure");
2958 }
2959
2960 #[test]
2961 fn test_debug_no_debug() {
2962 let str = r#"*[2]/*[3][text()='3']"#;
2963 let result = MyXPath::add_debug_string_arg(str);
2964 assert!(result.is_ok());
2965 assert_eq!(result.unwrap(), str);
2966 }
2967
2968 #[test]
2969 fn test_debug_no_debug_with_quote() {
2970 let str = r#"*[2]/*[3][text()='(']"#;
2971 let result = MyXPath::add_debug_string_arg(str);
2972 assert!(result.is_ok());
2973 assert_eq!(result.unwrap(), str);
2974 }
2975
2976 #[test]
2977 fn test_debug_no_quoted_paren() {
2978 let str = r#"DEBUG(*[2]/*[3][text()='3'])"#;
2979 let result = MyXPath::add_debug_string_arg(str);
2980 assert!(result.is_ok());
2981 assert_eq!(result.unwrap(), r#"DEBUG(*[2]/*[3][text()='3'], "*[2]/*[3][text()='3']")"#);
2982 }
2983
2984 #[test]
2985 fn test_debug_quoted_paren() {
2986 let str = r#"DEBUG(*[2]/*[3][text()='('])"#;
2987 let result = MyXPath::add_debug_string_arg(str);
2988 assert!(result.is_ok());
2989 assert_eq!(result.unwrap(), r#"DEBUG(*[2]/*[3][text()='('], "*[2]/*[3][text()='(']")"#);
2990 }
2991
2992 #[test]
2993 fn test_debug_quoted_paren_before_paren() {
2994 let str = r#"DEBUG(ClearSpeak_Matrix = 'Combinatorics') and IsBracketed(., '(', ')')"#;
2995 let result = MyXPath::add_debug_string_arg(str);
2996 assert!(result.is_ok());
2997 assert_eq!(result.unwrap(), r#"DEBUG(ClearSpeak_Matrix = 'Combinatorics', "ClearSpeak_Matrix = 'Combinatorics'") and IsBracketed(., '(', ')')"#);
2998 }
2999
3000
3001cfg_if::cfg_if! {if #[cfg(not(feature = "include-zip"))] {
3003 #[test]
3004 fn test_up_to_date() {
3005 use crate::interface::*;
3006 set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
3008 set_preference("Language", "zz-aa").unwrap();
3009 if let Err(e) = set_mathml("<math><mi>x</mi></math>") {
3011 error!("{}", crate::errors_to_string(&e));
3012 panic!("Should not be an error in setting MathML")
3013 }
3014
3015 set_preference("CheckRuleFiles", "All").unwrap();
3016 assert!(!is_file_time_same(), "file's time did not get updated");
3017 set_preference("CheckRuleFiles", "None").unwrap();
3018 assert!(is_file_time_same(), "file's time was wrongly updated (preference 'CheckRuleFiles' should have prevented updating)");
3019
3020 fn is_file_time_same() -> bool {
3022 use std::time::Duration;
3026 return SPEECH_RULES.with(|rules| {
3027 let start_main_file = rules.borrow().unicode_short_files.borrow().ft[0].clone();
3028
3029 let contents = std::fs::read(&start_main_file.file).expect(&format!("Failed to read file {} during test", &start_main_file.file.to_string_lossy()));
3031 std::fs::write(start_main_file.file, contents).unwrap();
3032 std::thread::sleep(Duration::from_millis(5)); if let Err(e) = get_spoken_text() {
3036 error!("{}", crate::errors_to_string(&e));
3037 panic!("Should not be an error in speech")
3038 }
3039 return rules.borrow().unicode_short_files.borrow().ft[0].time == start_main_file.time;
3040 });
3041 }
3042 }
3043}}
3044
3045 }