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