/home/runner/work/MathCAT/MathCAT/src/infer_intent.rs
Line | Count | Source |
1 | | //! Use heuristics to infer the intent. |
2 | | //! For example, an `mfrac` with `linethickness=0` would be a binomial |
3 | | //! The inference is added to the MathML |
4 | | //! |
5 | | //! The implementation of the module is on hold until the MathML committee figures out how it wants to do this. |
6 | | #![allow(clippy::needless_return)] |
7 | | |
8 | | use sxd_document::dom::{Element, Document, ChildOfElement}; |
9 | | use crate::prefs::PreferenceManager; |
10 | | use crate::speech::SpeechRulesWithContext; |
11 | | use crate::canonicalize::{as_element, as_text, name, create_mathml_element, set_mathml_name, INTENT_ATTR, MATHML_FROM_NAME_ATTR}; |
12 | | use crate::errors::*; |
13 | | use std::fmt; |
14 | | use std::sync::LazyLock; |
15 | | use crate::pretty_print::mml_to_string; |
16 | | use crate::xpath_functions::is_leaf; |
17 | | use regex::Regex; |
18 | | use phf::phf_set; |
19 | | use log::{debug, error, warn}; |
20 | | |
21 | | const IMPLICIT_FUNCTION_NAME: &str = "apply-function"; |
22 | | |
23 | 2.47k | pub fn infer_intent<'r, 'c, 's:'c, 'm:'c>(rules_with_context: &'r mut SpeechRulesWithContext<'c,'s,'m>, mathml: Element<'c>) -> Result<Element<'m>> { |
24 | 2.47k | match catch_errors_building_intent(rules_with_context, mathml) { |
25 | 2.45k | Ok(intent) => return Ok(intent), |
26 | 19 | Err(e) => { |
27 | | // lookup what we should do for error recovery |
28 | 19 | let intent_preference = rules_with_context.get_rules().pref_manager.borrow().pref_to_string("IntentErrorRecovery"); |
29 | 19 | if intent_preference == "Error" { |
30 | 9 | return Err(e); |
31 | | } else { |
32 | 10 | let saved_intent_attr = mathml.attribute_value(INTENT_ATTR).unwrap(); |
33 | 10 | mathml.remove_attribute(INTENT_ATTR); |
34 | | // can't call intent_from_mathml() because we have already borrowed_mut -- we call a more internal version |
35 | 10 | let intent_tree = match rules_with_context.match_pattern::<Element<'m>>(mathml) |
36 | 10 | .context("Pattern match/replacement failure!") { |
37 | 0 | Err(e) => Err(e), |
38 | 10 | Ok(intent) => { |
39 | 10 | intent.set_attribute_value(INTENT_ATTR, saved_intent_attr); // so attr can be potentially be viewed later |
40 | 10 | Ok(intent) |
41 | | }, |
42 | | }; |
43 | 10 | mathml.set_attribute_value(INTENT_ATTR, saved_intent_attr); |
44 | 10 | return intent_tree; |
45 | | } |
46 | | } |
47 | | } |
48 | | |
49 | 2.47k | fn catch_errors_building_intent<'r, 'c, 's:'c, 'm:'c>(rules_with_context: &'r mut SpeechRulesWithContext<'c,'s,'m>, mathml: Element<'c>) -> Result<Element<'m>> { |
50 | 2.47k | if let Some(intent_str) = mathml.attribute_value(INTENT_ATTR) { |
51 | | // debug!("Before intent: {}", crate::pretty_print::mml_to_string(mathml)); |
52 | 2.47k | let mut lex_state = LexState::init(intent_str.trim())?0 ; |
53 | 2.47k | let mut intent_offset = 0; |
54 | 2.47k | let result2.46k = build_intent(rules_with_context, &mut lex_state, mathml, &mut intent_offset) |
55 | 2.47k | .with_context(|| format!14 ("occurs before '{}' in intent attribute value '{}'", lex_state.remaining_str, intent_str))?14 ; |
56 | 2.46k | if lex_state.token != Token::None { |
57 | 5 | bail!("Error in intent value: extra unparsed intent '{}' in intent attribute value '{}'", lex_state.remaining_str, intent_str); |
58 | 2.45k | } |
59 | 2.45k | assert!(lex_state.remaining_str.is_empty()); |
60 | | // debug!("Resulting intent:\n{}", crate::pretty_print::mml_to_string(result)); |
61 | 2.45k | return Ok(result); |
62 | 0 | } |
63 | 0 | bail!("Internal error: infer_intent() called on MathML with no intent arg:\n{}", mml_to_string(mathml)); |
64 | 2.47k | } |
65 | 2.47k | } |
66 | | |
67 | | |
68 | | static FIXITIES: phf::Set<&str> = phf_set! { |
69 | | "function", "infix", "prefix", "postfix", "silent", "other", |
70 | | }; |
71 | | |
72 | | /// Eliminate all but the last fixity property |
73 | 7.63k | pub fn simplify_fixity_properties(properties: &str) -> String { |
74 | 7.63k | let parts: Vec<&str> = properties.split(':').collect(); |
75 | | // debug!("simplify_fixity_properties {} parts from input: '{}'", parts.len(), properties); |
76 | 7.63k | let mut fixity_property = ""; |
77 | 7.63k | let mut answer = ":".to_string(); |
78 | 19.2k | for part in parts7.63k { |
79 | 19.2k | if FIXITIES.contains(part) { |
80 | 1.12k | fixity_property = part; |
81 | 18.1k | } else if !part.is_empty() { |
82 | 4.71k | answer.push_str(part); |
83 | 4.71k | answer.push(':'); |
84 | 13.4k | } |
85 | | } |
86 | 7.63k | if !fixity_property.is_empty() { |
87 | 1.12k | answer.push_str(fixity_property); |
88 | 1.12k | answer.push(':'); |
89 | 6.51k | } |
90 | 7.63k | return answer; |
91 | 7.63k | } |
92 | | |
93 | | /// Given the intent add the fixity property for the intent if it isn't given (and one exists) |
94 | 2.72k | fn add_fixity(intent: Element) { |
95 | 2.72k | let properties = intent.attribute_value(INTENT_PROPERTY).unwrap_or_default(); |
96 | 7.47k | if properties.split(":")2.72k .all2.72k (|property| !FIXITIES.contains(property)) { |
97 | 2.63k | let intent_name = name(intent); |
98 | 2.63k | crate::definitions::SPEECH_DEFINITIONS.with(|definitions| { |
99 | 2.63k | let definitions = definitions.borrow(); |
100 | 2.63k | if let Some(definition12 ) = definitions.get_hashmap("IntentMappings").unwrap().get(intent_name) && |
101 | 12 | let Some((fixity, _)) = definition.split_once("=") { |
102 | 12 | let new_properties = (if properties.is_empty() {":"} else {properties0 }).to_string() + fixity + ":"; |
103 | 12 | intent.set_attribute_value(INTENT_PROPERTY, &new_properties); |
104 | | // debug!("Added fixity: new value '{}'", intent.attribute_value(INTENT_PROPERTY).unwrap()); |
105 | 2.62k | }; |
106 | 2.63k | }); |
107 | 90 | } |
108 | 2.72k | } |
109 | | |
110 | | |
111 | | /// Given some MathML, expand out any intents taking into account their fixity property |
112 | | /// This is recursive |
113 | 363 | pub fn add_fixity_children(intent: Element) -> Element { |
114 | 363 | let children = intent.children(); |
115 | 363 | if children.is_empty() || (children.len() == 1 && children[0].element().is_none()) { |
116 | 0 | return intent; |
117 | 363 | } |
118 | | |
119 | 363 | for child in children { |
120 | 363 | let child = as_element(child); |
121 | 363 | if child.attribute_value(INTENT_ATTR).is_some() { |
122 | 0 | add_fixity_child(child); |
123 | 363 | } |
124 | | } |
125 | 363 | return intent; |
126 | | |
127 | 0 | fn add_fixity_child(mathml: Element) -> Element { |
128 | 0 | let mut children = mathml.children(); |
129 | 0 | if children.is_empty() { |
130 | 0 | return mathml; |
131 | 0 | } |
132 | | // we also exclude fixity on mtable because they mess up the counts (see 'en::mtable::unknown_mtable_property') |
133 | 0 | if mathml.attribute_value(MATHML_FROM_NAME_ATTR).unwrap_or_default() == "mtable" { |
134 | 0 | return mathml; |
135 | 0 | } |
136 | 0 | let doc = mathml.document(); |
137 | 0 | let properties = mathml.attribute_value(INTENT_PROPERTY).unwrap_or_default(); |
138 | 0 | let fixity = properties.rsplit(':').find(|&property| FIXITIES.contains(property)).unwrap_or_default(); |
139 | 0 | let intent_name = name(mathml); |
140 | | |
141 | 0 | let op_name_id = mathml.attribute_value("id").unwrap_or("new-id"); |
142 | 0 | match fixity { |
143 | 0 | "infix" => { |
144 | 0 | let mut new_children = Vec::with_capacity(2*children.len()-1); |
145 | 0 | new_children.push(children[0]); |
146 | 0 | for (i, &child) in children.iter().enumerate().skip(1) { |
147 | 0 | new_children.push(create_operator_element(intent_name, fixity, op_name_id, i, &doc)); |
148 | 0 | new_children.push(child); |
149 | 0 | } |
150 | 0 | mathml.replace_children(new_children); |
151 | | }, |
152 | 0 | "prefix" => { |
153 | 0 | children.insert(0, create_operator_element(intent_name, fixity, op_name_id, 1, &doc)); |
154 | 0 | mathml.replace_children(children); |
155 | 0 | }, |
156 | 0 | "postfix" => { |
157 | 0 | children.push( create_operator_element(intent_name, fixity, op_name_id, 1, &doc)); |
158 | 0 | mathml.replace_children(children); |
159 | 0 | }, |
160 | 0 | "silent" => { |
161 | 0 | // children remain the same -- nothing to do |
162 | 0 | }, |
163 | 0 | "other" => { |
164 | 0 | // a special case -- will be handled with specific rules (e.g., intervals need to add "from" and "to", not a single word) |
165 | 0 | }, |
166 | | _ => { // "function" is the default |
167 | | // build a function like notation function-name U+2061 <mrow> children </mrow> |
168 | 0 | let mut new_children = Vec::with_capacity(3); |
169 | 0 | let function_name = create_operator_element(intent_name, "function", op_name_id, 1, &doc); |
170 | 0 | new_children.push(function_name); |
171 | 0 | let invisible_apply_function = create_operator_element("mo", "infix", op_name_id, 2, &doc); |
172 | 0 | invisible_apply_function.element().unwrap().set_text("\u{2061}"); |
173 | 0 | new_children.push(invisible_apply_function); |
174 | 0 | let mrow_wrapper = create_mathml_element(&doc, "mrow"); |
175 | 0 | mrow_wrapper.set_attribute_value("id", (op_name_id.to_string() + "3").as_str()); |
176 | 0 | mrow_wrapper.append_children(children); |
177 | 0 | new_children.push(ChildOfElement::Element(mrow_wrapper)); |
178 | 0 | mathml.replace_children(new_children); |
179 | 0 | if fixity.is_empty() { |
180 | 0 | mathml.set_attribute_value(INTENT_PROPERTY, ":function:"); |
181 | 0 | } |
182 | | }, |
183 | | } |
184 | 0 | return mathml; |
185 | | |
186 | 0 | fn create_operator_element<'a>(intent_name: &str, fixity: &str, id: &str, id_inc: usize, doc: &Document<'a>) -> ChildOfElement<'a> { |
187 | 0 | let intent_name = intent_speech_for_name(intent_name, &PreferenceManager::get().borrow().pref_to_string("NavMode"), fixity); |
188 | 0 | let element = create_mathml_element(doc, &intent_name); |
189 | 0 | element.set_attribute_value("id", &format!("{id}-fixity-{id_inc}")); |
190 | 0 | element.set_attribute_value(MATHML_FROM_NAME_ATTR, "mo"); |
191 | 0 | return ChildOfElement::Element(element); |
192 | 0 | } |
193 | 0 | } |
194 | 363 | } |
195 | | |
196 | 340 | pub fn intent_speech_for_name(intent_name: &str, verbosity: &str, fixity: &str) -> String { |
197 | 340 | crate::definitions::SPEECH_DEFINITIONS.with(|definitions| { |
198 | 340 | let definitions = definitions.borrow(); |
199 | 340 | if let Some(intent_name_pattern294 ) = definitions.get_hashmap("IntentMappings").unwrap().get(intent_name) { |
200 | | // Split the pattern is: |
201 | | // fixity-def [|| fixity-def]* |
202 | | // fixity-def := fixity=[open;] verbosity[; close] |
203 | | // verbosity := terse | medium | verbose |
204 | 396 | if let Some(matched_intent294 ) = intent_name_pattern.split("||")294 .find294 (|&entry| entry.trim().starts_with(fixity)) { |
205 | 294 | let (_, matched_intent) = matched_intent.split_once("=").unwrap_or_default(); |
206 | 294 | let parts = matched_intent.trim().split(";").collect::<Vec<&str>>(); |
207 | 294 | let mut operator_names = (if parts.len() > 1 {parts[1]129 } else {parts[0]165 }).split(":").collect::<Vec<&str>>(); |
208 | 294 | match operator_names.len() { |
209 | 236 | 1 => return operator_names[0].trim().to_string(), |
210 | | 2 | 3 => { |
211 | 58 | if operator_names.len() == 2 { |
212 | 0 | warn!("Intent '{intent_name}' has only two operator names, but should have three"); |
213 | 0 | operator_names.push(operator_names[1]); |
214 | 58 | } |
215 | 58 | let intent_word = match verbosity { |
216 | 58 | "Terse" => operator_names[0]2 , |
217 | 56 | "Medium" => operator_names[1]54 , |
218 | 2 | _ => operator_names[2], |
219 | | }; |
220 | 58 | return intent_word.trim().to_string(); |
221 | | }, |
222 | | _ => { |
223 | 0 | error!("Intent '{}' has too many ({}) operator names, should only have 2", intent_name, operator_names.len()); |
224 | 0 | return intent_name.to_string(); |
225 | | }, |
226 | | } |
227 | 0 | } |
228 | 46 | }; |
229 | 46 | return intent_name.replace(['_', '-'], " ").trim().to_string(); |
230 | 340 | }) |
231 | 340 | } |
232 | | |
233 | | |
234 | | |
235 | | // intent := self-property-list | expression |
236 | | // self-property-list := property+ S |
237 | | // expression := S ( term property* | application ) S |
238 | | // term := concept-or-literal | number | reference |
239 | | // concept-or-literal := NCName |
240 | | // number := '-'? \d+ ( '.' \d+ )? |
241 | | // reference := '$' NCName |
242 | | // application := expression '(' arguments? S ')' |
243 | | // arguments := expression ( ',' expression )* |
244 | | // property := S ':' NCName |
245 | | // S := [ \t\n\r]* |
246 | | |
247 | | // The practical restrictions of NCName are that it cannot contain several symbol characters like |
248 | | // !, ", #, $, %, &, ', (, ), *, +, ,, /, :, ;, <, =, >, ?, @, [, \, ], ^, `, {, |, }, ~, and whitespace characters |
249 | | // Furthermore an NCName cannot begin with a number, dot or minus character although they can appear later in an NCName. |
250 | | // NC_NAME defined in www.w3.org/TR/REC-xml/#sec-common-syn, but is complicated |
251 | | // We follow NC_NAME for the basic latin block, but then allow everything |
252 | 2 | static CONCEPT_OR_LITERAL: LazyLock<Regex> = LazyLock::new(|| { |
253 | 2 | Regex::new(r#"^[^\s\u{0}-\u{40}\[\\\]^`\u{7B}-\u{BF}][^\s\u{0}-\u{2C}/:;<=>?@\[\\\]^`\u{7B}-\u{BF}]*"# // NC_NAME but simpler |
254 | 2 | ).unwrap() |
255 | 2 | }); |
256 | 2 | static PROPERTY: LazyLock<Regex> = LazyLock::new(|| { |
257 | 2 | Regex::new(r#"^:[^\s\u{0}-\u{40}\[\\\]^`\u{7B}-\u{BF}][^\s\u{0}-\u{2C}/:;<=>?@\[\\\]^`\u{7B}-\u{BF}]*"# // : NC_NAME |
258 | 2 | ).unwrap() |
259 | 2 | }); |
260 | 2 | static ARG_REF: LazyLock<Regex> = LazyLock::new(|| { |
261 | 2 | Regex::new(r#"^\$[^\s\u{0}-\u{40}\[\\\]^`\u{7B}-\u{BF}][^\s\u{0}-\u{2C}/:;<=>?@\[\\\]^`\u{7B}-\u{BF}]*"# // $ NC_NAME |
262 | 2 | ).unwrap() |
263 | 2 | }); |
264 | 2 | static NUMBER: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"^-?[0-9]+(\.[0-9]+)?"#).unwrap()); |
265 | | |
266 | | static TERMINALS_AS_U8: [u8; 3] = [b'(', b',', b')']; |
267 | | // static TERMINALS: [char; 3] = ['(', ',',')']; |
268 | | |
269 | | // 'i -- "i" for the lifetime of the INTENT_ATTR string |
270 | | #[derive(Debug, PartialEq, Eq, Clone)] |
271 | | enum Token<'i> { |
272 | | Terminal(&'i str), // "(", ",", ")" |
273 | | Property(&'i str), |
274 | | ArgRef(&'i str), |
275 | | ConceptOrLiteral(&'i str), |
276 | | Number(&'i str), |
277 | | None, // out of characters |
278 | | } |
279 | | |
280 | | impl fmt::Display for Token<'_> { |
281 | 3 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
282 | 3 | return write!(f, "{}", |
283 | 3 | match self { |
284 | 3 | Token::Terminal(str) => format!("Terminal('{str}')"), |
285 | 0 | Token::Property(str) => format!("Property({str})"), |
286 | 0 | Token::ArgRef(str) => format!("ArgRef({str})"), |
287 | 0 | Token::ConceptOrLiteral(str) => format!("Literal({str})"), |
288 | 0 | Token::Number(str) => format!("Number({str})"), |
289 | 0 | Token::None => "None".to_string(), |
290 | | } |
291 | | ); |
292 | 3 | } |
293 | | } |
294 | | |
295 | | impl Token<'_> { |
296 | 3.64k | fn is_terminal(&self, terminal: &str) -> bool { |
297 | 3.64k | if let Token::Terminal(value1.02k ) = *self { |
298 | 1.02k | return value == terminal; |
299 | | } else { |
300 | 2.61k | return false; |
301 | | } |
302 | 3.64k | } |
303 | | |
304 | 5.21k | fn as_str(&self) -> &str { |
305 | 5.21k | return match self { |
306 | 0 | Token::Terminal(str) => str, |
307 | 4.79k | Token::Property(str) => str, |
308 | 226 | Token::ArgRef(str) => str, |
309 | 161 | Token::ConceptOrLiteral(str) => str, |
310 | 29 | Token::Number(str) => str, |
311 | 0 | Token::None => "", |
312 | | } |
313 | 5.21k | } |
314 | | } |
315 | | |
316 | | struct LexState<'i> { |
317 | | token: Token<'i>, |
318 | | remaining_str: &'i str, // always trimmed |
319 | | } |
320 | | |
321 | | impl fmt::Display for LexState<'_> { |
322 | 0 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
323 | 0 | return writeln!(f, "token: {}, remaining: '{}'", self.token, self.remaining_str); |
324 | 0 | } |
325 | | } |
326 | | |
327 | | impl<'i> LexState<'i> { |
328 | 2.50k | fn init(str: &'i str) -> Result<LexState<'i>> { |
329 | 2.50k | let mut lex_state = LexState { token: Token::None, remaining_str: str.trim() }; |
330 | 2.50k | lex_state.get_next()?0 ; |
331 | 2.50k | return Ok(lex_state); |
332 | 2.50k | } |
333 | | |
334 | | // helper function for LexState -- do not call outside of the impl |
335 | 2.82k | fn set_token(&mut self, str: &'i str) -> Result<()> { |
336 | | // Note: 'str' is already trimmed |
337 | 2.82k | if str.is_empty() { |
338 | 0 | self.token = Token::None; |
339 | 2.82k | } else if TERMINALS_AS_U8.contains(&str.as_bytes()[0]) { |
340 | 0 | self.token = Token::Terminal(str); |
341 | 2.82k | } else if let Some(matched_property2.40k ) = PROPERTY.find(str) { |
342 | 2.40k | self.token = Token::Property(matched_property.as_str()); |
343 | 2.40k | } else if let Some(matched_arg_ref226 ) = ARG_REF416 .find(str) { |
344 | 226 | self.token = Token::ArgRef(matched_arg_ref.as_str()); |
345 | 226 | } else if let Some(matched_literal161 ) = CONCEPT_OR_LITERAL190 .find(str) { |
346 | 161 | self.token = Token::ConceptOrLiteral(matched_literal.as_str()); |
347 | 161 | } else if let Some(matched_number29 ) = NUMBER29 .find(str) { |
348 | 29 | self.token = Token::Number(matched_number.as_str()); |
349 | 29 | } else { |
350 | 0 | bail!("Illegal 'intent' syntax: {}", str); |
351 | | } |
352 | 2.82k | return Ok( () ); |
353 | 2.82k | } |
354 | | |
355 | 5.69k | fn get_next(&mut self) -> Result<&Token<'_>> { |
356 | 5.69k | if self.remaining_str.is_empty() { |
357 | 2.48k | self.token = Token::None; |
358 | 3.21k | } else if TERMINALS_AS_U8.contains(&self.remaining_str.as_bytes()[0]) { |
359 | 391 | self.token = Token::Terminal(&self.remaining_str[..1]); |
360 | 391 | self.remaining_str = self.remaining_str[1..].trim_start(); |
361 | 391 | } else { |
362 | 2.82k | self.set_token(self.remaining_str)?0 ; |
363 | 2.82k | self.remaining_str = self.remaining_str[self.token.as_str().len()..].trim_start(); |
364 | | } |
365 | 5.69k | return Ok(&self.token); |
366 | 5.69k | } |
367 | | |
368 | 3.64k | fn is_terminal(&self, terminal: &str) -> bool { |
369 | 3.64k | return self.token.is_terminal(terminal); |
370 | 3.64k | } |
371 | | } |
372 | | |
373 | 2.74k | fn build_intent<'b, 'r, 'c, 's:'c, 'm:'c>(rules_with_context: &'r mut SpeechRulesWithContext<'c,'s,'m>, |
374 | 2.74k | lex_state: &mut LexState<'b>, |
375 | 2.74k | mathml: Element<'c>, |
376 | 2.74k | intent_offset: &mut u32) -> Result<Element<'m>> { |
377 | | // intent := self-property-list | expression |
378 | | // self-property-list := property+ S |
379 | | // expression := S ( term property* | application ) S |
380 | | // term := concept-or-literal | number | reference |
381 | | // concept-or-literal := NCName |
382 | | // number := '-'? \d+ ( '.' \d+ )? |
383 | | // reference := '$' NCName |
384 | | // application := expression '(' arguments? S ')' |
385 | | // |
386 | | // When we flatten intent we have this implementation looking for Tokens or '(' [for application] |
387 | | // Essentially, the grammar we deal with here is: |
388 | | // intent := property+ | (concept-or-literal | number | reference) property* '('? |
389 | | // debug!(" start build_intent: state: {}", lex_state); |
390 | 2.74k | let doc = rules_with_context.get_document(); |
391 | | let mut intent; |
392 | 2.74k | debug!(" build_intent: start mathml name={}, intent_offset={}", name0 (mathml0 ), intent_offset); |
393 | 2.74k | match lex_state.token { |
394 | | Token::Property(_) => { |
395 | | // We only have a property -- we want to keep this tag/element |
396 | | // There are two paths: |
397 | | // 1. If there is a function call, then the children are dealt with there |
398 | | // 2. If there is *no* function call, then the children are kept, which means we return to pattern matching |
399 | | // Note: to avoid infinite loop, we need to remove the 'intent' so we don't end up back here; we put it back later |
400 | 2.33k | let properties = get_properties(lex_state)?0 ; // advance state to see if funcall |
401 | 2.33k | if lex_state.is_terminal("(") { |
402 | 2 | intent = create_mathml_element(&doc, name(mathml)); |
403 | 2 | intent.set_attribute_value(INTENT_PROPERTY, &properties); |
404 | 2 | intent.set_attribute_value(MATHML_FROM_NAME_ATTR, name(mathml)); |
405 | 2 | intent.set_attribute_value("id", mathml.attribute_value("id") |
406 | 2 | .ok_or_else(|| anyhow!0 ("no id on intent function name"))?0 ); |
407 | | } else { |
408 | 2.32k | let saved_intent = mathml.attribute_value(INTENT_ATTR).unwrap(); |
409 | 2.32k | mathml.remove_attribute(INTENT_ATTR); |
410 | 2.32k | mathml.set_attribute_value(INTENT_PROPERTY, &properties); // needs to be set before the pattern match |
411 | 2.32k | intent = rules_with_context.match_pattern::<Element<'m>>(mathml)?0 ; |
412 | | // debug!("Intent after pattern match:\n{}", mml_to_string(intent)); |
413 | 2.32k | mathml.set_attribute_value(INTENT_ATTR, saved_intent); |
414 | | } |
415 | 2.33k | add_fixity(intent); |
416 | 2.33k | return Ok(intent); // if we start with properties, then there can only be properties |
417 | | }, |
418 | 161 | Token::ConceptOrLiteral(word) | Token::Number(word28 ) => { |
419 | 189 | let leaf_name = if let Token::Number(_) = lex_state.token {"mn"28 } else {"mi"161 }; |
420 | 189 | intent = create_mathml_element(&doc, leaf_name); |
421 | | // if the str is part of a larger intent and not the head (e.g., "a" in "f($x, a)", but not the "f" in it), then it is "made up" |
422 | | // debug!(" Token::ConceptOrLiteral, word={}, leaf_name={}", word, leaf_name); |
423 | 189 | intent.set_attribute_value(MATHML_FROM_NAME_ATTR, |
424 | 189 | if word == mathml.attribute_value(INTENT_ATTR).unwrap_or_default() {name(mathml)30 } else {leaf_name159 }); |
425 | 189 | intent.set_text(word); // '-' and '_' get removed by the rules. |
426 | 189 | if let Some(id136 ) = mathml.attribute_value("id") { |
427 | 136 | intent.set_attribute_value("id", &format!("{}-literal-{}", id, intent_offset)); |
428 | 136 | *intent_offset += 1; |
429 | 136 | }53 |
430 | 189 | lex_state.get_next()?0 ; |
431 | 189 | if let Token::Property(_) = lex_state.token { |
432 | 60 | let properties = get_properties(lex_state)?0 ; |
433 | 60 | intent.set_attribute_value(INTENT_PROPERTY, &properties); |
434 | 129 | } |
435 | | }, |
436 | 223 | Token::ArgRef(word) => { |
437 | 223 | intent = match find_arg(rules_with_context, &word[1..], mathml, intent_offset, true, false)?1 { |
438 | 221 | Some(e) => { |
439 | 221 | lex_state.get_next()?0 ; |
440 | 221 | e |
441 | | }, |
442 | 1 | None => bail!("intent arg '{}' not found", word), |
443 | | }; |
444 | 221 | if let Token::Property(_) = lex_state.token { |
445 | 3 | let properties = get_properties(lex_state)?0 ; |
446 | 3 | intent.set_attribute_value(INTENT_PROPERTY, &properties); |
447 | 218 | } |
448 | | }, |
449 | 3 | _ => bail!("Illegal 'intent' syntax: found {}", lex_state.token), |
450 | | }; |
451 | 410 | if lex_state.is_terminal("(") { |
452 | 136 | intent = build_function(intent, rules_with_context, lex_state, mathml, intent_offset)?15 ; |
453 | 274 | } |
454 | | // debug!(" end build_intent: state: {} piece: {}", lex_state, mml_to_string(intent)); |
455 | 395 | add_fixity(intent); |
456 | 395 | return Ok(intent); |
457 | 2.74k | } |
458 | | |
459 | | pub const INTENT_PROPERTY: &str = "data-intent-property"; |
460 | | |
461 | | /// Get all the properties, stopping we don't have any more |
462 | | /// Returns the string of the properties terminated with an additional ":" |
463 | 2.39k | fn get_properties(lex_state: &mut LexState) -> Result<String> { |
464 | | // return the 'hint' leaving the state |
465 | 2.39k | assert!(matches!(lex_state.token, Token::Property(str) if str.starts_with(':'))); |
466 | 2.39k | let mut properties = String::with_capacity(60); |
467 | 2.39k | properties.push_str(lex_state.token.as_str()); |
468 | | loop { |
469 | 2.40k | let token = lex_state.get_next()?0 ; |
470 | 2.40k | if let Token::Property(property11 ) = token { |
471 | 11 | properties.push_str(property); |
472 | 11 | } else { |
473 | 2.39k | properties.push(':'); |
474 | | // debug!(" get_properties: returns {}", properties); |
475 | 2.39k | return Ok(simplify_fixity_properties(&properties)); |
476 | | } |
477 | | } |
478 | 2.39k | } |
479 | | |
480 | | /// Build a function 'f(...)' where '...' can be empty |
481 | | /// |
482 | | /// Also handles nested functions like f(...)(...) |
483 | | /// |
484 | | /// Start state: at '(' |
485 | | /// |
486 | | /// End state: after ')' |
487 | 136 | fn build_function<'b, 'r, 'c, 's:'c, 'm:'c>( |
488 | 136 | function_name: Element<'m>, |
489 | 136 | rules_with_context: &'r mut SpeechRulesWithContext<'c,'s,'m>, |
490 | 136 | lex_state: &mut LexState<'b>, |
491 | 136 | mathml: Element<'c>, |
492 | 136 | intent_offset: &mut u32) -> Result<Element<'m>> { |
493 | | // debug!(" start build_function: name: {}, state: {}", name(function_name), lex_state); |
494 | | // application := intent '(' arguments? S ')' where 'function_name' is 'intent' |
495 | 136 | assert!(lex_state.is_terminal("(")); |
496 | 136 | let mut function = function_name; |
497 | 136 | function.set_attribute_value(MATHML_FROM_NAME_ATTR, name(mathml)); |
498 | 260 | while lex_state.is_terminal("(") { |
499 | 139 | lex_state.get_next()?0 ; |
500 | 139 | if lex_state.is_terminal(")") { |
501 | | // grammar requires at least one argument |
502 | 9 | bail!("Illegal 'intent' syntax: missing argument for intent name '{}'", name(function_name)); |
503 | 130 | } |
504 | 130 | let children125 = build_arguments(rules_with_context, lex_state, mathml, intent_offset)?5 ; |
505 | 125 | function = lift_function_name(rules_with_context.get_document(), function, children); |
506 | | |
507 | 125 | if !lex_state.is_terminal(")") { |
508 | 1 | bail!("Illegal 'intent' syntax: missing ')' for intent name '{}'", name(function_name)); |
509 | 124 | } |
510 | 124 | lex_state.get_next()?0 ; |
511 | | } |
512 | | |
513 | | // debug!(" end build_function/# children: {}, #state: {} ..[bfa] function name: {}", |
514 | | // function.children().len(), lex_state, mml_to_string(function)); |
515 | 121 | return Ok(function); |
516 | 136 | } |
517 | | |
518 | | // process all the args of a function |
519 | | // Start state: after '(' |
520 | | // End state: on ')' |
521 | 130 | fn build_arguments<'b, 'r, 'c, 's:'c, 'm:'c>( |
522 | 130 | rules_with_context: &'r mut SpeechRulesWithContext<'c,'s,'m>, |
523 | 130 | lex_state: &mut LexState<'b>, |
524 | 130 | mathml: Element<'c>, |
525 | 130 | intent_offset: &mut u32) -> Result<Vec<Element<'m>>> { |
526 | | // arguments := intent ( ',' intent )*' |
527 | | // debug!(" start build_args state: {}", lex_state); |
528 | | |
529 | | // there is at least one arg |
530 | 130 | let mut children = Vec::with_capacity(lex_state.remaining_str.len()/3 + 1); // conservative estimate ('3' - "$x,"); |
531 | 130 | children.push127 ( build_intent(rules_with_context, lex_state, mathml, intent_offset)?3 ); // arg before ',' |
532 | | // debug!(" build_args: # children {}; state: {}", children.len(), lex_state); |
533 | | |
534 | 239 | while lex_state.is_terminal(",") { |
535 | 114 | lex_state.get_next()?0 ; |
536 | 114 | children.push112 ( build_intent(rules_with_context, lex_state, mathml, intent_offset)?2 ); // arg before ',' |
537 | | // debug!(" build_args, # children {}; state: {}", children.len(), lex_state); |
538 | | } |
539 | | |
540 | | // debug!(" end build_args, # children {}; state: {}", children.len(), lex_state); |
541 | 125 | return Ok(children); |
542 | 130 | } |
543 | | |
544 | | /// lift the children up to LITERAL_NAME |
545 | 125 | fn lift_function_name<'m>(doc: Document<'m>, function_name: Element<'m>, children: Vec<Element<'m>>) -> Element<'m> { |
546 | | // debug!(" lift_function_name: {}", name(function_name)); |
547 | | // debug!(" lift_function_name: {}", mml_to_string(function_name)); |
548 | 125 | if name(function_name) == "mi" || name(function_name) == "mn"4 { // FIX -- really want to test for all leaves, but not "data-from-mathml" |
549 | | // simple/normal case of f(x,y) |
550 | | // don't want to say that this is a leaf -- doing so messes up because it potentially has children |
551 | 121 | set_mathml_name(function_name, as_text(function_name)); |
552 | 121 | function_name.set_text(""); |
553 | 121 | function_name.replace_children(children); |
554 | 129 | if name(function_name)121 .find121 (|ch| ch!='_' && ch!='-'108 ).is_none121 () { |
555 | 14 | let properties = function_name.attribute_value(INTENT_PROPERTY).unwrap_or(":").to_owned(); |
556 | 14 | function_name.set_attribute_value(INTENT_PROPERTY, &(properties + "silent:")); |
557 | 107 | } |
558 | 121 | return function_name; |
559 | 4 | } else if function_name.children().is_empty() { |
560 | | // "... :property(...)" -- no function name |
561 | 0 | function_name.replace_children(children); |
562 | 0 | return function_name; |
563 | | } else { |
564 | | // more complicated case of nested name: f(x)(y,z) |
565 | | // create an apply_function(f(x), y, z) |
566 | 4 | let result = create_mathml_element(&doc, IMPLICIT_FUNCTION_NAME); |
567 | 4 | result.set_attribute_value(MATHML_FROM_NAME_ATTR, "mrow"); |
568 | 4 | result.append_child(function_name); |
569 | 4 | result.append_children(children); |
570 | 4 | return result; |
571 | | } |
572 | 125 | } |
573 | | |
574 | | |
575 | | /// look for @arg=name in mathml |
576 | | /// if 'check_intent', then look at an @intent for this element (typically false for non-recursive calls) |
577 | 946 | fn find_arg<'r, 'c, 's:'c, 'm:'c>( |
578 | 946 | rules_with_context: &'r mut SpeechRulesWithContext<'c,'s,'m>, |
579 | 946 | name: &str, |
580 | 946 | mathml: Element<'c>, |
581 | 946 | intent_offset: &mut u32, |
582 | 946 | skip_self: bool, |
583 | 946 | no_check_inside: bool) -> Result<Option<Element<'m>>> { |
584 | | // debug!("Looking for '{}' in\n{}", name, mml_to_string(mathml)); |
585 | 946 | if !skip_self && |
586 | 723 | let Some(arg_val411 ) = mathml.attribute_value("arg") { |
587 | | // debug!("looking for '{}', found arg='{}'", name, arg_val); |
588 | 411 | if name == arg_val { |
589 | | // check to see if this mathml has an intent value -- if so the value is the value of its intent value |
590 | 222 | if let Some(intent_str28 ) = mathml.attribute_value(INTENT_ATTR) { |
591 | 28 | let mut lex_state = LexState::init(intent_str.trim())?0 ; |
592 | 28 | return Ok( Some( build_intent(rules_with_context, &mut lex_state, mathml, intent_offset)?1 ) ); |
593 | | } else { |
594 | 194 | return Ok( Some( rules_with_context.match_pattern::<Element<'m>>(mathml)?0 ) ); |
595 | | } |
596 | 189 | } else if no_check_inside { |
597 | 189 | return Ok(None); // don't look inside 'arg' |
598 | 0 | } |
599 | 535 | } |
600 | | |
601 | 535 | if no_check_inside && mathml.attribute_value(INTENT_ATTR)312 .is_some312 () { |
602 | 2 | return Ok(None); // don't look inside 'intent' |
603 | 533 | } |
604 | | |
605 | 533 | if is_leaf(mathml){ |
606 | 121 | return Ok(None); |
607 | 412 | } |
608 | | |
609 | 723 | for child in mathml412 .children412 () { |
610 | 723 | let child = as_element(child); |
611 | 723 | if let Some(element396 ) = find_arg(rules_with_context, name, child, intent_offset, false, true)?1 { |
612 | 396 | return Ok( Some(element) ); |
613 | 326 | } |
614 | | } |
615 | | |
616 | 15 | return Ok(None); // not present |
617 | 946 | } |
618 | | |
619 | | #[cfg(test)] |
620 | | mod tests { |
621 | | #[allow(unused_imports)] |
622 | | use crate::init_logger; |
623 | | use log::debug; |
624 | | use sxd_document::parser; |
625 | | |
626 | | |
627 | 27 | fn test_intent(mathml: &str, target: &str, intent_error_recovery: &str) -> bool { |
628 | | use crate::interface::*; |
629 | | use crate::pretty_print::mml_to_string; |
630 | | // this forces initialization |
631 | 27 | crate::interface::set_rules_dir(super::super::abs_rules_dir_path()).unwrap(); |
632 | | // crate::speech::SpeechRules::initialize_all_rules().unwrap(); |
633 | 27 | set_preference("IntentErrorRecovery", intent_error_recovery).unwrap(); |
634 | 27 | set_preference("SpeechStyle", "SimpleSpeak").unwrap(); // avoids possibility of "LiteralSpeak" |
635 | 27 | let package1 = &parser::parse(mathml).expect("Failed to parse test input"); |
636 | 27 | let mathml = get_element(package1); |
637 | 27 | trim_element(mathml, false); |
638 | 27 | debug!("test:\n{}", mml_to_string0 (mathml0 )); |
639 | | |
640 | 27 | let package2 = &parser::parse(target).expect("Failed to parse target input"); |
641 | 27 | let target = get_element(package2); |
642 | 27 | trim_element(target,true); |
643 | 27 | debug!("target:\n{}", mml_to_string0 (target0 )); |
644 | | |
645 | 27 | let result18 = match crate::speech::intent_from_mathml(mathml, package2.as_document()) { |
646 | 18 | Ok(e) => e, |
647 | 9 | Err(e) => { |
648 | 9 | debug!("{}", crate::interface::errors_to_string0 (&e0 )); |
649 | 9 | return false; // could be intentional failure |
650 | | } |
651 | | }; |
652 | 18 | debug!("result:\n{}", mml_to_string0 (result0 )); |
653 | 18 | match is_same_element(result, target, &[]) { |
654 | 18 | Ok(_) => return true, |
655 | 0 | Err(e) => panic!("{}:\nresult: {}target: {}", e, mml_to_string(result), mml_to_string(target)), |
656 | | } |
657 | 27 | } |
658 | | |
659 | | #[test] |
660 | 1 | fn infer_binomial() { |
661 | 1 | let mathml = "<mrow intent='binomial($n, $m)'> |
662 | 1 | <mo>(</mo> |
663 | 1 | <mfrac linethickness='0'> <mn arg='n'>7</mn> <mn arg='m'>3</mn> </mfrac> |
664 | 1 | <mo>)</mo> |
665 | 1 | </mrow>"; |
666 | 1 | let intent = "<binomial data-from-mathml='mrow' data-intent-property=':infix:'> <mn data-from-mathml='mn' arg='n'>7</mn> <mn data-from-mathml='mn' arg='m'>3</mn> </binomial>"; |
667 | 1 | assert!(test_intent(mathml, intent, "Error")); |
668 | 1 | } |
669 | | |
670 | | #[test] |
671 | 1 | fn infer_binomial_intent_arg() { |
672 | 1 | let mathml = "<msubsup intent='$op($n,$m)'> |
673 | 1 | <mi arg='op' intent='binomial'>C</mi> |
674 | 1 | <mi arg='n'>n</mi> |
675 | 1 | <mi arg='m'>m</mi> |
676 | 1 | </msubsup>"; |
677 | 1 | let intent = "<binomial data-from-mathml='msubsup' data-intent-property=':infix:'> <mi data-from-mathml='mi' arg='n'>n</mi> <mi data-from-mathml='mi' arg='m'>m</mi></binomial>"; |
678 | 1 | assert!(test_intent(mathml, intent, "Error")); |
679 | 1 | } |
680 | | |
681 | | #[test] |
682 | 1 | fn silent_underscore() { |
683 | 1 | let mathml = "<mrow><mi intent='__-'>silent</mi><mo>+</mo><mi>e</mi></mrow>"; |
684 | 1 | let intent = "<mrow data-from-mathml='mrow'> |
685 | 1 | <mi data-from-mathml='mi'>__-</mi> |
686 | 1 | <mo data-from-mathml='mo'>+</mo> |
687 | 1 | <mi data-from-mathml='mi'>e</mi> |
688 | 1 | </mrow>"; |
689 | 1 | assert!(test_intent(mathml, intent, "Error")); |
690 | 1 | } |
691 | | |
692 | | |
693 | | #[test] |
694 | 1 | fn silent_underscore_function() { |
695 | 1 | let mathml = "<mrow intent='__-_(speak, this)'></mrow>"; |
696 | 1 | let intent = "<__-_ data-from-mathml='mrow' data-intent-property=':silent:'> |
697 | 1 | <mi data-from-mathml='mi'>speak</mi> |
698 | 1 | <mi data-from-mathml='mi'>this</mi> |
699 | 1 | </__-_>"; |
700 | 1 | assert!(test_intent(mathml, intent, "Error")); |
701 | 1 | } |
702 | | |
703 | | #[test] |
704 | 1 | fn intent_multiple_properties() { |
705 | 1 | let mathml = "<mrow intent='foo:silent:int(bar:positive-int:int, $a:foo:bar:foo-bar, $b:number)'> |
706 | 1 | <mi arg='a'>a</mi> |
707 | 1 | <mo arg='p' intent='plus'>+</mo> |
708 | 1 | <mi arg='b' intent=':negative-int:int'>b</mi> |
709 | 1 | </mrow>"; |
710 | 1 | let intent = "<foo data-intent-property=':int:silent:' data-from-mathml='mrow'> |
711 | 1 | <mi data-from-mathml='mi' data-intent-property=':positive-int:int:'>bar</mi> |
712 | 1 | <mi data-from-mathml='mi' arg='a' data-intent-property=':foo:bar:foo-bar:'>a</mi> |
713 | 1 | <mi data-from-mathml='mi' arg='b' data-intent-property=':number:'>b</mi> |
714 | 1 | </foo>"; |
715 | 1 | assert!(test_intent(mathml, intent, "Error")); |
716 | 1 | } |
717 | | #[test] |
718 | 1 | fn intent_nest_no_arg_call() { |
719 | 1 | let mathml = "<mrow intent='foo(bar())'> |
720 | 1 | <mi arg='a'>a</mi> |
721 | 1 | <mo arg='p' intent='plus'>+</mo> |
722 | 1 | <mi arg='b'>b</mi> |
723 | 1 | <mo arg='f' intent='factorial'>!</mo> |
724 | 1 | </mrow>"; |
725 | 1 | let intent = "<foo><bar></bar></foo>"; |
726 | 1 | assert!(!test_intent(mathml, intent, "Error")); |
727 | 1 | } |
728 | | |
729 | | #[test] |
730 | 1 | fn intent_hints() { |
731 | 1 | let mathml = "<mrow intent='foo:silent(bar:postfix(3))'> |
732 | 1 | <mi arg='a'>a</mi> |
733 | 1 | <mo arg='p' intent='plus'>+</mo> |
734 | 1 | <mi arg='b'>b</mi> |
735 | 1 | <mo arg='f' intent='factorial'>!</mo> |
736 | 1 | </mrow>"; |
737 | 1 | let intent = "<foo data-intent-property=':silent:' data-from-mathml='mrow'> |
738 | 1 | <bar data-intent-property=':postfix:' data-from-mathml='mrow'> |
739 | 1 | <mn data-from-mathml='mn'>3</mn> |
740 | 1 | </bar> |
741 | 1 | </foo>"; |
742 | 1 | assert!(test_intent(mathml, intent, "Error")); |
743 | 1 | } |
744 | | |
745 | | #[test] |
746 | 1 | fn intent_hints_and_type() { |
747 | 1 | let mathml = "<mrow intent='foo:is-foolish:function($b)'> |
748 | 1 | <mi arg='a'>a</mi> |
749 | 1 | <mo arg='p' intent='plus'>+</mo> |
750 | 1 | <mi intent='b:int' arg='b'>b</mi> |
751 | 1 | <mo arg='f' intent='factorial'>!</mo> |
752 | 1 | </mrow>"; |
753 | 1 | let intent = "<foo data-intent-property=':is-foolish:function:' data-from-mathml='mrow'> |
754 | 1 | <mi data-intent-property=':int:' data-from-mathml='mi'>b</mi> |
755 | 1 | </foo>"; |
756 | 1 | assert!(test_intent(mathml, intent, "Error")); |
757 | 1 | } |
758 | | |
759 | | #[test] |
760 | 1 | fn intent_in_intent_first_arg() { |
761 | 1 | let mathml = "<mrow intent='p(f(b), a)'> |
762 | 1 | <mi arg='a'>a</mi> |
763 | 1 | <mo arg='p' intent='plus'>+</mo> |
764 | 1 | <mi arg='b'>b</mi> |
765 | 1 | <mo arg='f' intent='factorial'>!</mo> |
766 | 1 | </mrow>"; |
767 | 1 | let intent = "<p data-from-mathml='mrow'> |
768 | 1 | <f data-from-mathml='mrow'> |
769 | 1 | <mi data-from-mathml='mi'>b</mi> |
770 | 1 | </f> |
771 | 1 | <mi data-from-mathml='mi'>a</mi> |
772 | 1 | </p>"; |
773 | 1 | assert!(test_intent(mathml, intent, "Error")); |
774 | 1 | } |
775 | | |
776 | | #[test] |
777 | 1 | fn intent_in_intent_second_arg() { |
778 | 1 | let mathml = "<mrow intent='$p(a,$f(b))'> |
779 | 1 | <mi arg='a'>a</mi> |
780 | 1 | <mo arg='p' intent='plus'>+</mo> |
781 | 1 | <mi arg='b'>b</mi> |
782 | 1 | <mo arg='f' intent='factorial'>!</mo> |
783 | 1 | </mrow>"; |
784 | 1 | let intent = "<plus data-from-mathml='mrow' data-intent-property=':infix:'> |
785 | 1 | <mi data-from-mathml='mi'>a</mi> |
786 | 1 | <factorial data-from-mathml='mrow'> |
787 | 1 | <mi data-from-mathml='mi'>b</mi> |
788 | 1 | </factorial> |
789 | 1 | </plus>"; |
790 | 1 | assert!(test_intent(mathml, intent, "Error")); |
791 | 1 | } |
792 | | |
793 | | #[test] |
794 | 1 | fn intent_with_whitespace() { |
795 | 1 | let mathml = "<mrow intent=' $arrow ( $a , $b,$c ) '> |
796 | 1 | <mi arg='a'>A</mi> |
797 | 1 | <mover> |
798 | 1 | <mo movablelimits='false' arg='arrow' intent='map'>⟶</mo> |
799 | 1 | <mo arg='U2245' intent='congruence'>≅</mo> |
800 | 1 | </mover> |
801 | 1 | <mi arg='b'>B</mi> |
802 | 1 | <mi arg='c'>C</mi> |
803 | 1 | </mrow>"; |
804 | 1 | let intent = "<map data-from-mathml='mrow'> <mi data-from-mathml='mi' arg='a'>A</mi> <mi data-from-mathml='mi' arg='b'>B</mi> <mi data-from-mathml='mi' arg='c'>C</mi> </map>"; |
805 | 1 | assert!(test_intent(mathml, intent, "Error")); |
806 | 1 | } |
807 | | |
808 | | #[test] |
809 | 1 | fn intent_template_at_toplevel() { |
810 | 1 | let mathml = "<msup intent='$H $n'> |
811 | 1 | <mi arg='H' mathvariant='normal'>H</mi> |
812 | 1 | <mn arg='n'>2</mn> |
813 | 1 | </msup>"; |
814 | 1 | let intent = "<mrow><mi arg='H' mathvariant='normal'>H</mi><mn arg='n'>2</mn></mrow>"; |
815 | 1 | assert!(!test_intent(mathml, intent, "Error")); |
816 | 1 | } |
817 | | |
818 | | #[test] |
819 | 1 | fn intent_with_nested_indirect_head() { |
820 | 1 | let mathml = "<mrow intent='$op($a,$b)'> |
821 | 1 | <mi arg='a'>A</mi> |
822 | 1 | <mover arg='op' intent='$ra($cong)'> |
823 | 1 | <mo movablelimits='false' arg='ra' intent='map'>⟶</mo> |
824 | 1 | <mo arg='cong' intent='congruence'>≅</mo> |
825 | 1 | </mover> |
826 | 1 | <mi arg='b'>B</mi> |
827 | 1 | </mrow>"; |
828 | 1 | let intent = "<apply-function data-from-mathml='mrow'> |
829 | 1 | <map data-from-mathml='mrow'> |
830 | 1 | <mi data-from-mathml='mo'>congruence</mi> |
831 | 1 | </map> |
832 | 1 | <mi data-from-mathml='mi' arg='a'>A</mi> |
833 | 1 | <mi data-from-mathml='mi' arg='b'>B</mi> |
834 | 1 | </apply-function>"; |
835 | 1 | assert!(test_intent(mathml, intent, "Error")); |
836 | 1 | } |
837 | | |
838 | | #[test] |
839 | 1 | fn intent_with_literals() { |
840 | 1 | let mathml = "<mrow intent='vector(1, 0.0, 0.1, -23, -0.1234, last)'> |
841 | 1 | <mi>x</mi> |
842 | 1 | </mrow>"; |
843 | 1 | let intent = "<vector data-from-mathml='mrow' data-intent-property=':function:'> |
844 | 1 | <mn data-from-mathml='mn'>1</mn> |
845 | 1 | <mn data-from-mathml='mn'>0.0</mn> |
846 | 1 | <mn data-from-mathml='mn'>0.1</mn> |
847 | 1 | <mn data-from-mathml='mn'>-23</mn> |
848 | 1 | <mn data-from-mathml='mn'>-0.1234</mn> |
849 | 1 | <mi data-from-mathml='mi'>last</mi> |
850 | 1 | </vector>"; |
851 | 1 | assert!(test_intent(mathml, intent, "Error")); |
852 | 1 | } |
853 | | |
854 | | #[test] |
855 | 1 | fn intent_with_template_literals() { |
856 | 1 | let mathml = "<mrow intent='1 0.0 0.1 -23 -0.1234 last'> |
857 | 1 | <mi>x</mi> |
858 | 1 | </mrow>"; |
859 | 1 | let intent = "<mrow><mn>1</mn><mn>0.</mn><mn>.1</mn><mn>-23</mn><mn>-.1234</mn><mi>last</mi></mrow>"; |
860 | 1 | assert!(!test_intent(mathml, intent, "Error")); |
861 | 1 | } |
862 | | |
863 | | #[test] |
864 | 1 | fn intent_with_nested_head() { |
865 | 1 | let mathml = "<mrow intent='$ra($cong)($a,$b)'> |
866 | 1 | <mi arg='a'>A</mi> |
867 | 1 | <mover> |
868 | 1 | <mo movablelimits='false' arg='ra' intent='map'>⟶</mo> |
869 | 1 | <mo arg='cong' intent='congruence'>≅</mo> |
870 | 1 | </mover> |
871 | 1 | <mi arg='b'>B</mi> |
872 | 1 | </mrow>"; |
873 | 1 | let intent = "<apply-function data-from-mathml='mrow'> |
874 | 1 | <map data-from-mathml='mrow'> |
875 | 1 | <mi data-from-mathml='mo'>congruence</mi> |
876 | 1 | </map> |
877 | 1 | <mi data-from-mathml='mi' arg='a'>A</mi> |
878 | 1 | <mi data-from-mathml='mi' arg='b'>B</mi> |
879 | 1 | </apply-function>"; |
880 | 1 | assert!(test_intent(mathml, intent, "Error")); |
881 | 1 | } |
882 | | |
883 | | |
884 | | #[test] |
885 | 1 | fn intent_with_nested_head_and_hints() { |
886 | 1 | let mathml = "<mrow intent='pre:prefix(in:infix($a, x))(post:postfix($b))'> |
887 | 1 | <mi arg='a'>A</mi> |
888 | 1 | <mover> |
889 | 1 | <mo intent='map'>⟶</mo> |
890 | 1 | <mo intent='congruence'>≅</mo> |
891 | 1 | </mover> |
892 | 1 | <mi arg='b'>B</mi> |
893 | 1 | </mrow>"; |
894 | 1 | let intent = "<apply-function data-from-mathml='mrow'> |
895 | 1 | <pre data-intent-property=':prefix:' data-from-mathml='mrow'> |
896 | 1 | <in data-intent-property=':infix:' data-from-mathml='mrow'> |
897 | 1 | <mi data-from-mathml='mi' arg='a'>A</mi> |
898 | 1 | <mi data-from-mathml='mi'>x</mi> |
899 | 1 | </in> |
900 | 1 | </pre> |
901 | 1 | <post data-intent-property=':postfix:' data-from-mathml='mrow'> |
902 | 1 | <mi data-from-mathml='mi' arg='b'>B</mi> |
903 | 1 | </post> |
904 | 1 | </apply-function>"; |
905 | 1 | assert!(test_intent(mathml, intent, "Error")); |
906 | 1 | } |
907 | | |
908 | | |
909 | | #[test] |
910 | 1 | fn intent_double_indirect_head() { |
911 | 1 | let mathml = "<mrow intent='$m:prefix($c)($a,$b)'> |
912 | 1 | <mi arg='a'>A</mi> |
913 | 1 | <mover> |
914 | 1 | <mo movablelimits='false' arg='m' intent='map'>⟶</mo> |
915 | 1 | <mo arg='c' intent='congruence'>≅</mo> |
916 | 1 | </mover> |
917 | 1 | <mi arg='b'>B</mi> |
918 | 1 | </mrow>"; |
919 | 1 | let intent = "<apply-function data-from-mathml='mrow'> |
920 | 1 | <map data-intent-property=':prefix:' data-from-mathml='mrow'> |
921 | 1 | <mi data-from-mathml='mo'>congruence</mi> |
922 | 1 | </map> |
923 | 1 | <mi data-from-mathml='mi' arg='a'>A</mi> |
924 | 1 | <mi data-from-mathml='mi' arg='b'>B</mi> |
925 | 1 | </apply-function>"; |
926 | 1 | assert!(test_intent(mathml, intent, "Error")); |
927 | 1 | } |
928 | | |
929 | | #[test] |
930 | 1 | fn intent_missing_open() { |
931 | 1 | let mathml = "<mrow intent='$p $a,$f($b))'> |
932 | 1 | <mi arg='a'>a</mi> |
933 | 1 | <mo arg='p' intent='plus'>+</mo> |
934 | 1 | <mi arg='b'>b</mi> |
935 | 1 | <mo arg='f' intent='factorial'>!</mo> |
936 | 1 | </mrow>"; |
937 | 1 | let intent = "<plus> <mi arg='a'>a</mi> <factorial><mi arg='b'>b</mi></factorial> </plus>"; |
938 | 1 | assert!(!test_intent(mathml, intent, "Error")); |
939 | 1 | } |
940 | | |
941 | | #[test] |
942 | 1 | fn intent_no_comma() { |
943 | 1 | let mathml = "<mrow intent='$p($a $f($b))'> |
944 | 1 | <mi arg='a'>a</mi> |
945 | 1 | <mo arg='p' intent='plus'>+</mo> |
946 | 1 | <mi arg='b'>b</mi> |
947 | 1 | <mo arg='f' intent='factorial'>!</mo> |
948 | 1 | </mrow>"; |
949 | 1 | let intent = "<plus> |
950 | 1 | <mrow> |
951 | 1 | <mi arg='a'>a</mi> |
952 | 1 | <factorial> <mi arg='b'>b</mi> </factorial> |
953 | 1 | </mrow> |
954 | 1 | </plus>"; |
955 | 1 | assert!(!test_intent(mathml, intent, "Error")); |
956 | 1 | } |
957 | | |
958 | | #[test] |
959 | 1 | fn intent_no_arg() { |
960 | 1 | let mathml = "<mrow intent='factorial()'> |
961 | 1 | <mi arg='a'>a</mi> |
962 | 1 | <mo arg='p' intent='plus'>+</mo> |
963 | 1 | <mi arg='b'>b</mi> |
964 | 1 | <mo arg='f' intent='factorial'>!</mo> |
965 | 1 | </mrow>"; |
966 | 1 | let target = "<factorial></factorial>"; |
967 | 1 | assert!(!test_intent(mathml, target, "Error")); |
968 | 1 | } |
969 | | |
970 | | #[test] |
971 | 1 | fn intent_illegal_no_arg() { |
972 | 1 | let mathml = "<mrow intent='factorial(()))'> |
973 | 1 | <mi arg='a'>a</mi> |
974 | 1 | <mo arg='p' intent='plus'>+</mo> |
975 | 1 | <mi arg='b'>b</mi> |
976 | 1 | <mo arg='f' intent='factorial'>!</mo> |
977 | 1 | </mrow>"; |
978 | 1 | let target = "<factorial></factorial>"; |
979 | 1 | assert!(!test_intent(mathml, target, "Error")); |
980 | 1 | } |
981 | | |
982 | | #[test] |
983 | 1 | fn intent_illegal_no_arg_ignore() { |
984 | 1 | let mathml = "<mrow intent='factorial()'> |
985 | 1 | <mi arg='a'>a</mi> |
986 | 1 | <mo arg='p' intent='plus'>+</mo> |
987 | 1 | <mi arg='b'>b</mi> |
988 | 1 | <mo arg='f' intent='factorial'>!</mo> |
989 | 1 | </mrow>"; |
990 | 1 | let target = "<mrow data-from-mathml='mrow' intent='factorial()'> |
991 | 1 | <mi data-from-mathml='mi' arg='a'>a</mi> |
992 | 1 | <mi data-from-mathml='mo'>plus</mi> |
993 | 1 | <mi data-from-mathml='mi' arg='b'>b</mi> |
994 | 1 | <mi data-from-mathml='mo'>factorial</mi> |
995 | 1 | </mrow>"; |
996 | 1 | assert!(test_intent(mathml, target, "IgnoreIntent")); |
997 | 1 | } |
998 | | |
999 | | #[test] |
1000 | 1 | fn intent_illegal_self_ref() { |
1001 | 1 | let mathml = "<mrow intent='foo:is-foolish:function($b)'> |
1002 | 1 | <mi intent='$b:int' arg='b'>b</mi> |
1003 | 1 | </mrow>"; |
1004 | 1 | let target = "<foo data-intent-property=':function:' data-intent-type='is-foolish'><mi data-intent-type='int'>b</mi></foo>"; |
1005 | 1 | assert!(!test_intent(mathml, target, "Error")); |
1006 | 1 | } |
1007 | | |
1008 | | #[test] |
1009 | 1 | fn infer_missing_second_arg() { |
1010 | 1 | let mathml = "<mrow intent='binomial($n,)'> |
1011 | 1 | <mo>(</mo> |
1012 | 1 | <mfrac linethickness='0'> <mn arg='n'>7</mn> <mn arg='m'>3</mn> </mfrac> |
1013 | 1 | <mo>)</mo> |
1014 | 1 | </mrow>"; |
1015 | 1 | let target = "<binomial data-intent-property='binomial($n,)'> \n |
1016 | 1 | <mn data-from-mathml='mn' arg='n'>7</mn> <mn data-from-mathml='mn' arg='m'>3</mn> </binomial>"; |
1017 | 1 | assert!(!test_intent(mathml, target, "Error")); |
1018 | 1 | } |
1019 | | |
1020 | | #[test] |
1021 | 1 | fn infer_missing_second_arg_ignore() { |
1022 | 1 | let mathml = "<mrow intent='binomial($n,)'> |
1023 | 1 | <mo>(</mo> |
1024 | 1 | <mfrac linethickness='0'> <mn arg='n'>7</mn> <mn arg='m'>3</mn> </mfrac> |
1025 | 1 | <mo>)</mo> |
1026 | 1 | </mrow>"; |
1027 | 1 | let target = "<mrow data-from-mathml='mrow' intent='binomial($n,)'> |
1028 | 1 | <mo data-from-mathml='mo'>(</mo> |
1029 | 1 | <fraction data-from-mathml='mfrac' linethickness='0'> <mn data-from-mathml='mn' arg='n'>7</mn> <mn data-from-mathml='mn' arg='m'>3</mn> </fraction> |
1030 | 1 | <mo data-from-mathml='mo'>)</mo> |
1031 | 1 | </mrow>"; |
1032 | 1 | assert!(test_intent(mathml, target, "IgnoreIntent")); |
1033 | 1 | } |
1034 | | |
1035 | | #[test] |
1036 | 1 | fn plane1_char_in_concept_name() { |
1037 | 1 | let mathml = "<math><mrow><mo intent='🐇'>🐇</mo><mi>X</mi></mrow></math>"; |
1038 | 1 | let intent = "<math data-from-mathml='math'> |
1039 | 1 | <mrow data-from-mathml='mrow'> |
1040 | 1 | <mi data-from-mathml='mo'>🐇</mi> |
1041 | 1 | <mi data-from-mathml='mi'>X</mi> |
1042 | 1 | </mrow> |
1043 | 1 | </math>"; |
1044 | 1 | assert!(test_intent(mathml, intent, "Error")); |
1045 | 1 | } |
1046 | | } |