/home/runner/work/MathCAT/MathCAT/src/xpath_functions.rs
Line | Count | Source |
1 | | #![allow(clippy::needless_return)] |
2 | | //! XPath underlies rule matching and speech generation. The version of xpath used is based on xpath 1.0 |
3 | | //! and includes the ability to define functions and variables. |
4 | | //! The variables defined are all the preferences and also variables set in speech rules via the `variables` keyword. |
5 | | //! The function defined here are: |
6 | | //! * `IsNode(node, kind)`: returns true if the node matches the "kind". |
7 | | //! Valid values are "leaf", "2D", "simple", "common_fraction", "trig_name". |
8 | | //! * `ToOrdinal(number, fractional, plural)`: converts the number to an ordinal (e.g, third) |
9 | | //! * `number` -- the number to translate |
10 | | //! * `fractional` -- true if this is a fractional ordinal (e.g, "half") |
11 | | //! * `plural` -- true if answer should be plural |
12 | | //! * `ToCommonFraction(mfrac)` -- converts the fraction to an ordinal version (e.g, 2 thirds) |
13 | | //! * `IsLargeOp(node)` -- returns true if the node is a large operator (e.g, integral or sum) |
14 | | //! * `IsBracketed(node, left, right, requires_comma)` -- returns true if the first/last element in the mrow match `left`/`right`. |
15 | | //! If the optional `requires_comma` argument is given and is `true`, then there also must be a "," in the mrow (e.g., "f(x,y)") |
16 | | //! * `DEBUG(xpath)` -- _Very_ useful function for debugging speech rules. |
17 | | //! This can be used to surround a whole or part of an xpath expression in a match or output. |
18 | | //! The result will be printed to standard output and the result returned so that `DEBUG` does not affect the computation. |
19 | | |
20 | | use sxd_document::dom::{Element, ChildOfElement}; |
21 | | use sxd_xpath::{Value, Context, context, function::*, nodeset::*}; |
22 | | use crate::definitions::{Definitions, SPEECH_DEFINITIONS, BRAILLE_DEFINITIONS}; |
23 | | use regex::Regex; |
24 | | use crate::pretty_print::mml_to_string; |
25 | | use std::cell::{Ref, RefCell}; |
26 | | use log::{debug, error, warn}; |
27 | | use std::sync::LazyLock; |
28 | | use std::thread::LocalKey; |
29 | | use phf::phf_set; |
30 | | use sxd_xpath::function::Error as XPathError; |
31 | | use crate::canonicalize::{as_element, name, get_parent, MATHML_FROM_NAME_ATTR}; |
32 | | |
33 | | // useful utility functions |
34 | | // note: child of an element is a ChildOfElement, so sometimes it is useful to have parallel functions, |
35 | | // one for Element and one for ChildOfElement. |
36 | | |
37 | | // @returns {String} -- the text of the (leaf) element otherwise an empty string |
38 | 126k | fn get_text_from_element(e: Element) -> String { |
39 | 126k | if e.children().len() == 1 && |
40 | 103k | let ChildOfElement::Text(t102k ) = e.children()[0] { |
41 | 102k | return t.text().to_string(); |
42 | 23.4k | } |
43 | 23.4k | return "".to_string(); |
44 | 126k | } |
45 | | |
46 | | #[allow(non_snake_case)] |
47 | | // Same as 'is_tag', but for ChildOfElement |
48 | 110k | fn get_text_from_COE(coe: &ChildOfElement) -> String { |
49 | 110k | coe.element().map_or_else(String::new, get_text_from_element) |
50 | 110k | } |
51 | | |
52 | | // make sure that there is only one node in the NodeSet |
53 | | // Returns the node or an Error |
54 | 147k | pub fn validate_one_node<'n>(nodes: Nodeset<'n>, func_name: &str) -> Result<Node<'n>, Error> { |
55 | 147k | if nodes.size() == 0 { |
56 | 0 | return Err(Error::Other(format!("Missing argument for {func_name}"))); |
57 | 147k | } else if nodes.size() > 1 { |
58 | 0 | return Err( Error::Other(format!("{} arguments for {}; expected 1 argument", nodes.size(), func_name)) ); |
59 | 147k | } |
60 | 147k | return Ok( nodes.iter().next().unwrap() ); |
61 | 147k | } |
62 | | |
63 | | // Return true if the element's name is 'name' |
64 | 157k | fn is_tag(e: Element, name: &str) -> bool { |
65 | | // need to check name before the fallback of where the name came from |
66 | 157k | return e.name().local_part() == name || e47.8k .attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or_default() == name; |
67 | 157k | } |
68 | | |
69 | | #[allow(non_snake_case)] |
70 | | // Same as 'is_tag', but for ChildOfElement |
71 | 1.40k | fn is_COE_tag(coe: ChildOfElement, name: &str) -> bool { |
72 | 1.40k | coe.element().is_some_and(|element| is_tag(element, name)) |
73 | 1.40k | } |
74 | | |
75 | | /// Should be an internal structure for implementation of the IsNode, but it was useful in one place in a separate module. |
76 | | /// This should probably be restructured slightly. |
77 | | pub struct IsNode; |
78 | | |
79 | | impl IsNode { |
80 | | /// implements ClearSpeak's definition of "simple" |
81 | | /// this is fairly detailed, so we define a few local functions (at end) to help out |
82 | | /// Also, it doesn't help that the structure is a bit complicated Elements->ChildOfElement->Element/Text |
83 | 7.43k | pub fn is_simple(elem: Element) -> bool { |
84 | 7.43k | if is_trivially_simple(elem) { |
85 | 3.62k | return true; |
86 | 3.81k | } |
87 | | |
88 | 3.81k | if is_negative_of_trivially_simple(elem) { |
89 | | // -3 or -x |
90 | 41 | return true; |
91 | 3.76k | } |
92 | | |
93 | 3.76k | if !is_tag(elem, "mrow") || elem.children()867 .is_empty867 () { |
94 | 2.90k | return false; |
95 | 867 | } |
96 | | |
97 | | // x y or -x or -3 x or -x y or -3 x y or x° or n° or -x° or -n° |
98 | | #[allow(clippy::if_same_then_else)] |
99 | 867 | if is_times_mi(elem) { |
100 | 42 | return true; // x y |
101 | 825 | } else if is_degrees(elem) { |
102 | 0 | return true; // x° or n° |
103 | 825 | } else if is_function(elem) { |
104 | 44 | return true; |
105 | 781 | } |
106 | | |
107 | 781 | return false; |
108 | | |
109 | | |
110 | | // returns the element's text value |
111 | 5.71k | fn to_str(e: Element<'_>) -> &str { |
112 | | // typically usage assumes 'e' is a leaf |
113 | | // bad MathML is the following isn't true |
114 | 5.71k | if e.children().len() == 1 { |
115 | 5.71k | let text_node = e.children()[0]; |
116 | 5.71k | if let Some(t) = text_node.text() { |
117 | 5.71k | return t.text(); |
118 | 0 | } |
119 | 0 | } |
120 | 0 | return ""; |
121 | 5.71k | } |
122 | | |
123 | | // same as 'to_str' but for ChildOfElement |
124 | 1.01k | fn coe_to_str(coe: ChildOfElement<'_>) -> &str { |
125 | | // typically usage assumes 'coe' is a leaf |
126 | 1.01k | let element_node = coe.element(); |
127 | 1.01k | if let Some(e) = element_node { |
128 | | // bad MathML is the following isn't true |
129 | 1.01k | if e.children().len() == 1 { |
130 | 1.01k | let text_node = e.children()[0]; |
131 | 1.01k | if let Some(t) = text_node.text() { |
132 | 1.01k | return t.text(); |
133 | 0 | } |
134 | 8 | } |
135 | 0 | } |
136 | 8 | return ""; |
137 | 1.01k | } |
138 | | |
139 | | // returns true if the string is just a single *char* (which can be multiple bytes) |
140 | 5.71k | fn is_single_char(str: &str) -> bool { |
141 | 5.71k | let mut chars = str.chars(); |
142 | 5.71k | return chars.next().is_some() && chars.next().is_none(); |
143 | 5.71k | } |
144 | | |
145 | | // checks the single element to see if it is simple (mn, mi that is a single char, common fraction) |
146 | 8.33k | fn is_trivially_simple(elem: Element) -> bool { |
147 | 8.33k | if is_tag(elem, "mn") { |
148 | 914 | return true; |
149 | 7.41k | } |
150 | 7.41k | if is_tag(elem, "mi") && is_single_char5.71k (to_str(elem)5.71k ) { |
151 | | // "simple" only if it is a single char (which can be multiple bytes) |
152 | 3.14k | return true; |
153 | 4.27k | } |
154 | | |
155 | | // FIX: need to consult preference Fraction_Ordinal |
156 | 4.27k | if IsNode::is_common_fraction(elem, 10, 19) { |
157 | 66 | return true; |
158 | 4.21k | } |
159 | 4.21k | return false; |
160 | 8.33k | } |
161 | | |
162 | | // true if the negative of a single element that is simple |
163 | 4.20k | fn is_negative_of_trivially_simple(elem: Element) -> bool { |
164 | 4.20k | if is_tag(elem, "mrow") && elem.children().len() == 2933 { |
165 | 38 | let children = elem.children(); |
166 | | // better be negative of something at this point... |
167 | 38 | if is_COE_tag(children[0], "mo") && is_equal11 (children[0]11 , '-') && |
168 | 6 | children[1].element().is_some() && is_trivially_simple(children[1].element().unwrap()) { |
169 | 6 | return true; |
170 | 32 | } |
171 | 4.16k | } |
172 | 4.20k | if is_tag(elem, "minus") && elem.children().len() == 154 { |
173 | 54 | let child = elem.children()[0]; |
174 | 54 | if let Some(e) = child.element() { |
175 | 54 | return is_trivially_simple(e); |
176 | 0 | } |
177 | 4.14k | } |
178 | | |
179 | 4.14k | return false; |
180 | 4.20k | } |
181 | | |
182 | | // return true if ChildOfElement has exactly text 'ch' |
183 | 967 | fn is_equal(coe: ChildOfElement, ch: char) -> bool { |
184 | 967 | return coe_to_str(coe).starts_with(ch); |
185 | 967 | } |
186 | | |
187 | | // true if mrow(xxx, ⁢, mi) or mrow(xxx, ⁢ mi, ⁢, mi) where mi's have len==1 |
188 | 867 | fn is_times_mi(mrow: Element) -> bool { |
189 | 867 | assert!( is_tag(mrow, "mrow") ); |
190 | 867 | let children = mrow.children(); |
191 | 867 | if !(children.len() == 3 || children.len() == 541 ) { |
192 | 34 | return false; |
193 | 833 | } |
194 | 833 | if children[0].element().is_none() { |
195 | 0 | return false; |
196 | 833 | } |
197 | | |
198 | 833 | let first_child = children[0].element().unwrap(); |
199 | 833 | if !is_trivially_simple(first_child) { |
200 | 396 | if !is_negative_of_trivially_simple(first_child) { |
201 | 382 | return false; |
202 | 14 | } |
203 | 14 | if children.len() == 5 && |
204 | 2 | ( (name(first_child) == "minus" && first_child.children().len() == 10 && !0 is_COE_tag0 (first_child.children()[0], "mn")) || |
205 | 2 | (name(first_child) == "mrow" && !is_COE_tag(first_child.children()[1], "mn")) ) { |
206 | 1 | return false; // '-x y z' is too complicated () -- -2 x y is ok |
207 | 13 | } |
208 | 437 | } |
209 | | |
210 | 450 | if !(is_COE_tag(children[1], "mo") && |
211 | 450 | is_equal(children[1], '\u{2062}') && |
212 | 63 | is_COE_tag(children[2], "mi") && |
213 | 51 | coe_to_str(children[2]).len()==1 ) { |
214 | 408 | return false; |
215 | 42 | } |
216 | | |
217 | 42 | if children.len() == 3 { |
218 | 41 | return true; |
219 | 1 | } |
220 | | |
221 | | // len == 5 |
222 | 1 | return is_COE_tag(children[3], "mo") && |
223 | 1 | is_equal(children[3], '\u{2062}') && // invisible times |
224 | 1 | is_COE_tag(children[4], "mi") && |
225 | 1 | coe_to_str(children[4]).len()==1 ; |
226 | 867 | } |
227 | | |
228 | | // return true if the mrow is var° or num° |
229 | 825 | fn is_degrees(mrow: Element) -> bool { |
230 | 825 | assert!( is_tag(mrow, "mrow") ); |
231 | 825 | let children = mrow.children(); |
232 | 825 | return children.len() == 2 && |
233 | 32 | is_equal(children[1], '°') && |
234 | 0 | (is_COE_tag(children[0], "mi") || |
235 | 0 | is_COE_tag(children[0], "mn") ); |
236 | 825 | } |
237 | | |
238 | | // fn_name ⁡ [simple arg or (simple arg)] |
239 | 825 | fn is_function(mrow: Element) -> bool { |
240 | 825 | assert!( is_tag(mrow, "mrow") ); |
241 | 825 | let children = mrow.children(); |
242 | 825 | if children.len() != 3 { |
243 | 40 | return false; |
244 | 785 | } |
245 | 785 | if !(is_COE_tag(children[1], "mo") && |
246 | 473 | is_equal(children[1], '\u{2061}') ) { // invisible function application |
247 | 717 | return false; |
248 | 68 | } |
249 | 68 | if !is_COE_tag(children[0], "mi") { |
250 | 0 | return false; |
251 | 68 | } |
252 | 68 | let function_arg = children[2].element().unwrap(); |
253 | 68 | if IsBracketed::is_bracketed(function_arg, "(", ")", false, false) { |
254 | 60 | return IsNode::is_simple(function_arg.children()[1].element().unwrap()); |
255 | | } else { |
256 | 8 | return IsNode::is_simple(function_arg); |
257 | | } |
258 | 825 | } |
259 | 7.43k | } |
260 | | |
261 | | // Returns true if 'frac' is a common fraction |
262 | | // In this case, the numerator and denominator can be no larger than 'num_limit' and 'denom_limit' |
263 | 4.31k | fn is_common_fraction(frac: Element, num_limit: usize, denom_limit: usize) -> bool { |
264 | 2 | static ALL_DIGITS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\d+").unwrap()); // match one or more digits |
265 | | |
266 | 4.31k | if !is_tag(frac, "mfrac") && !4.12k is_tag4.12k (frac, "fraction"){ |
267 | 4.12k | return false; |
268 | 188 | } |
269 | 188 | let children = frac.children(); |
270 | 188 | if children.len() != 2 { |
271 | 0 | return false; |
272 | 188 | } |
273 | | |
274 | 188 | let num = children[0].element(); |
275 | 188 | let denom = children[1].element(); |
276 | 188 | if num.is_none() || denom.is_none() { |
277 | 0 | return false; |
278 | 188 | }; |
279 | | |
280 | 188 | let num = num.unwrap(); |
281 | 188 | let denom = denom.unwrap(); |
282 | 188 | if !is_tag(num, "mn") || !115 is_tag115 (denom, "mn") { |
283 | 87 | return false |
284 | 101 | }; |
285 | | |
286 | 101 | let num = get_text_from_element(num); |
287 | 101 | let denom = get_text_from_element(denom); |
288 | 101 | if num.is_empty() || denom.is_empty() { |
289 | 0 | return false; |
290 | 101 | } |
291 | | |
292 | 101 | return ALL_DIGITS.is_match(&num) && is_small_enough(&num, num_limit) && |
293 | 100 | ALL_DIGITS.is_match(&denom) && is_small_enough(&denom, denom_limit); |
294 | | |
295 | 201 | fn is_small_enough(val: &str, upper_bound: usize) -> bool { |
296 | 201 | return if let Ok(value) = val.parse::<usize>() { value <= upper_bound } else { false0 }; |
297 | 201 | } |
298 | 4.31k | } |
299 | | |
300 | 14.2k | pub fn is_mathml(elem: Element) -> bool { |
301 | | // doesn't check MATHML_FROM_NAME_ATTR because we are interested in if it is an intent. |
302 | 14.2k | return ALL_MATHML_ELEMENTS.contains(name(elem)); |
303 | 14.2k | } |
304 | | |
305 | | #[allow(non_snake_case)] |
306 | 14.3k | pub fn is_2D(elem: Element) -> bool { |
307 | 14.3k | return MATHML_2D_NODES.contains(elem.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(elem))); |
308 | 14.3k | } |
309 | | |
310 | 37.8k | pub fn is_scripted(elem: Element) -> bool { |
311 | 37.8k | return MATHML_SCRIPTED_NODES.contains(elem.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(elem))); |
312 | 37.8k | } |
313 | | |
314 | 138k | pub fn is_modified(elem: Element) -> bool { |
315 | 138k | return MATHML_MODIFIED_NODES.contains(elem.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(elem))); |
316 | 138k | } |
317 | | } |
318 | | |
319 | | /// All MathML elements, including a few that get cleaned away |
320 | | /// "semantics", "annotation-xml", "annotation" and Content MathML are not included |
321 | | static ALL_MATHML_ELEMENTS: phf::Set<&str> = phf_set!{ |
322 | | "mi", "mo", "mn", "mtext", "ms", "mspace", "mglyph", |
323 | | "mfrac", "mroot", "msub", "msup", "msubsup","munder", "mover", "munderover", "mmultiscripts", |
324 | | "mstack", "mlongdiv", "msgroup", "msrow", "mscarries", "mscarry", "msline", |
325 | | "none", "mprescripts", "malignmark", "maligngroup", |
326 | | "math", "msqrt", "merror", "mpadded", "mphantom", "menclose", "mtd", "mstyle", |
327 | | "mrow", "a", "mfenced", "mtable", "mtr", "mlabeledtr", |
328 | | }; |
329 | | |
330 | | static MATHML_LEAF_NODES: phf::Set<&str> = phf_set! { |
331 | | "mi", "mo", "mn", "mtext", "ms", "mspace", "mglyph", |
332 | | "none", "annotation", "ci", "cn", "csymbol", // content could be inside an annotation-xml (faster to allow here than to check lots of places) |
333 | | }; |
334 | | |
335 | | |
336 | | // Should mstack and mlongdiv be included here? |
337 | | static MATHML_2D_NODES: phf::Set<&str> = phf_set! { |
338 | | "mfrac", "msqrt", "mroot", "menclose", |
339 | | "msub", "msup", "msubsup", "munder", "mover", "munderover", "mmultiscripts", |
340 | | "mtable", "mtr", "mlabeledtr", "mtd", |
341 | | }; |
342 | | |
343 | | // Should mstack and mlongdiv be included here? |
344 | | static MATHML_MODIFIED_NODES: phf::Set<&str> = phf_set! { |
345 | | "msub", "msup", "msubsup", "munder", "mover", "munderover", "mmultiscripts", |
346 | | }; |
347 | | |
348 | | // Should mstack and mlongdiv be included here? |
349 | | static MATHML_SCRIPTED_NODES: phf::Set<&str> = phf_set! { |
350 | | "msub", "msup", "msubsup", "mmultiscripts", |
351 | | }; |
352 | | |
353 | 1.07M | pub fn is_leaf(element: Element) -> bool { |
354 | 1.07M | return MATHML_LEAF_NODES.contains(name(element)); |
355 | 1.07M | } |
356 | | |
357 | | impl Function for IsNode { |
358 | | // eval function for IsNode |
359 | | // errors happen for wrong number/kind of arg |
360 | 5.12k | fn evaluate<'d>(&self, |
361 | 5.12k | _context: &context::Evaluation<'_, 'd>, |
362 | 5.12k | args: Vec<Value<'d>>) |
363 | 5.12k | -> Result<Value<'d>, Error> |
364 | | { |
365 | | |
366 | 5.12k | let mut args = Args(args); |
367 | 5.12k | args.exactly(2)?0 ; |
368 | 5.12k | let kind = args.pop_string()?0 ; |
369 | | // FIX: there is some conflict problem with xpath errors and error-chain |
370 | | // .chain_err(|e| format!("Second arg to is_leaf is not a string: {}", e.to_string()))?; |
371 | 5.12k | match kind.as_str() { |
372 | 5.12k | "simple" | "leaf"3.09k | "common_fraction"849 | "2D"849 | "modified"162 | "scripted"140 | "mathml"49 => (), |
373 | 0 | _ => return Err( Error::Other(format!("Unknown argument value '{}' for IsNode", kind.as_str())) ), |
374 | | }; |
375 | | |
376 | 5.12k | let nodes = args.pop_nodeset()?0 ; |
377 | 5.12k | if nodes.size() == 0 { |
378 | 0 | return Ok (Value::Boolean(false)); // like xpath, don't make this an error |
379 | 5.12k | }; |
380 | | return Ok( |
381 | | Value::Boolean( |
382 | 5.12k | nodes.iter() |
383 | 5.12k | .all(|node| |
384 | 5.39k | if let Node::Element(e) = node { |
385 | 5.39k | match kind.as_str() { |
386 | 5.39k | "simple" => IsNode::is_simple2.29k (e2.29k ), |
387 | 3.09k | "leaf" => is_leaf_any_name2.25k (e2.25k ), |
388 | 849 | "2D" => IsNode::is_2D687 (e687 ), |
389 | 162 | "modified" => IsNode::is_modified22 (e22 ), |
390 | 140 | "scripted" => IsNode::is_scripted91 (e91 ), |
391 | 49 | "mathml" => IsNode::is_mathml(e), |
392 | 0 | "common_fraction" => IsNode::is_common_fraction(e, usize::MAX, usize::MAX), |
393 | 0 | _ => true, // can't happen due to check above |
394 | | } |
395 | | } else { |
396 | | // xpath is something besides an element, so no match |
397 | 0 | false |
398 | 5.39k | } |
399 | | ) |
400 | | ) |
401 | | ); |
402 | | |
403 | 2.25k | fn is_leaf_any_name(e: Element) -> bool { |
404 | 2.25k | let children = e.children(); |
405 | 2.25k | if children.is_empty() { |
406 | 0 | return true; |
407 | 2.25k | } else if children.len() == 1 && |
408 | 1.24k | let ChildOfElement::Text(_) = children[0] { |
409 | 1.17k | return true; |
410 | 1.07k | } |
411 | 1.07k | return false |
412 | 2.25k | } |
413 | 5.12k | } |
414 | | } |
415 | | |
416 | | struct ToOrdinal; |
417 | | impl ToOrdinal { |
418 | | // ordinals often have an irregular start (e.g., "half") before becoming regular. |
419 | | // if the number is irregular, return the ordinal form, otherwise return 'None'. |
420 | 353 | fn compute_irregular_fractional_speech(number: &str, plural: bool) -> Option<String> { |
421 | 353 | SPEECH_DEFINITIONS.with(|definitions| { |
422 | 353 | let definitions = definitions.borrow(); |
423 | 353 | let words = if plural { |
424 | 208 | definitions.get_vec("NumbersOrdinalFractionalPluralOnes")?0 |
425 | | } else { |
426 | 145 | definitions.get_vec("NumbersOrdinalFractionalOnes")?0 |
427 | | }; |
428 | 353 | let number_as_int: usize = number.parse().unwrap(); // already verified it is only digits |
429 | 353 | if number_as_int < words.len() { |
430 | | // use the words associated with this irregular pattern. |
431 | 291 | return Some( words[number_as_int].clone() ); |
432 | 62 | }; |
433 | 62 | return None; |
434 | 353 | }) |
435 | 353 | } |
436 | | |
437 | | /** |
438 | | * Translates a number of up to twelve digits into a string representation. |
439 | | * number -- the number to translate |
440 | | * fractional -- true if this is a fractional ordinal (e.g, "half") |
441 | | * plural -- true if answer should be plural |
442 | | * Returns the string representation of that number or an error message |
443 | | */ |
444 | 416 | fn convert(number: &str, fractional: bool, plural: bool) -> Option<String> { |
445 | 2 | static NO_DIGIT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^\d]").unwrap()); // match anything except a digit |
446 | 416 | return SPEECH_DEFINITIONS.with(|definitions| { |
447 | 416 | let definitions = definitions.borrow(); |
448 | 416 | let numbers_large = definitions.get_vec("NumbersLarge")?0 ; |
449 | | |
450 | 416 | let pref_manager = crate::prefs::PreferenceManager::get(); |
451 | 416 | let pref_manager = pref_manager.borrow(); |
452 | 416 | let block_separators = pref_manager.pref_to_string("BlockSeparators"); |
453 | 416 | let decimal_separator = pref_manager.pref_to_string("DecimalSeparators"); |
454 | | // check number validity (has digits, not a decimal) |
455 | 416 | if number.is_empty() || number.contains(&decimal_separator) { |
456 | 0 | return Some(String::from(number)); |
457 | 416 | } |
458 | | // remove any block separators |
459 | 416 | let number = match clean_number(number, &block_separators) { |
460 | 0 | None => return Some(String::from(number)), |
461 | 416 | Some(num) => num, |
462 | | }; |
463 | | |
464 | | // check to see if the number is too big or is not an integer or has non-digits |
465 | 416 | if number.len() > 3*numbers_large.len() { |
466 | 0 | return Some(number); |
467 | 416 | } |
468 | 416 | if NO_DIGIT.is_match(&number) { |
469 | | // this shouldn't have been part of an mn, so likely an error. Log a warning |
470 | | // FIX: log a warning that a non-number was passed to convert() |
471 | 0 | return Some(number); |
472 | 416 | } |
473 | | |
474 | | // first deal with the abnormalities of fractional ordinals (one half, etc). That simplifies what remains |
475 | 416 | if fractional && |
476 | 353 | let Some(string291 ) = ToOrdinal::compute_irregular_fractional_speech(&number, plural) { |
477 | 291 | return Some(string); |
478 | 125 | } |
479 | | |
480 | | // at this point, we only need to worry about singular/plural distinction |
481 | | |
482 | | // break into groups of three digits and add 10^3 word (thousands, millions, ...) after each chunk |
483 | | // FIX: add a pause between groups of three -- need to use TTS-specific pause |
484 | | |
485 | | // handle special case of trailing zeros |
486 | | // num_thousands_at_end represents the amount to shift NumbersLarge... (e.g., millions->thousands) |
487 | 243 | let num_thousands_at_end125 = match number125 .rfind125 (|ch| ch > '0') { // last non-0 on right |
488 | 122 | Some(n) => (number.len() - 1 - n) / 3 , |
489 | 3 | None => 0 |
490 | | }; |
491 | 125 | let (number,_) = number.split_at(number.len() - 3 * num_thousands_at_end); // drop the 0s |
492 | | |
493 | | // everything is simplified if we add zeros at the start so that block size is a factor of 3 |
494 | 125 | let number = match number.len() % 3 { |
495 | 18 | 0 => "".to_string() + number, |
496 | 69 | 1 => "00".to_string() + number, |
497 | 38 | _ => "0".to_string() + number, // can only be "2" -- compiler doesn't know there aren't other options |
498 | | }; |
499 | | |
500 | | // At this point we have at least three "digits", and length is a multiple of 3 |
501 | | // We have already verified that there are only ASCII digits, so we can subtract '0' to get an index |
502 | | const ASCII_0: usize = 48; |
503 | 125 | let digits = number.as_bytes() |
504 | 125 | .iter() |
505 | 411 | .map125 (|&byte| byte as usize - ASCII_0) |
506 | 125 | .collect::<Vec<usize>>(); |
507 | | |
508 | 125 | let mut answer = String::with_capacity(255); // reasonable max most of the time |
509 | 125 | let large_words = numbers_large; |
510 | 125 | if digits.len() > 3 { |
511 | | // speak this first groups as cardinal numbers |
512 | 7 | let words = [ |
513 | 7 | definitions.get_vec("NumbersHundreds")?0 , |
514 | 7 | definitions.get_vec("NumbersTens")?0 , |
515 | 7 | definitions.get_vec("NumbersOnes")?0 , |
516 | | ]; |
517 | 7 | answer = digits[0..digits.len()-3] |
518 | 7 | .chunks(3) |
519 | 7 | .enumerate() |
520 | 12 | .map7 (|(i, chunk)| { |
521 | 12 | if chunk[0] != 0 || chunk[1] != 08 || chunk[2] != 08 { |
522 | 7 | Some(ToOrdinal::hundreds_to_words(chunk, &words)?0 + " " + |
523 | 7 | &large_words[num_thousands_at_end + digits.len()/3 - 1 - i] + " ") |
524 | | } else { |
525 | 5 | Some("".to_string()) |
526 | | } |
527 | 12 | }) |
528 | 7 | .collect::<Option<Vec<String>>>()?0 |
529 | 7 | .join(""); // can't use " " because 1000567 would get extra space in the middle |
530 | 7 | if num_thousands_at_end > 0 { |
531 | | // add on "billionths", etc and we are done |
532 | 0 | let large_words = if plural { |
533 | 0 | definitions.get_vec("NumbersOrdinalPluralLarge") |
534 | | } else { |
535 | 0 | definitions.get_vec("NumbersOrdinalLarge") |
536 | | }; |
537 | 0 | return Some(answer + &large_words?[num_thousands_at_end]); |
538 | 7 | } |
539 | 118 | }; |
540 | | |
541 | | // all that is left is to speak the hundreds part, possibly followed by "thousands", "billions", etc |
542 | 125 | let words = match (num_thousands_at_end > 0, plural) { |
543 | | (true, _) => [ |
544 | 10 | definitions.get_vec("NumbersHundreds")?0 , |
545 | 10 | definitions.get_vec("NumbersTens")?0 , |
546 | 10 | definitions.get_vec("NumbersOnes")?0 , |
547 | | ], |
548 | | (false, true) => [ |
549 | 54 | definitions.get_vec("NumbersOrdinalPluralHundreds")?0 , |
550 | 54 | definitions.get_vec("NumbersOrdinalPluralTens")?0 , |
551 | 54 | definitions.get_vec("NumbersOrdinalPluralOnes")?0 , |
552 | | ], |
553 | | (false, false) => [ |
554 | 61 | definitions.get_vec("NumbersOrdinalHundreds")?0 , |
555 | 61 | definitions.get_vec("NumbersOrdinalTens")?0 , |
556 | 61 | definitions.get_vec("NumbersOrdinalOnes")?0 , |
557 | | ], |
558 | | }; |
559 | 125 | answer += &ToOrdinal::hundreds_to_words(&digits[digits.len()-3..], &words)?0 ; |
560 | 125 | if num_thousands_at_end > 0 { |
561 | 10 | let large_words = if plural { |
562 | 3 | definitions.get_vec("NumbersOrdinalPluralLarge")?0 |
563 | | } else { |
564 | 7 | definitions.get_vec("NumbersOrdinalLarge")?0 |
565 | | }; |
566 | 10 | answer = answer + " " + &large_words[num_thousands_at_end]; |
567 | 115 | } |
568 | 125 | return Some(answer); |
569 | 416 | }); |
570 | | |
571 | | /// Remove block separators and convert alphanumeric digits to ascii digits |
572 | 416 | fn clean_number(number: &str, block_separators: &str) -> Option<String> { |
573 | 416 | let mut answer = String::with_capacity(number.len()); |
574 | 617 | for ch in number416 .chars416 () { |
575 | 617 | if block_separators.contains(ch) { |
576 | 0 | continue; |
577 | 617 | } |
578 | 617 | if ch.is_ascii_digit() { |
579 | 615 | answer.push(ch); |
580 | 615 | } else { |
581 | 2 | let shifted_ch = match ch { |
582 | 2 | '𝟎'..='𝟗' => ch as u32 -'𝟎' as u32 + '0' as u32, |
583 | 0 | '𝟘'..='𝟡' => ch as u32 -'𝟘' as u32 + '0' as u32, |
584 | 0 | '𝟢'..='𝟫' => ch as u32 -'𝟢' as u32 + '0' as u32, |
585 | 0 | '𝟬'..='𝟵' => ch as u32 -'𝟬' as u32 + '0' as u32, |
586 | 0 | '𝟶'..='𝟿' => ch as u32 -'𝟶' as u32 + '0' as u32, |
587 | 0 | _ => return None, |
588 | | }; |
589 | 2 | answer.push(char::from_u32(shifted_ch).unwrap()); |
590 | | } |
591 | | } |
592 | 416 | return Some(answer); |
593 | 416 | } |
594 | 416 | } |
595 | | |
596 | | |
597 | 132 | fn hundreds_to_words(number: &[usize], words: &[Ref<Vec<String>>; 3]) -> Option<String> { |
598 | 132 | assert!( number.len() == 3 ); |
599 | 132 | return SPEECH_DEFINITIONS.with(|definitions| { |
600 | 132 | let definitions = definitions.borrow(); |
601 | 132 | if number[0] != 0 && number[1] == 024 && number[2] == 012 { |
602 | 6 | return Some(words[0][number[0]].clone()); |
603 | 126 | } |
604 | | |
605 | 126 | let mut hundreds = definitions.get_vec("NumbersHundreds")?0 [number[0]].clone(); |
606 | 126 | if !hundreds.is_empty() { |
607 | 18 | hundreds += " "; |
608 | 108 | } |
609 | | |
610 | 126 | if number[1] != 0 && number[2] == 049 { |
611 | 26 | return Some(hundreds + &words[1][number[1]]); |
612 | 100 | } |
613 | | |
614 | 100 | if 10*number[1] < words[2].len() { |
615 | | // usurp regular ordering to handle something like '14' |
616 | 85 | return Some(hundreds + &words[2][10*number[1] + number[2]]); |
617 | | } else { |
618 | 15 | return Some(hundreds + &definitions.get_vec("NumbersTens")?0 [number[1]] + " " + &words[2][number[2]]); |
619 | | } |
620 | 132 | }); |
621 | 132 | } |
622 | | } |
623 | | |
624 | | impl Function for ToOrdinal { |
625 | | // convert a node to an ordinal number |
626 | 320 | fn evaluate<'d>(&self, |
627 | 320 | _context: &context::Evaluation<'_, 'd>, |
628 | 320 | args: Vec<Value<'d>>) |
629 | 320 | -> Result<Value<'d>, Error> |
630 | | { |
631 | 320 | let mut args = Args(args); |
632 | 320 | if let Err(e0 ) = args.exactly(1).or_else(|_| args288 .exactly288 (3)) { |
633 | 0 | return Err( XPathError::Other(format!("ToOrdinal requires 1 or 3 args: {e}"))); |
634 | 320 | }; |
635 | 320 | let mut fractional = false; |
636 | 320 | let mut plural = false; |
637 | 320 | if args.len() == 3 { |
638 | 288 | plural = args.pop_boolean()?0 ; |
639 | 288 | fractional = args.pop_boolean()?0 ; |
640 | 32 | } |
641 | 320 | let node = validate_one_node(args.pop_nodeset()?0 , "ToOrdinal")?0 ; |
642 | 320 | return match node { |
643 | 0 | Node::Text(t) => Ok( Value::String( |
644 | 0 | match ToOrdinal::convert(t.text(), fractional, plural) { |
645 | 0 | None => t.text().to_string(), |
646 | 0 | Some(ord) => ord, |
647 | | } ) ), |
648 | 320 | Node::Element(e) => Ok( Value::String( |
649 | 320 | match ToOrdinal::convert(&get_text_from_element(e), fractional, plural) { |
650 | 0 | None => get_text_from_element(e).to_string(), |
651 | 320 | Some(ord) => ord, |
652 | | } ) ), |
653 | 0 | _ => Err( Error::ArgumentNotANodeset{actual: ArgumentType::String} ), |
654 | | } |
655 | 320 | } |
656 | | } |
657 | | |
658 | | |
659 | | struct ToCommonFraction; |
660 | | |
661 | | impl Function for ToCommonFraction { |
662 | | // convert a node to a common fraction (if the numerator and denominator are within given limits) |
663 | 34 | fn evaluate<'d>(&self, |
664 | 34 | _context: &context::Evaluation<'_, 'd>, |
665 | 34 | args: Vec<Value<'d>>) |
666 | 34 | -> Result<Value<'d>, Error> |
667 | | { |
668 | 34 | let mut args = Args(args); |
669 | 34 | args.exactly(1)?0 ; |
670 | | |
671 | | // FIX: should probably handle errors by logging them and then trying to evaluate any children |
672 | 34 | let node = validate_one_node(args.pop_nodeset()?0 , "ToCommonFraction")?0 ; |
673 | 34 | if let Node::Element(frac) = node { |
674 | 34 | if !IsNode::is_common_fraction(frac, usize::MAX, usize::MAX) { |
675 | 0 | return Err( Error::Other( format!("ToCommonFraction -- argument is not an 'mfrac': {}': ", mml_to_string(frac))) ); |
676 | 34 | } |
677 | | |
678 | | // everything has been verified, so we can just get the pieces and ignore potential error results |
679 | 34 | let children = frac.children(); |
680 | 34 | let num = children[0].element().unwrap(); |
681 | 34 | let num = get_text_from_element( num ); |
682 | 34 | let denom = children[1].element().unwrap(); |
683 | 34 | let denom = get_text_from_element( denom ); |
684 | 34 | let mut answer = num.clone() + " "; |
685 | 34 | answer += &match ToOrdinal::convert(&denom, true, num!="1") { |
686 | 0 | None => denom, |
687 | 34 | Some(ord) => ord, |
688 | | }; |
689 | | |
690 | 34 | return Ok( Value::String( answer ) ) |
691 | | } else { |
692 | 0 | return Err( Error::Other( "ToCommonFraction -- argument is not an element".to_string()) ); |
693 | | } |
694 | 34 | } |
695 | | } |
696 | | |
697 | | struct Min; |
698 | | /** |
699 | | * Returns true the smallest of the two args |
700 | | * @param(num1) |
701 | | * @param(num2) |
702 | | */ |
703 | | impl Function for Min { |
704 | | |
705 | 0 | fn evaluate<'d>(&self, |
706 | 0 | _context: &context::Evaluation<'_, 'd>, |
707 | 0 | args: Vec<Value<'d>>) |
708 | 0 | -> Result<Value<'d>, Error> |
709 | | { |
710 | 0 | let mut args = Args(args); |
711 | 0 | args.exactly(2)?; |
712 | 0 | let num1 = args.pop_number()?; |
713 | 0 | let num2 = args.pop_number()?; |
714 | 0 | return Ok( Value::Number( num1.min(num2) ) ); |
715 | 0 | } |
716 | | } |
717 | | |
718 | | struct Max; |
719 | | |
720 | | impl Function for Max { |
721 | | |
722 | 0 | fn evaluate<'d>(&self, |
723 | 0 | _context: &context::Evaluation<'_, 'd>, |
724 | 0 | args: Vec<Value<'d>>) |
725 | 0 | -> Result<Value<'d>, Error> |
726 | | { |
727 | 0 | let mut args = Args(args); |
728 | 0 | args.exactly(2)?; |
729 | 0 | let num1 = args.pop_number()?; |
730 | 0 | let num2 = args.pop_number()?; |
731 | 0 | return Ok( Value::Number( num1.max(num2) ) ); |
732 | 0 | } |
733 | | } |
734 | | |
735 | | |
736 | | struct BaseNode; |
737 | | /** |
738 | | * Returns true if the node is a large op |
739 | | * @param(node) -- node(s) to test -- should be an <mo> |
740 | | */ |
741 | | impl BaseNode { |
742 | | /// Recursively find the base node |
743 | | /// The base node of a non scripted element is the element itself |
744 | 1.26k | fn base_node(node: Element) -> Element { |
745 | 1.26k | let name = node.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(node)); |
746 | 1.26k | if ["msub", "msup", "msubsup", "munder", "mover", "munderover", "mmultiscripts"].contains(&name) { |
747 | 97 | return BaseNode::base_node(as_element(node.children()[0])); |
748 | | } else { |
749 | 1.16k | return node; |
750 | | } |
751 | 1.26k | } |
752 | | } |
753 | | impl Function for BaseNode { |
754 | | |
755 | 1.16k | fn evaluate<'d>(&self, |
756 | 1.16k | _context: &context::Evaluation<'_, 'd>, |
757 | 1.16k | args: Vec<Value<'d>>) |
758 | 1.16k | -> Result<Value<'d>, Error> |
759 | | { |
760 | 1.16k | let mut args = Args(args); |
761 | 1.16k | args.exactly(1)?0 ; |
762 | 1.16k | let node = validate_one_node(args.pop_nodeset()?0 , "BaseNode")?0 ; |
763 | 1.16k | if let Node::Element(e) = node { |
764 | 1.16k | let mut node_set = Nodeset::new(); |
765 | 1.16k | node_set.add(BaseNode::base_node(e)); |
766 | 1.16k | return Ok( Value::Nodeset(node_set) ); |
767 | | } else { |
768 | | // xpath is something besides an element, so no match |
769 | 0 | return Err( Error::Other("Argument other than a node given to BaseNode".to_string()) ); |
770 | | } |
771 | 1.16k | } |
772 | | } |
773 | | |
774 | | |
775 | | struct IfThenElse; |
776 | | impl Function for IfThenElse { |
777 | 36.2k | fn evaluate<'d>(&self, |
778 | 36.2k | _context: &context::Evaluation<'_, 'd>, |
779 | 36.2k | args: Vec<Value<'d>>) |
780 | 36.2k | -> Result<Value<'d>, Error> |
781 | | { |
782 | 36.2k | let args = Args(args); |
783 | 36.2k | args.exactly(3)?0 ; |
784 | 36.2k | let if_val = &args[0]; |
785 | 36.2k | let then_val = &args[1]; |
786 | 36.2k | let else_val = &args[2]; |
787 | 36.2k | let is_true = match if_val { |
788 | 14.5k | Value::Nodeset(nodes) => nodes.size() > 0, |
789 | 21.7k | Value::Boolean(b) => *b, |
790 | 0 | Value::Number(f) => *f != 0.0, |
791 | 0 | Value::String(s) => !s.is_empty(), |
792 | | }; |
793 | 36.2k | return Ok( if is_true {then_val4.13k .clone4.13k ()} else {else_val32.1k .clone32.1k ()}); |
794 | 36.2k | } |
795 | | } |
796 | | |
797 | | |
798 | | struct Debug; |
799 | | /** |
800 | | * Prints it's argument along with the string that was evaluated |
801 | | * @param(node) -- node(s) to be evaluated/printed |
802 | | * @param(string) -- string showing what is being evaluated |
803 | | */ |
804 | | impl Function for Debug { |
805 | | |
806 | 348 | fn evaluate<'d>(&self, |
807 | 348 | _context: &context::Evaluation<'_, 'd>, |
808 | 348 | args: Vec<Value<'d>>) |
809 | 348 | -> Result<Value<'d>, Error> |
810 | | { |
811 | 348 | let mut args = Args(args); |
812 | 348 | args.exactly(2)?0 ; |
813 | 348 | let xpath_str = args.pop_string()?0 ; |
814 | 348 | let eval_result = &args[0]; |
815 | 348 | debug!(" -- Debug: value of '{xpath_str}' is "); |
816 | 348 | match eval_result { |
817 | 78 | Value::Nodeset(nodes) => { |
818 | 78 | if nodes.size() == 0 { |
819 | 0 | debug!("0 nodes (false)"); |
820 | | } else { |
821 | 78 | let singular = nodes.size()==1; |
822 | 78 | debug!("{} node{}. {}:", nodes0 .size0 (), |
823 | 0 | if singular {""} else {"s"}, |
824 | 0 | if singular {"Node is"} else {"Nodes are"}); |
825 | 78 | nodes.document_order() |
826 | 78 | .iter() |
827 | 78 | .enumerate() |
828 | 78 | .for_each(|(i, node)| { |
829 | 78 | match node { |
830 | 78 | Node::Element(mathml) => debug!("#{}:\n{}", |
831 | 0 | i, mml_to_string(*mathml)), |
832 | 0 | _ => debug!("'{node:?}'"), |
833 | | } |
834 | 78 | }) |
835 | | } |
836 | | }, |
837 | 270 | _ => debug!("'{eval_result:?}'"), |
838 | | } |
839 | 348 | return Ok( eval_result.clone() ); |
840 | 348 | } |
841 | | } |
842 | | |
843 | | |
844 | | /// Should be an internal structure for implementation of the IsBracketed, but it was useful in one place in a separate module. |
845 | | /// This should probably be restructured slightly. |
846 | | pub struct IsBracketed; |
847 | | impl IsBracketed { |
848 | 139k | pub fn is_bracketed(element: Element, left: &str, right: &str, requires_comma: bool, requires_mrow: bool) -> bool { |
849 | | use crate::canonicalize::is_fence; |
850 | 139k | if requires_mrow && !116k is_tag116k (element, "mrow") { |
851 | 18.8k | return false; |
852 | 120k | } |
853 | 120k | let children = element.children(); |
854 | 120k | let n_children = children.len(); |
855 | 120k | if (n_children == 0 || |
856 | 120k | !left.is_empty() && !right.is_empty()108k && n_children < 2108k ) || |
857 | 116k | requires_comma && element.children().len() < 34.04k { |
858 | | // not enough argument for there to be a match |
859 | 4.44k | return false; |
860 | 115k | } |
861 | | |
862 | 115k | let first_child = as_element(children[0]); |
863 | 115k | let last_child = as_element(children[children.len()-1]); |
864 | | // debug!("first_child: {}", crate::pretty_print::mml_to_string(first_child)); |
865 | | // debug!("last_child: {}", crate::pretty_print::mml_to_string(last_child)); |
866 | 115k | if (left.is_empty() && (name(first_child) != "mo"11.2k || !is_fence(first_child)2.26k )) || |
867 | 106k | (right.is_empty() && (name(last_child) != "mo"639 || !is_fence(last_child)629 )) { |
868 | 9.61k | return false; |
869 | 106k | } |
870 | | |
871 | 106k | if !left.is_empty() && get_text_from_COE104k (&children[0]) != left || |
872 | 6.14k | !right.is_empty() && get_text_from_COE5.51k (&children5.51k [children.len()-1]) != right { |
873 | | // left or right don't match |
874 | 101k | return false; |
875 | 5.12k | } |
876 | | |
877 | 5.12k | if requires_comma { |
878 | 445 | if let ChildOfElement::Element(contents) = children[1] { |
879 | 445 | let children = contents.children(); |
880 | 445 | if !is_tag(contents, "mrow") || children.len() <= 1248 { |
881 | 197 | return false; |
882 | 248 | } |
883 | | // finally, we can check for a comma -- we might not have operands, so we to check first and second entry |
884 | 248 | if get_text_from_COE(&children[0]).as_str() == "," { |
885 | 1 | return true; |
886 | 247 | } |
887 | 247 | if children.len() > 1 && get_text_from_COE(&children[1]).as_str() == "," { |
888 | 133 | return true; |
889 | 114 | } |
890 | 0 | } |
891 | 114 | return false; |
892 | | } else { |
893 | 4.67k | return true; |
894 | | } |
895 | 139k | } |
896 | | } |
897 | | |
898 | | /** |
899 | | * Returns true if the node is a bracketed expr with the indicated left/right chars |
900 | | * node -- node(s) to test |
901 | | * left -- string (like "[") or empty |
902 | | * right -- string (like "]") or empty |
903 | | * requires_comma - boolean, optional (check the top level of 'node' for commas) |
904 | | */ |
905 | | // 'requiresComma' is useful for checking parenthesized expressions vs function arg lists and other lists |
906 | | impl Function for IsBracketed { |
907 | 115k | fn evaluate<'d>(&self, |
908 | 115k | _context: &context::Evaluation<'_, 'd>, |
909 | 115k | args: Vec<Value<'d>>) |
910 | 115k | -> Result<Value<'d>, Error> |
911 | | { |
912 | 115k | let mut args = Args(args); |
913 | 115k | args.at_least(3)?0 ; |
914 | 115k | args.at_most(5)?0 ; |
915 | 115k | let mut requires_comma = false; |
916 | 115k | let mut requires_mrow = true; |
917 | 115k | if args.len() == 5 { |
918 | 0 | requires_mrow = args.pop_boolean()?; |
919 | 115k | } |
920 | 115k | if args.len() >= 4 { |
921 | 15 | requires_comma = args.pop_boolean()?0 ; |
922 | 115k | } |
923 | 115k | let right = args.pop_string()?0 ; |
924 | 115k | let left = args.pop_string()?0 ; |
925 | | return Ok( Value::Boolean( |
926 | 115k | match validate_one_node(args.pop_nodeset()?0 , "IsBracketed") { |
927 | 0 | Err(_) => false, // be fault tolerant, like xpath, |
928 | 115k | Ok(node) => { |
929 | 115k | if let Node::Element(e) = node { |
930 | 115k | IsBracketed::is_bracketed(e, &left, &right, requires_comma, requires_mrow) |
931 | | } else { |
932 | 0 | false |
933 | | } |
934 | | } |
935 | | }) ); |
936 | 115k | } |
937 | | } |
938 | | |
939 | | pub struct IsInDefinition; |
940 | | impl IsInDefinition { |
941 | | /// Returns true if `test_str` is in `set_name` |
942 | | /// Returns an error if `set_name` is not defined |
943 | 11.0k | pub fn is_defined_in(test_str: &str, defs: &'static LocalKey<RefCell<Definitions>>, set_name: &str) -> Result<bool, Error> { |
944 | 11.0k | return defs.with(|definitions| { |
945 | 11.0k | if let Some(set11.0k ) = definitions.borrow().get_hashset(set_name) { |
946 | 11.0k | return Ok( set.contains(test_str) ); |
947 | 12 | } |
948 | 12 | if let Some(hashmap) = definitions.borrow().get_hashmap(set_name) { |
949 | 12 | return Ok( hashmap.contains_key(test_str) ); |
950 | 0 | } |
951 | 0 | return Err( Error::Other( format!("\n IsInDefinition: '{set_name}' is not defined in definitions.yaml") ) ); |
952 | 11.0k | }); |
953 | 11.0k | } |
954 | | } |
955 | | |
956 | | /** |
957 | | * Returns true if the text is contained in the set defined in Speech or Braille. |
958 | | * element/string -- element (converted to string)/string to test |
959 | | * speech or braille |
960 | | * set_name -- the set in which the string is to be searched |
961 | | */ |
962 | | // 'requiresComma' is useful for checking parenthesized expressions vs function arg lists and other lists |
963 | | impl Function for IsInDefinition { |
964 | 12.0k | fn evaluate<'d>(&self, |
965 | 12.0k | _context: &context::Evaluation<'_, 'd>, |
966 | 12.0k | args: Vec<Value<'d>>) |
967 | 12.0k | -> Result<Value<'d>, Error> |
968 | | { |
969 | 12.0k | let mut args = Args(args); |
970 | | // FIX: temporarily accept two args as assume SPEECH_DEFINITIONS until the Rule files are fixed |
971 | 12.0k | args.at_least(2)?0 ; |
972 | 12.0k | args.at_most(3)?0 ; |
973 | 12.0k | let set_name = args.pop_string()?0 ; |
974 | | // FIX: this (len == 1) is temporary until all the usages are switched to the (new) 3-arg form |
975 | 12.0k | let definitions = if args.len() == 2 { |
976 | 10.4k | match args.pop_string()?0 .as_str() { |
977 | 10.4k | "Speech" => &SPEECH_DEFINITIONS1.35k , |
978 | 9.09k | "Braille" => &BRAILLE_DEFINITIONS, |
979 | 0 | _ => return Err( Error::Other("IsInDefinition:: second argument must be either 'Speech' or 'Braille'".to_string()) ) |
980 | | } |
981 | | } else { |
982 | 1.61k | &SPEECH_DEFINITIONS |
983 | | }; |
984 | 12.0k | match &args[0] { |
985 | 5.04k | Value::String(str) => return match IsInDefinition::is_defined_in(str, definitions, &set_name) { |
986 | 5.04k | Ok(result) => Ok( Value::Boolean( result ) ), |
987 | 0 | Err(e) => Err(e), |
988 | | }, |
989 | 7.02k | Value::Nodeset(nodes) => { |
990 | 7.02k | return if nodes.size() == 0 { |
991 | 0 | Ok( Value::Boolean(false) ) // trivially not in definition |
992 | | } else { |
993 | 7.02k | let node = validate_one_node(nodes.clone(), "IsInDefinition")?0 ; |
994 | 7.02k | if let Node::Element(e) = node { |
995 | 7.02k | let text = get_text_from_element(e); |
996 | 7.02k | if text.is_empty() { |
997 | 979 | Ok( Value::Boolean(false) ) |
998 | | } else { |
999 | 6.04k | match IsInDefinition::is_defined_in(&text, definitions, &set_name) { |
1000 | 6.04k | Ok(result) => Ok( Value::Boolean( result ) ), |
1001 | 0 | Err(e) => Err(e), |
1002 | | } |
1003 | | } |
1004 | | } else { |
1005 | 0 | Ok( Value::Boolean(false)) // trivially not in definition } |
1006 | | } |
1007 | | } |
1008 | | }, |
1009 | 0 | _ => Err( Error::Other("IsInDefinition:: neither a node nor a string is passed for first argument".to_string()) ), |
1010 | | } |
1011 | 12.0k | } |
1012 | | } |
1013 | | |
1014 | | |
1015 | | pub struct DefinitionValue; |
1016 | | impl DefinitionValue { |
1017 | | /// Returns the value associated with `key` in `set_name`. If `key` is not in `set_name`, an empty string is returned |
1018 | | /// Returns an error if `set_name` is not defined |
1019 | 12.7k | pub fn definition_value(key: &str, defs: &'static LocalKey<RefCell<Definitions>>, set_name: &str) -> Result<String, Error> { |
1020 | 12.7k | return defs.with(|definitions| { |
1021 | 12.7k | if let Some(map) = definitions.borrow().get_hashmap(set_name) { |
1022 | 12.7k | return Ok( match map.get(key) { |
1023 | 5.64k | None => "".to_string(), |
1024 | 7.09k | Some(str) => str.clone(), |
1025 | | }); |
1026 | 0 | } |
1027 | 0 | return Err( Error::Other( format!("\n DefinitionValue: '{set_name}' is not defined in definitions.yaml") ) ); |
1028 | 12.7k | }); |
1029 | 12.7k | } |
1030 | | } |
1031 | | |
1032 | | /** |
1033 | | * Returns true if the node is a bracketed expr with the indicated left/right chars |
1034 | | * element/string -- element (converted to string)/string to test |
1035 | | * left -- string (like "[") or empty |
1036 | | * right -- string (like "]") or empty |
1037 | | * requires_comma - boolean, optional (check the top level of 'node' for commas |
1038 | | */ |
1039 | | // 'requiresComma' is useful for checking parenthesized expressions vs function arg lists and other lists |
1040 | | impl Function for DefinitionValue { |
1041 | 13.1k | fn evaluate<'d>(&self, |
1042 | 13.1k | _context: &context::Evaluation<'_, 'd>, |
1043 | 13.1k | args: Vec<Value<'d>>) |
1044 | 13.1k | -> Result<Value<'d>, Error> |
1045 | | { |
1046 | 13.1k | let mut args = Args(args); |
1047 | 13.1k | args.exactly(3)?0 ; |
1048 | 13.1k | let set_name = args.pop_string()?0 ; |
1049 | 13.1k | let definitions = match args.pop_string()?0 .as_str() { |
1050 | 13.1k | "Speech" => &SPEECH_DEFINITIONS13.1k , |
1051 | 12 | "Braille" => &BRAILLE_DEFINITIONS, |
1052 | 0 | _ => return Err( Error::Other("IsInDefinition:: second argument must be either 'Speech' or 'Braille'".to_string()) ) |
1053 | | }; |
1054 | 13.1k | match &args[0] { |
1055 | 5.04k | Value::String(str) => return match DefinitionValue::definition_value(str, definitions, &set_name) { |
1056 | 5.04k | Ok(result) => Ok( Value::String( result ) ), |
1057 | 0 | Err(e) => Err(e), |
1058 | | }, |
1059 | 8.10k | Value::Nodeset(nodes) => { |
1060 | 8.10k | return if nodes.size() == 0 { |
1061 | 0 | Ok( Value::String("".to_string()) ) // trivially not in definition |
1062 | | } else { |
1063 | 8.10k | let node = validate_one_node(nodes.clone(), "DefinitionValue")?0 ; |
1064 | 8.10k | if let Node::Element(e8.10k ) = node { |
1065 | 8.10k | let text = get_text_from_element(e); |
1066 | 8.10k | if text.is_empty() { |
1067 | 410 | Ok( Value::String("".to_string()) ) |
1068 | | } else { |
1069 | 7.69k | match DefinitionValue::definition_value(&text, definitions, &set_name) { |
1070 | 7.69k | Ok(result) => Ok( Value::String( result ) ), |
1071 | 0 | Err(e) => Err(e), |
1072 | | } |
1073 | | } |
1074 | | } else { |
1075 | 3 | Ok( Value::String("".to_string()) ) // trivially not in definition } |
1076 | | } |
1077 | | } |
1078 | | }, |
1079 | 0 | _ => Err( Error::Other("DefinitionValue:: neither a node nor a string is passed for first argument".to_string()) ), |
1080 | | } |
1081 | 13.1k | } |
1082 | | } |
1083 | | |
1084 | | pub struct DistanceFromLeaf; |
1085 | | impl DistanceFromLeaf { |
1086 | 240 | fn distance(element: Element, use_left_side: bool, treat_2d_elements_as_tokens: bool) -> usize { |
1087 | | // FIX: need to handle char level (i.e., chars in a leaf element) |
1088 | 240 | let mut element = element; |
1089 | 240 | let mut distance = 1; |
1090 | | loop { |
1091 | | // debug!("distance={} -- element: {}", distance, mml_to_string(element)); |
1092 | 361 | if MATHML_LEAF_NODES.contains(element.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(element))) { |
1093 | 199 | return distance; |
1094 | 162 | } |
1095 | 162 | if treat_2d_elements_as_tokens && (IsNode::is_2D60 (element60 ) || !IsNode::is_mathml(element)20 ) { |
1096 | 41 | return distance; |
1097 | 121 | } |
1098 | 121 | let children = element.children(); |
1099 | 121 | assert!(!children.is_empty()); |
1100 | 121 | element = as_element( if use_left_side {children[0]0 } else {children[children.len()-1]} ); |
1101 | 121 | distance += 1; |
1102 | | } |
1103 | 240 | } |
1104 | | } |
1105 | | |
1106 | | /** |
1107 | | * Returns distance from the current node to the leftmost/rightmost leaf (if char, then = 0, if token, then 1). |
1108 | | * If the node is a bracketed expr with the indicated left/right chars |
1109 | | * node -- node(s) to test |
1110 | | * left_side -- (bool) traverse leftmost child to leaf |
1111 | | * treat2D_elements_as_tokens -- (bool) 2D notations such as fractions are treated like leaves |
1112 | | */ |
1113 | | impl Function for DistanceFromLeaf { |
1114 | 240 | fn evaluate<'d>(&self, |
1115 | 240 | _context: &context::Evaluation<'_, 'd>, |
1116 | 240 | args: Vec<Value<'d>>) |
1117 | 240 | -> Result<Value<'d>, Error> |
1118 | | { |
1119 | 240 | let mut args = Args(args); |
1120 | 240 | args.exactly(3)?0 ; |
1121 | 240 | let treat_2d_elements_as_tokens = args.pop_boolean()?0 ; |
1122 | 240 | let use_left_side = args.pop_boolean()?0 ; |
1123 | 240 | let node = validate_one_node(args.pop_nodeset()?0 , "DistanceFromLeaf")?0 ; |
1124 | 240 | if let Node::Element(e) = node { |
1125 | 240 | return Ok( Value::Number( DistanceFromLeaf::distance(e, use_left_side, treat_2d_elements_as_tokens) as f64) ); |
1126 | 0 | } |
1127 | | |
1128 | | // FIX: should having a non-element be an error instead?? |
1129 | 0 | return Err(Error::Other(format!("DistanceFromLeaf: first arg '{node:?}' is not a node"))); |
1130 | 240 | } |
1131 | | } |
1132 | | |
1133 | | |
1134 | | |
1135 | | pub struct EdgeNode; |
1136 | | impl EdgeNode { |
1137 | | // Return the root of the ancestor tree if we are at the left/right side of a path from that to 'element' |
1138 | 2.09k | fn edge_node<'a>(element: Element<'a>, use_left_side: bool, stop_node_name: &str) -> Option<Element<'a>> { |
1139 | 2.09k | let element_name = element.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(element)); |
1140 | 2.09k | if element_name == "math" { |
1141 | 86 | return Some(element); |
1142 | 2.00k | }; |
1143 | | |
1144 | 2.00k | let parent = get_parent(element); // there is always a "math" node |
1145 | 2.00k | let parent_name = parent.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(parent)); |
1146 | | |
1147 | | // first check to see if we have the special case of punctuation as last child of math/mrow element |
1148 | | // it only matters if we are looking at the right edge |
1149 | | |
1150 | | // debug!("EdgeNode: there are {} preceding siblings",element.preceding_siblings().len() ); |
1151 | 2.00k | if use_left_side && !element.preceding_siblings().is_empty()1.15k {// not at left side |
1152 | 587 | return None; |
1153 | 1.41k | }; |
1154 | | |
1155 | 1.41k | if !use_left_side && !element.following_siblings().is_empty()848 { // not at right side |
1156 | | // check for the special case that the parent is an mrow and the grandparent is <math> and we have punctuation |
1157 | 574 | let grandparent = get_parent(parent); |
1158 | 574 | let grandparent_name = grandparent.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or(name(grandparent)); |
1159 | 574 | if grandparent_name == "math" && |
1160 | 105 | parent_name == "mrow" && parent.children().len() == 289 { // right kind of mrow |
1161 | 11 | let text = get_text_from_element( as_element(parent.children()[1]) ); |
1162 | 11 | if text == "," || text == "." || text == ";"10 || text == "?"10 { |
1163 | 1 | return Some(grandparent); |
1164 | 10 | } |
1165 | 563 | } |
1166 | 573 | return None; |
1167 | 843 | }; |
1168 | | |
1169 | | // at an edge -- check to see the parent is desired root |
1170 | 843 | if parent_name == stop_node_name || |
1171 | 735 | (stop_node_name == "2D" && IsNode::is_2D338 (parent338 )) { |
1172 | 176 | return Some(parent); |
1173 | 667 | }; |
1174 | | |
1175 | | // debug!("EdgeNode: recurse to {}", parent_name); |
1176 | 667 | return EdgeNode::edge_node(parent, use_left_side, stop_node_name) |
1177 | 2.09k | } |
1178 | | } |
1179 | | |
1180 | | // EdgeNode(node, "left"/"right", stopNodeName) |
1181 | | // -- returns the stopNode if at left/right edge of named ancestor node. "stopNodeName' can also be "2D' |
1182 | | // returns original node match isn't found |
1183 | | // Note: if stopNodeName=="math", then punctuation is taken into account since it isn't really part of the math |
1184 | | impl Function for EdgeNode { |
1185 | 1.41k | fn evaluate<'d>(&self, |
1186 | 1.41k | _context: &context::Evaluation<'_, 'd>, |
1187 | 1.41k | args: Vec<Value<'d>>) |
1188 | 1.41k | -> Result<Value<'d>, Error> |
1189 | | { |
1190 | 1.41k | let mut args = Args(args); |
1191 | 1.41k | args.exactly(3)?0 ; |
1192 | 1.41k | let stop_node_name = args.pop_string()?0 ; |
1193 | 1.41k | let use_left_side = args.pop_string()?0 .to_lowercase() == "left"; |
1194 | 1.41k | let node = validate_one_node(args.pop_nodeset()?0 , "EdgeNode")?0 ; |
1195 | 1.41k | if let Node::Element(e) = node { |
1196 | 1.41k | let result = match EdgeNode::edge_node(e, use_left_side, &stop_node_name) { |
1197 | 260 | Some(found) => found, |
1198 | 1.15k | None => e, |
1199 | | }; |
1200 | 1.41k | let mut node_set = Nodeset::new(); |
1201 | 1.41k | node_set.add(result); |
1202 | 1.41k | return Ok( Value::Nodeset(node_set) ); |
1203 | 0 | } |
1204 | | |
1205 | | // FIX: should having a non-element be an error instead?? |
1206 | 0 | return Err(Error::Other(format!("EdgeNode: first arg '{node:?}' is not a node"))); |
1207 | 1.41k | } |
1208 | | } |
1209 | | |
1210 | | pub struct SpeakIntentName; |
1211 | | /// SpeakIntentName(intent, verbosity) |
1212 | | /// Returns a string corresponding to the intent name with the indicated verbosity |
1213 | | impl Function for SpeakIntentName { |
1214 | 340 | fn evaluate<'d>(&self, |
1215 | 340 | _context: &context::Evaluation<'_, 'd>, |
1216 | 340 | args: Vec<Value<'d>>) |
1217 | 340 | -> Result<Value<'d>, Error> |
1218 | | { |
1219 | 340 | let mut args = Args(args); |
1220 | 340 | args.exactly(3)?0 ; |
1221 | 340 | let fixity = args.pop_string()?0 ; |
1222 | 340 | let verbosity = args.pop_string()?0 ; |
1223 | 340 | let intent_name = args.pop_string()?0 ; |
1224 | 340 | return Ok( Value::String(crate::infer_intent::intent_speech_for_name(&intent_name, &verbosity, &fixity)) ); |
1225 | 340 | } |
1226 | | } |
1227 | | |
1228 | | pub struct GetBracketingIntentName; |
1229 | | /// GetBracketingIntentName(name, verbosity, at_start_or_end) |
1230 | | /// Returns a potentially empty string to use to bracket an intent expression (start foo... end foo) |
1231 | | /// |
1232 | | impl GetBracketingIntentName { |
1233 | 61 | fn bracketing_words(intent_name: &str, verbosity: &str, fixity: &str, at_start: bool) -> String { |
1234 | 61 | crate::definitions::SPEECH_DEFINITIONS.with(|definitions| { |
1235 | 61 | let definitions = definitions.borrow(); |
1236 | 61 | if let Some(intent_name_pattern57 ) = definitions.get_hashmap("IntentMappings").unwrap().get(intent_name) { |
1237 | | // Split the pattern is: fixity-def [|| fixity-def]* |
1238 | | // fixity-def := fixity=open; verbosity; close |
1239 | | // verbosity := terse | medium | verbose |
1240 | 68 | if let Some(matched_intent57 ) = intent_name_pattern.split("||")57 .find57 (|&entry| entry.trim().starts_with(fixity)) { |
1241 | 57 | let (_, matched_intent) = matched_intent.split_once("=").unwrap_or_default(); |
1242 | 57 | let parts = matched_intent.trim().split(";").collect::<Vec<&str>>(); |
1243 | 57 | if parts.len() == 1 { |
1244 | 30 | return "".to_string(); |
1245 | 27 | } |
1246 | 27 | if parts.len() != 3 { |
1247 | 0 | error!("Intent '{}' has {} ';' separated parts, should have 3", intent_name, parts.len()); |
1248 | 0 | return "".to_string(); |
1249 | 27 | } |
1250 | 27 | let mut speech = (if at_start {parts[0]4 } else {parts[2]23 }).split(":").collect::<Vec<&str>>(); |
1251 | 27 | match speech.len() { |
1252 | 20 | 1 => return speech[0].to_string(), |
1253 | | 2 | 3 => { |
1254 | 7 | if speech.len() == 2 { |
1255 | 0 | warn!("Intent '{intent_name}' has only two ':' separated parts, but should have three"); |
1256 | 0 | speech.push(speech[1]); |
1257 | 7 | } |
1258 | 7 | let bracketing_words = match verbosity { |
1259 | 7 | "Terse" => speech[0]0 , |
1260 | 7 | "Medium" => speech[1], |
1261 | 0 | _ => speech[2], |
1262 | | }; |
1263 | 7 | return bracketing_words.to_string(); |
1264 | | }, |
1265 | | _ => { |
1266 | 0 | error!("Intent '{}' has too many ({}) operator names, should only have 2", intent_name, speech.len()); |
1267 | | }, |
1268 | | } |
1269 | 0 | } |
1270 | 4 | }; |
1271 | 4 | return "".to_string(); |
1272 | 61 | }) |
1273 | 61 | } |
1274 | | } |
1275 | | |
1276 | | impl Function for GetBracketingIntentName { |
1277 | 61 | fn evaluate<'d>(&self, |
1278 | 61 | _context: &context::Evaluation<'_, 'd>, |
1279 | 61 | args: Vec<Value<'d>>) |
1280 | 61 | -> Result<Value<'d>, Error> |
1281 | | { |
1282 | 61 | let mut args = Args(args); |
1283 | 61 | args.exactly(4)?0 ; |
1284 | 61 | let start_or_end = args.pop_string()?0 ; |
1285 | 61 | if start_or_end != "start" && start_or_end != "end"57 { |
1286 | 0 | return Err( Error::Other("GetBracketingIntentName: first argument must be either 'start' or 'end'".to_string()) ); |
1287 | 61 | } |
1288 | 61 | let fixity = args.pop_string()?0 ; |
1289 | 61 | let verbosity = args.pop_string()?0 ; |
1290 | 61 | let name = args.pop_string()?0 ; |
1291 | 61 | return Ok( Value::String(GetBracketingIntentName:: bracketing_words(&name, &verbosity, &fixity, start_or_end == "start")) ); |
1292 | 61 | } |
1293 | | } |
1294 | | |
1295 | | pub struct GetNavigationPartName; |
1296 | | /// GetNavigationPartName(name, index) |
1297 | | /// Returns the name to use to speak the part of a navigation expression (e.g., 'numerator', 'denominator', 'base', 'exponent', ...). |
1298 | | /// If there is no match, an empty string is returned. |
1299 | | /// 'index' is 0-based |
1300 | | /// |
1301 | | impl GetNavigationPartName { |
1302 | 129 | fn navigation_part_name(intent_name: &str, index: usize) -> String { |
1303 | 129 | crate::definitions::SPEECH_DEFINITIONS.with(|definitions| { |
1304 | 129 | let definitions = definitions.borrow(); |
1305 | 129 | if let Some(navigation_names) = definitions.get_hashmap("NavigationParts") && |
1306 | 129 | let Some(nav_part_names105 ) = navigation_names.get(intent_name) { |
1307 | | // Split the pattern is: part [; part]* |
1308 | 105 | if let Some(part_name) = nav_part_names.trim().split(";").nth(index) { |
1309 | 105 | return part_name.trim().to_string(); |
1310 | 0 | } |
1311 | 24 | } |
1312 | 24 | return "".to_string(); |
1313 | 129 | }) |
1314 | 129 | } |
1315 | | } |
1316 | | |
1317 | | impl Function for GetNavigationPartName { |
1318 | 129 | fn evaluate<'d>(&self, |
1319 | 129 | _context: &context::Evaluation<'_, 'd>, |
1320 | 129 | args: Vec<Value<'d>>) |
1321 | 129 | -> Result<Value<'d>, Error> |
1322 | | { |
1323 | 129 | let mut args = Args(args); |
1324 | 129 | args.exactly(2)?0 ; |
1325 | 129 | let index = args.pop_number()?0 as usize; |
1326 | 129 | let name = args.pop_string()?0 ; |
1327 | 129 | return Ok( Value::String(GetNavigationPartName:: navigation_part_name(&name, index)) ); |
1328 | 129 | } |
1329 | | } |
1330 | | |
1331 | | pub struct FontSizeGuess; |
1332 | | /// FontSizeGuess(size_string) |
1333 | | /// returns a guess of the size in "ems" |
1334 | | /// Examples: |
1335 | | /// "0.278em" -> 0.278 |
1336 | | /// "" |
1337 | | // returns original node match isn't found |
1338 | | impl FontSizeGuess { |
1339 | 224 | pub fn em_from_value(value_with_unit: &str) -> f64 { |
1340 | | // match one or more digits followed by a unit -- there are many more units, but they tend to be large and rarer(?) |
1341 | 3 | static FONT_VALUE: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"(-?[0-9]*\.?[0-9]*)(px|cm|mm|Q|in|ppc|pt|ex|em|rem)").unwrap() }); |
1342 | 224 | let cap = FONT_VALUE.captures(value_with_unit); |
1343 | 224 | if let Some(cap200 ) = cap { |
1344 | 200 | if cap.len() == 3 { |
1345 | 200 | let multiplier = match &cap[2] { // guess based on 12pt font to convert to ems |
1346 | 200 | "px" => 1.0/12.00 , |
1347 | 200 | "cm" => 2.370 , |
1348 | 200 | "mm" => 0.2370 , |
1349 | 200 | "Q" => 0.0590 , // 1/4 mm |
1350 | 200 | "in" => 6.0223 , |
1351 | 177 | "pc" => 1.00 , |
1352 | 177 | "pt" => 1.0/12.06 , |
1353 | 171 | "ex" => 0.50 , |
1354 | 171 | "em" => 1.0, |
1355 | 0 | "rem" => 16.0/12.0, |
1356 | 0 | default => {debug!("unit='{default}'"); 10.0} |
1357 | | }; |
1358 | | // debug!("FontSizeGuess: {}->{}, val={}, multiplier={}", value_with_unit, value*multiplier, value, multiplier); |
1359 | 200 | return cap[1].parse::<f64>().unwrap_or(0.0) * multiplier; |
1360 | | } else { |
1361 | 0 | return 0.0; // something bad happened |
1362 | | } |
1363 | | }else { |
1364 | 24 | let multiplier = match value_with_unit { // guess based on 12pt font to convert to ems |
1365 | 24 | "veryverythinspace" => 1.0/18.00 , |
1366 | 24 | "verythinspace" => 2.0/18.00 , |
1367 | 24 | "thinspace" => 3.0/18.00 , |
1368 | 24 | "mediumspace" => 4.0/18.00 , |
1369 | 24 | "thickspace" => 5.0/18.00 , |
1370 | 24 | "verythickspace" => 6.0/18.00 , |
1371 | 24 | "veryverythickspace" => 7.0/18.00 , |
1372 | 24 | _ => 0.0, |
1373 | | }; |
1374 | 24 | return multiplier; |
1375 | | } |
1376 | 224 | } |
1377 | | } |
1378 | | impl Function for FontSizeGuess { |
1379 | 0 | fn evaluate<'d>(&self, |
1380 | 0 | _context: &context::Evaluation<'_, 'd>, |
1381 | 0 | args: Vec<Value<'d>>) |
1382 | 0 | -> Result<Value<'d>, Error> |
1383 | | { |
1384 | 0 | let mut args = Args(args); |
1385 | 0 | args.exactly(1)?; |
1386 | 0 | let value_with_unit = args.pop_string()?; |
1387 | 0 | let em_value = FontSizeGuess::em_from_value(&value_with_unit); |
1388 | 0 | return Ok( Value::Number(em_value) ); |
1389 | 0 | } |
1390 | | } |
1391 | | |
1392 | | pub struct ReplaceAll; |
1393 | | /// ReplaceAll(haystack, needle, replacement) |
1394 | | /// Returns a string with all occurrences of 'needle' replaced with 'replacement' |
1395 | | impl Function for ReplaceAll { |
1396 | 0 | fn evaluate<'d>(&self, |
1397 | 0 | _context: &context::Evaluation<'_, 'd>, |
1398 | 0 | args: Vec<Value<'d>>) |
1399 | 0 | -> Result<Value<'d>, Error> |
1400 | | { |
1401 | 0 | let mut args = Args(args); |
1402 | 0 | args.exactly(3)?; |
1403 | 0 | let replacement = args.pop_string()?; |
1404 | 0 | let needle = args.pop_string()?; |
1405 | 0 | let haystack = args.pop_string()?; |
1406 | 0 | return Ok( Value::String(haystack.replace(&needle, &replacement)) ); |
1407 | 0 | } |
1408 | | } |
1409 | | |
1410 | | /// Add all the functions defined in this module to `context`. |
1411 | 22.7k | pub fn add_builtin_functions(context: &mut Context) { |
1412 | 22.7k | context.set_function("NestingChars", crate::braille::NemethNestingChars); |
1413 | 22.7k | context.set_function("BrailleChars", crate::braille::BrailleChars); |
1414 | 22.7k | context.set_function("NeedsToBeGrouped", crate::braille::NeedsToBeGrouped); |
1415 | 22.7k | context.set_function("IsNode", IsNode); |
1416 | 22.7k | context.set_function("ToOrdinal", ToOrdinal); |
1417 | 22.7k | context.set_function("ToCommonFraction", ToCommonFraction); |
1418 | 22.7k | context.set_function("IsBracketed", IsBracketed); |
1419 | 22.7k | context.set_function("IsInDefinition", IsInDefinition); |
1420 | 22.7k | context.set_function("DefinitionValue", DefinitionValue); |
1421 | 22.7k | context.set_function("BaseNode", BaseNode); |
1422 | 22.7k | context.set_function("IfThenElse", IfThenElse); |
1423 | 22.7k | context.set_function("IFTHENELSE", IfThenElse); |
1424 | 22.7k | context.set_function("DistanceFromLeaf", DistanceFromLeaf); |
1425 | 22.7k | context.set_function("EdgeNode", EdgeNode); |
1426 | 22.7k | context.set_function("SpeakIntentName", SpeakIntentName); |
1427 | 22.7k | context.set_function("GetBracketingIntentName", GetBracketingIntentName); |
1428 | 22.7k | context.set_function("GetNavigationPartName", GetNavigationPartName); |
1429 | 22.7k | context.set_function("DEBUG", Debug); |
1430 | | |
1431 | | // Not used: remove?? |
1432 | 22.7k | context.set_function("min", Min); // missing in xpath 1.0 |
1433 | 22.7k | context.set_function("max", Max); // missing in xpath 1.0 |
1434 | 22.7k | context.set_function("FontSizeGuess", FontSizeGuess); |
1435 | 22.7k | context.set_function("ReplaceAll", ReplaceAll); |
1436 | 22.7k | } |
1437 | | |
1438 | | |
1439 | | #[cfg(test)] |
1440 | | mod tests { |
1441 | | use super::*; |
1442 | | use sxd_document::parser; |
1443 | | use crate::interface::{trim_element, get_element}; |
1444 | | |
1445 | | |
1446 | 4 | fn init_word_list() { |
1447 | 4 | crate::interface::set_rules_dir(super::super::abs_rules_dir_path()).unwrap(); |
1448 | 4 | crate::interface::set_preference("Language", "en").unwrap(); |
1449 | 4 | let result = crate::definitions::read_definitions_file(true); |
1450 | 4 | if let Err(e0 ) = result { |
1451 | 0 | panic!("unable to read 'Rules/Languages/en/definitions.yaml\n{e}"); |
1452 | 4 | } |
1453 | 4 | } |
1454 | | |
1455 | | #[test] |
1456 | 1 | fn ordinal_one_digit() { |
1457 | 1 | init_word_list(); |
1458 | 1 | assert_eq!("zeroth", ToOrdinal::convert("0", false, false).unwrap()); |
1459 | 1 | assert_eq!("second", ToOrdinal::convert("2", false, false).unwrap()); |
1460 | 1 | assert_eq!("ninth", ToOrdinal::convert("9", false, false).unwrap()); |
1461 | | |
1462 | 1 | assert_eq!("zeroth", ToOrdinal::convert("0", false, true).unwrap()); |
1463 | 1 | assert_eq!("seconds", ToOrdinal::convert("2", false, true).unwrap()); |
1464 | 1 | assert_eq!("ninths", ToOrdinal::convert("9", false, true).unwrap()); |
1465 | | |
1466 | 1 | assert_eq!("first", ToOrdinal::convert("1", true, false).unwrap()); |
1467 | 1 | assert_eq!("half", ToOrdinal::convert("2", true, false).unwrap()); |
1468 | 1 | assert_eq!("half", ToOrdinal::convert("02", true, false).unwrap()); |
1469 | 1 | assert_eq!("ninth", ToOrdinal::convert("9", true, false).unwrap()); |
1470 | | |
1471 | 1 | assert_eq!("halves", ToOrdinal::convert("2", true, true).unwrap()); |
1472 | 1 | assert_eq!("halves", ToOrdinal::convert("002", true, true).unwrap()); |
1473 | 1 | assert_eq!("ninths", ToOrdinal::convert("9", true, true).unwrap()); |
1474 | 1 | } |
1475 | | |
1476 | | #[test] |
1477 | 1 | fn ordinal_two_digit() { |
1478 | 1 | init_word_list(); |
1479 | 1 | assert_eq!("tenth", ToOrdinal::convert("10", false, false).unwrap()); |
1480 | 1 | assert_eq!("seventeenth", ToOrdinal::convert("17", false, false).unwrap()); |
1481 | 1 | assert_eq!("thirty second", ToOrdinal::convert("32", false, false).unwrap()); |
1482 | 1 | assert_eq!("fortieth", ToOrdinal::convert("40", false, false).unwrap()); |
1483 | | |
1484 | 1 | assert_eq!("tenths", ToOrdinal::convert("10", false, true).unwrap()); |
1485 | 1 | assert_eq!("sixteenths", ToOrdinal::convert("16", false, true).unwrap()); |
1486 | 1 | assert_eq!("eighty eighths", ToOrdinal::convert("88", false, true).unwrap()); |
1487 | 1 | assert_eq!("fiftieths", ToOrdinal::convert("50", false, true).unwrap()); |
1488 | | |
1489 | 1 | assert_eq!("eleventh", ToOrdinal::convert("11", true, false).unwrap()); |
1490 | 1 | assert_eq!("forty fourth", ToOrdinal::convert("44", true, false).unwrap()); |
1491 | 1 | assert_eq!("ninth", ToOrdinal::convert("9", true, false).unwrap()); |
1492 | 1 | assert_eq!("ninth", ToOrdinal::convert("00000009", true, false).unwrap()); |
1493 | 1 | assert_eq!("sixtieth", ToOrdinal::convert("60", true, false).unwrap()); |
1494 | | |
1495 | 1 | assert_eq!("tenths", ToOrdinal::convert("10", true, true).unwrap()); |
1496 | 1 | assert_eq!("tenths", ToOrdinal::convert("0010", true, true).unwrap()); |
1497 | 1 | assert_eq!("elevenths", ToOrdinal::convert("11", true, true).unwrap()); |
1498 | 1 | assert_eq!("nineteenths", ToOrdinal::convert("19", true, true).unwrap()); |
1499 | 1 | assert_eq!("twentieths", ToOrdinal::convert("20", true, true).unwrap()); |
1500 | 1 | assert_eq!("nineteenths", ToOrdinal::convert("𝟏𝟗", true, true).unwrap()); |
1501 | 1 | } |
1502 | | |
1503 | | #[test] |
1504 | 1 | fn ordinal_three_digit() { |
1505 | 1 | init_word_list(); |
1506 | 1 | assert_eq!("one hundred first", ToOrdinal::convert("101", false, false).unwrap()); |
1507 | 1 | assert_eq!("two hundred tenth", ToOrdinal::convert("210", false, false).unwrap()); |
1508 | 1 | assert_eq!("four hundred thirty second", ToOrdinal::convert("432", false, false).unwrap()); |
1509 | 1 | assert_eq!("four hundred second", ToOrdinal::convert("402", false, false).unwrap()); |
1510 | | |
1511 | 1 | assert_eq!("one hundred first", ToOrdinal::convert("101", true, false).unwrap()); |
1512 | 1 | assert_eq!("two hundred second", ToOrdinal::convert("202", true, false).unwrap()); |
1513 | 1 | assert_eq!("four hundred thirty second", ToOrdinal::convert("432", true, false).unwrap()); |
1514 | 1 | assert_eq!("five hundred third", ToOrdinal::convert("503", true, false).unwrap()); |
1515 | | |
1516 | 1 | assert_eq!("three hundred elevenths", ToOrdinal::convert("311", false, true).unwrap()); |
1517 | 1 | assert_eq!("four hundred ninety ninths", ToOrdinal::convert("499", false, true).unwrap()); |
1518 | 1 | assert_eq!("nine hundred ninetieths", ToOrdinal::convert("990", false, true).unwrap()); |
1519 | 1 | assert_eq!("six hundred seconds", ToOrdinal::convert("602", false, true).unwrap()); |
1520 | | |
1521 | 1 | assert_eq!("seven hundredths", ToOrdinal::convert("700", true, true).unwrap()); |
1522 | 1 | assert_eq!("one hundredths", ToOrdinal::convert("100", true, true).unwrap()); |
1523 | 1 | assert_eq!("eight hundred seventeenths", ToOrdinal::convert("817", true, true).unwrap()); |
1524 | 1 | } |
1525 | | #[test] |
1526 | 1 | fn ordinal_large() { |
1527 | 1 | init_word_list(); |
1528 | 1 | assert_eq!("one thousandth", ToOrdinal::convert("1000", false, false).unwrap()); |
1529 | 1 | assert_eq!("two thousand one hundredth", ToOrdinal::convert("2100", false, false).unwrap()); |
1530 | 1 | assert_eq!("thirty thousandth", ToOrdinal::convert("30000", false, false).unwrap()); |
1531 | 1 | assert_eq!("four hundred thousandth", ToOrdinal::convert("400000", false, false).unwrap()); |
1532 | | |
1533 | 1 | assert_eq!("four hundred thousandth", ToOrdinal::convert("400000", true, false).unwrap()); |
1534 | 1 | assert_eq!("five hundred thousand second", ToOrdinal::convert("500002", true, false).unwrap()); |
1535 | 1 | assert_eq!("six millionth", ToOrdinal::convert("6000000", true, false).unwrap()); |
1536 | 1 | assert_eq!("sixty millionth", ToOrdinal::convert("60000000", true, false).unwrap()); |
1537 | | |
1538 | 1 | assert_eq!("seven billionths", ToOrdinal::convert("7000000000", false, true).unwrap()); |
1539 | 1 | assert_eq!("eight trillionths", ToOrdinal::convert("8000000000000", false, true).unwrap()); |
1540 | 1 | assert_eq!("nine quadrillionths", ToOrdinal::convert("9000000000000000", false, true).unwrap()); |
1541 | 1 | assert_eq!("one quintillionth", ToOrdinal::convert("1000000000000000000", false, false).unwrap()); |
1542 | | |
1543 | 1 | assert_eq!("nine billion eight hundred seventy six million five hundred forty three thousand two hundred tenths", ToOrdinal::convert("9876543210", true, true).unwrap()); |
1544 | 1 | assert_eq!("nine billion five hundred forty three thousand two hundred tenths", ToOrdinal::convert("9000543210", true, true).unwrap()); |
1545 | 1 | assert_eq!("zeroth", ToOrdinal::convert("00000", false, false).unwrap()); |
1546 | 1 | } |
1547 | | |
1548 | | |
1549 | 11 | fn test_is_simple(message: &'static str, mathml_str: &'static str) { |
1550 | | // this forces initialization |
1551 | 11 | crate::speech::SPEECH_RULES.with(|_| true); |
1552 | 11 | let package = parser::parse(mathml_str) |
1553 | 11 | .expect("failed to parse XML"); |
1554 | 11 | let mathml = get_element(&package); |
1555 | 11 | trim_element(mathml, false); |
1556 | 11 | assert!(IsNode::is_simple(mathml), "{}", message); |
1557 | 11 | } |
1558 | | |
1559 | 7 | fn test_is_not_simple(message: &'static str, mathml_str: &'static str) { |
1560 | | // this forces initialization |
1561 | 7 | crate::speech::SPEECH_RULES.with(|_| true); |
1562 | 7 | let package = parser::parse(mathml_str) |
1563 | 7 | .expect("failed to parse XML"); |
1564 | 7 | let mathml = get_element(&package); |
1565 | 7 | trim_element(mathml, false); |
1566 | 7 | assert!(!IsNode::is_simple(mathml), "{}", message); |
1567 | 7 | } |
1568 | | #[test] |
1569 | 1 | fn is_simple() { |
1570 | 1 | test_is_simple("single variable", "<mi>x</mi>"); |
1571 | 1 | test_is_simple("single number", "<mn>1.2</mn>"); |
1572 | 1 | test_is_simple("negative number", "<mrow><mo>-</mo><mn>10</mn></mrow>"); |
1573 | 1 | test_is_simple("negative variable", "<mrow><mo>-</mo><mi>x</mi></mrow>"); |
1574 | 1 | test_is_simple("ordinal fraction", "<mfrac><mn>3</mn><mn>4</mn></mfrac>"); |
1575 | 1 | test_is_simple("x y", "<mrow><mi>x</mi><mo>⁢</mo><mi>y</mi></mrow>"); |
1576 | 1 | test_is_simple("negative two vars", |
1577 | | "<mrow><mrow><mo>-</mo><mi>x</mi></mrow><mo>⁢</mo><mi>y</mi></mrow>"); |
1578 | 1 | test_is_simple("-2 x y", |
1579 | | "<mrow><mrow><mo>-</mo><mn>2</mn></mrow> |
1580 | | <mo>⁢</mo><mi>x</mi><mo>⁢</mo><mi>z</mi></mrow>"); |
1581 | 1 | test_is_simple("sin x", "<mrow><mi>sin</mi><mo>⁡</mo><mi>x</mi></mrow>"); |
1582 | 1 | test_is_simple("f(x)", "<mrow><mi>f</mi><mo>⁡</mo><mrow><mo>(</mo><mi>x</mi><mo>)</mo></mrow></mrow>"); |
1583 | 1 | test_is_simple("f(x+y)", |
1584 | | "<mrow><mi>f</mi><mo>⁡</mo>\ |
1585 | | <mrow><mo>(</mo><mi>x</mi><mo>+</mo><mi>y</mi><mo>)</mo></mrow></mrow>"); |
1586 | | |
1587 | 1 | } |
1588 | | |
1589 | | #[test] |
1590 | 1 | fn is_not_simple() { |
1591 | 1 | test_is_not_simple("multi-char variable", "<mi>rise</mi>"); |
1592 | 1 | test_is_not_simple("large ordinal fraction", "<mfrac><mn>30</mn><mn>4</mn></mfrac>"); |
1593 | 1 | test_is_not_simple("fraction with var in numerator", "<mfrac><mi>x</mi><mn>4</mn></mfrac>"); |
1594 | 1 | test_is_not_simple("square root", "<msqrt><mi>x</mi></msqrt>"); |
1595 | 1 | test_is_not_simple("subscript", "<msub><mi>x</mi><mn>4</mn></msub>"); |
1596 | 1 | test_is_not_simple("-x y z", |
1597 | | "<mrow><mrow><mo>-</mo><mi>x</mi></mrow> |
1598 | | <mo>⁢</mo><mi>y</mi><mo>⁢</mo><mi>z</mi></mrow>"); |
1599 | 1 | test_is_not_simple("C(-2,1,4)", // github.com/NSoiffer/MathCAT/issues/199 |
1600 | | "<mrow><mi>C</mi><mrow><mo>(</mo><mo>−</mo><mn>2</mn><mo>,</mo><mn>1</mn><mo>,</mo><mn>4</mn><mo>)</mo></mrow></mrow>"); |
1601 | | |
1602 | 1 | } |
1603 | | |
1604 | | #[test] |
1605 | 1 | fn at_left_edge() { |
1606 | 1 | let mathml = "<math><mfrac><mrow><mn>30</mn><mi>x</mi></mrow><mn>4</mn></mfrac></math>"; |
1607 | 1 | let package = parser::parse(mathml).expect("failed to parse XML"); |
1608 | 1 | let mathml = get_element(&package); |
1609 | 1 | trim_element(mathml, false); |
1610 | 1 | let fraction = as_element(mathml.children()[0]); |
1611 | 1 | let mn = as_element(as_element(fraction.children()[0]).children()[0]); |
1612 | 1 | assert_eq!(EdgeNode::edge_node(mn, true, "2D"), Some(fraction)); |
1613 | 1 | assert_eq!(EdgeNode::edge_node(mn, false, "2D"), None); |
1614 | | |
1615 | 1 | let mi = as_element(as_element(fraction.children()[0]).children()[1]); |
1616 | 1 | assert_eq!(EdgeNode::edge_node(mi, true, "2D"), None); |
1617 | 1 | } |
1618 | | |
1619 | | #[test] |
1620 | 1 | fn at_right_edge() { |
1621 | 1 | let mathml = "<math><mrow><mfrac><mn>4</mn><mrow><mn>30</mn><mi>x</mi></mrow></mfrac><mo>.</mo></mrow></math>"; |
1622 | 1 | let package = parser::parse(mathml).expect("failed to parse XML"); |
1623 | 1 | let mathml = get_element(&package); |
1624 | 1 | trim_element(mathml, false); |
1625 | 1 | let fraction = as_element(as_element(mathml.children()[0]).children()[0]); |
1626 | 1 | let mi = as_element(as_element(fraction.children()[1]).children()[1]); |
1627 | 1 | assert_eq!(EdgeNode::edge_node(mi, true, "2D"), None); |
1628 | 1 | assert_eq!(EdgeNode::edge_node(mi, false, "2D"), Some(fraction)); |
1629 | 1 | assert_eq!(EdgeNode::edge_node(mi, false, "math"), Some(mathml)); |
1630 | | |
1631 | 1 | let mn = as_element(as_element(fraction.children()[1]).children()[0]); |
1632 | 1 | assert_eq!(EdgeNode::edge_node(mn, true, "2D"), None); |
1633 | 1 | } |
1634 | | } |