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