Coverage Report

Created: 2026-05-25 08:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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='🐇'>&#x1F407;</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
}