## 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
    }