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