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/navigate.rs
Line
Count
Source
1
//! Navigation is controlled by a `Navigation_Rules.yaml` file in conjunction with preferences.
2
//! See preference documentation for more info on navigation preferences.
3
#![allow(clippy::needless_return)]
4
5
use std::cell::{Ref, RefCell, RefMut};
6
use sxd_xpath::context::Evaluation;
7
use sxd_xpath::Value;
8
use sxd_document::dom::Element;
9
use sxd_document::Package;
10
11
use std::fmt;
12
use crate::canonicalize::{name, get_parent};
13
use crate::pretty_print::mml_to_string;
14
use crate::speech::{NAVIGATION_RULES, CONCAT_INDICATOR, CONCAT_STRING, SpeechRules, SpeechRulesWithContext};
15
use crate::infer_intent::add_fixity_children;
16
use crate::interface::copy_mathml;
17
#[cfg(not(target_family = "wasm"))]
18
use std::time::Instant;
19
use crate::errors::*;
20
use phf::phf_set;
21
use log::{debug};
22
23
pub const ID_OFFSET: &str = "data-id-offset";
24
25
const MAX_PLACE_MARKERS: usize = 10;
26
27
thread_local!{
28
    /// The current set of navigation rules
29
    pub static NAVIGATION_STATE: RefCell<NavigationState> =
30
            RefCell::new( NavigationState::new() );
31
}
32
33
pub static NAV_COMMANDS: phf::Set<&str> = phf_set! {
34
    "MovePrevious", "MoveNext", "MoveStart", "MoveEnd", "MoveLineStart", "MoveLineEnd", 
35
    "MoveCellPrevious", "MoveCellNext", "MoveCellUp", "MoveCellDown", "MoveColumnStart", "MoveColumnEnd", 
36
    "ZoomIn", "ZoomOut", "ZoomOutAll", "ZoomInAll", 
37
    "MoveLastLocation", 
38
    "ReadPrevious", "ReadNext", "ReadCurrent", "ReadCellCurrent", "ReadStart", "ReadEnd", "ReadLineStart", "ReadLineEnd", 
39
    "DescribePrevious", "DescribeNext", "DescribeCurrent", 
40
    "WhereAmI", "WhereAmIAll", 
41
    "ToggleZoomLockUp", "ToggleZoomLockDown", "ToggleSpeakMode", 
42
    "Exit", 
43
    "MoveTo0","MoveTo1","MoveTo2","MoveTo3","MoveTo4","MoveTo5","MoveTo6","MoveTo7","MoveTo8","MoveTo9",
44
    "Read0","Read1","Read2","Read3","Read4","Read5","Read6","Read7","Read8","Read9",
45
    "Describe0","Describe1","Describe2","Describe3","Describe4","Describe5","Describe6","Describe7","Describe8","Describe9",
46
    "SetPlacemarker0","SetPlacemarker1","SetPlacemarker2","SetPlacemarker3","SetPlacemarker4","SetPlacemarker5","SetPlacemarker6","SetPlacemarker7","SetPlacemarker8","SetPlacemarker9",
47
};
48
49
#[derive(Clone, PartialEq, Debug)]
50
struct NavigationPosition {
51
    current_node: String,           // id of current node
52
    current_node_offset: usize,     // for leaves, char offset in leaf (default = 0), otherwise id for artificial intent node
53
}
54
55
impl fmt::Display for NavigationPosition {
56
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57
0
        return write!(f, "{}[+{}]", self.current_node, self.current_node_offset);
58
0
    }
59
}
60
61
const ILLEGAL_NODE_ID: &str = "!not set";     // an illegal 'id' value
62
impl Default for NavigationPosition {
63
48.6k
    fn default() -> Self {
64
48.6k
        NavigationPosition {
65
48.6k
            current_node: ILLEGAL_NODE_ID.to_string(),
66
48.6k
            current_node_offset: 0
67
48.6k
        }
68
48.6k
     }
69
}
70
71
72
#[derive(Debug, Clone)]
73
pub struct NavigationState {
74
    // it might be better to use a linked for the stacks, with the first node being the top
75
    // these two stacks should be kept in sync.
76
    position_stack: Vec<NavigationPosition>,    // all positions, so we can go back to them
77
    command_stack: Vec<&'static str>,           // all commands, so we can undo them
78
    place_markers: [NavigationPosition; MAX_PLACE_MARKERS],
79
    where_am_i: NavigationPosition,             // current 'where am i' location
80
81
    #[cfg(target_family = "wasm")]
82
    where_am_i_start_time: usize,               // FIX: for web
83
    #[cfg(not(target_family = "wasm"))]
84
    where_am_i_start_time: Instant,
85
    mode: String,                               // one of "Character", "Simple", or "Enhanced"
86
    speak_overview: bool,                       // true => describe after move; false => (standard) speech rules
87
}
88
89
impl fmt::Display for NavigationState {
90
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
91
0
        writeln!(f, "NavigationState{{")?;
92
0
        write!(f, "  Position Stack: ")?;
93
0
        for (i, nav_state) in self.position_stack.iter().enumerate() {
94
0
            write!(f, "{}{}", if i==0 {""} else {", "}, nav_state)?;
95
        }
96
0
        writeln!(f)?;
97
0
        write!(f, "  Command Stack: ")?;
98
0
        for (i, nav_state) in self.command_stack.iter().enumerate() {
99
0
            write!(f, "{}{}", if i==0 {""} else {", "}, *nav_state)?;
100
        }
101
0
        writeln!(f)?;
102
0
        writeln!(f, "  where_am_i: {}, start_time: {:?}", self.where_am_i, self.where_am_i_start_time)?;
103
0
        writeln!(f, "  mode: {}, speak_overview: {}", self.mode, self.speak_overview)?;
104
0
        writeln!(f, "}}")?;
105
0
        return Ok( () );
106
0
    }
107
}
108
109
impl NavigationState {
110
3.92k
    fn new() -> NavigationState {
111
3.92k
        return NavigationState {
112
3.92k
            position_stack: Vec::with_capacity(1024),
113
3.92k
            command_stack: Vec::with_capacity(1024),
114
3.92k
            place_markers: Default::default(),
115
3.92k
            where_am_i: NavigationPosition::default(),
116
3.92k
            // FIX: figure this out for the web
117
3.92k
            #[cfg(target_family = "wasm")]
118
3.92k
            where_am_i_start_time: 0,           // FIX: for web
119
3.92k
            #[cfg(not(target_family = "wasm"))]
120
3.92k
            where_am_i_start_time: Instant::now(),      // need to give it some value, and "default()" isn't an option
121
3.92k
            mode: "".to_string(),                       // set latter when we have some context
122
3.92k
            speak_overview: false,                      // set latter when we have some context
123
3.92k
        };
124
3.92k
    }
125
126
4.88k
    pub fn reset(&mut self) {
127
4.88k
        self.position_stack.clear();
128
4.88k
        self.command_stack.clear();
129
4.88k
        self.where_am_i = NavigationPosition::default();
130
4.88k
        self.reset_start_time()
131
4.88k
    }
132
133
134
    // defining reset_start_time because of the following message if done inline
135
    // attributes on expressions are experimental
136
    // see issue #15701 <https://github.com/rust-lang/rust/issues/15701> for more information
137
    #[cfg(target_family = "wasm")]
138
    fn reset_start_time(&mut self) {
139
         self.where_am_i_start_time = 0;
140
    }
141
142
    #[cfg(not(target_family = "wasm"))]
143
4.88k
    fn reset_start_time(&mut self) {
144
4.88k
         self.where_am_i_start_time = Instant::now();      // need to give it some value, and "default()" isn't an option
145
4.88k
    }
146
147
148
563
    fn push(&mut self, position: NavigationPosition, command: &'static str) {
149
563
        self.position_stack.push(position);
150
563
        self.command_stack.push(command);
151
563
    }
152
153
46
    fn pop(&mut self) -> Option<(NavigationPosition, &'static str)> {
154
46
        assert_eq!(self.position_stack.len(), self.command_stack.len());
155
46
        if self.position_stack.is_empty() {
156
0
            return None;
157
        } else {
158
46
            return Some( (self.position_stack.pop().unwrap(), self.command_stack.pop().unwrap()) );
159
        }
160
46
    }
161
162
2.75k
    fn top(&self) -> Option<(&NavigationPosition, &'static str)> {
163
2.75k
        if self.position_stack.is_empty() {
164
0
            return None;
165
2.75k
        }
166
2.75k
        let last = self.position_stack.len()-1;
167
2.75k
        return Some( (&self.position_stack[last], self.command_stack[last]) );
168
2.75k
    }
169
170
0
    pub fn get_navigation_mathml<'a>(&self, mathml: Element<'a>) -> Result<(Element<'a>, usize)> {
171
0
        if self.position_stack.is_empty() {
172
0
            return Ok( (mathml, 0) );
173
        } else {
174
0
            let (position, _) = self.top().unwrap();
175
0
            return match get_node_by_id(mathml, position) {
176
0
                None => bail!("internal error: id '{}' was not found in mathml:\n{}",
177
0
                                position.current_node, mml_to_string(mathml)),
178
0
                Some(found) => Ok( (found, position.current_node_offset) )
179
            };
180
        }
181
0
    }
182
183
1.09k
    pub fn get_navigation_mathml_id(&self, mathml: Element) -> (String, usize) {
184
1.09k
        if self.position_stack.is_empty() {
185
47
            return (mathml.attribute_value("id").unwrap().to_string(), 0);
186
        } else {
187
1.05k
            let (position, _) = self.top().unwrap();
188
1.05k
            return (position.current_node.clone(), position.current_node_offset);
189
        }
190
1.09k
    }
191
192
549
    fn init_navigation_context(&self, context: &mut sxd_xpath::Context, command: &'static str,
193
549
                               nav_state_top: Option<(&NavigationPosition, &'static str)>) {
194
549
        context.set_variable("NavCommand", command);
195
196
549
        if command == "WhereAmI" && 
self.where_am_i == NavigationPosition::default()0
{
197
0
            context.set_variable("NavNode", self.where_am_i.current_node.as_str());
198
0
            context.set_variable("NavNodeOffset", self.where_am_i.current_node_offset as f64);
199
549
        } else {
200
549
            let position = &self.position_stack[self.position_stack.len()-1];
201
549
            context.set_variable("NavNode", position.current_node.as_str());
202
549
            context.set_variable("NavNodeOffset", position.current_node_offset as f64);
203
549
        }
204
205
        // get the index from command (e.g., '3' in 'SetPlacemarker3 or MoveTo3' and set 'PlaceMarker' to it's position)
206
549
        if command.ends_with(|ch: char| ch.is_ascii_digit()) {
207
6
            let index = convert_last_char_to_number(command);
208
6
            let position = &self.place_markers[index];
209
6
            context.set_variable("PlaceMarkerIndex", index as f64);
210
6
            context.set_variable("PlaceMarker", position.current_node.as_str());
211
6
            context.set_variable("PlaceMarkerOffset", position.current_node_offset as f64);
212
543
        }
213
           
214
549
        context.set_variable("Overview", self.speak_overview);
215
549
        context.set_variable("ReadZoomLevel", (if self.mode == "Enhanced" {
-1200
} else {
1349
}) as f64);
216
549
        context.set_variable("MatchCounter", 0 as f64);
217
218
549
        if command == "MoveLastLocation" {
219
3
            let previous_command = match nav_state_top {
220
0
                None => "None",
221
3
                Some( (_, previous_command) ) => previous_command,
222
            };
223
3
            context.set_variable("PreviousNavCommand", previous_command);
224
546
        }
225
226
        // used by nav rules for speech -- needs an initial value so tests don't fail
227
549
        context.set_variable("SayCommand", "" );
228
549
        context.set_variable("Move2D", "" );
229
549
        context.set_variable("SpeakExpression", true );    // default is to speak the expr after navigation
230
549
        return;
231
232
6
        fn convert_last_char_to_number(str: &str) -> usize {
233
6
            let last_char = str.as_bytes()[str.len()-1];
234
6
            assert!( last_char.is_ascii_digit() );
235
6
            return (last_char - b'0') as usize;
236
6
        }
237
549
    }
238
}
239
240
// convert the last digit of a Placemarker command to an integer
241
2
fn convert_last_char_to_number(str: &str) -> usize {
242
2
    let last_char = str.as_bytes()[str.len()-1];
243
2
    assert!( last_char.is_ascii_digit() );
244
2
    return (last_char - b'0') as usize;
245
2
}
246
247
/// Get the node associated with a `NavigationPosition`.
248
/// This can be called on an intent tree 
249
9.18k
fn get_node_by_id<'a>(mathml: Element<'a>, pos: &NavigationPosition) -> Option<Element<'a>> {
250
9.18k
    if let Some(
mathml_id9.17k
) = mathml.attribute_value("id") &&
251
9.17k
       mathml_id == pos.current_node.as_str() &&
252
1.46k
        (crate::xpath_functions::is_leaf(mathml) || 
253
537
        mathml.attribute_value(ID_OFFSET).unwrap_or("0") == pos.current_node_offset.to_string()) {
254
1.46k
        return Some(mathml);
255
7.71k
    }
256
257
10.0k
    for child in 
mathml7.71k
.
children7.71k
() {
258
10.0k
        if let Some(
child7.71k
) = child.element() &&
259
7.71k
           let Some(
found4.41k
) = get_node_by_id(child, pos) {
260
4.41k
                return Some(found);
261
5.60k
            }
262
    }
263
3.29k
    return None;
264
9.18k
}
265
266
/// Search the mathml for the id and set the navigation node to that id
267
/// Resets the navigation stack
268
2
pub fn set_navigation_node_from_id(mathml: Element, id: &str, offset: usize) -> Result<()> {
269
2
    let current_node = id.to_string();
270
2
    let pos = NavigationPosition { current_node: current_node.clone(), current_node_offset: offset };
271
2
    let node = get_node_by_id(mathml, &pos);
272
2
    if node.is_some() {
273
2
        return NAVIGATION_STATE.with(|nav_state| {
274
2
            let mut nav_state = nav_state.borrow_mut();
275
2
            nav_state.reset();
276
2
            nav_state.push(NavigationPosition{
277
2
                current_node,
278
2
                current_node_offset: offset
279
2
            }, "None");
280
2
            return Ok( () );
281
2
        })
282
    } else {
283
0
        bail!("Id {} not found in MathML {}", id, mml_to_string(mathml));
284
    }
285
2
}
286
287
/// Get's the Nav Node from the context, with some exceptions such as Toggle commands where it isn't set.
288
/// Note: mathml can be any node. It isn't really used but some Element needs to be part of Evaluate().
289
571
pub fn get_nav_node<'c>(context: &sxd_xpath::Context<'c>, var_name: &str, mathml: Element<'c>, start_node: Element<'c>, command: &str, nav_mode: &str) -> Result<String> {
290
571
    let start_id = start_node.attribute_value("id").unwrap_or_default();
291
571
    if command.starts_with("Toggle") {
292
1
        return Ok( start_id.to_string() );
293
    } else {
294
570
        return context_get_variable(context, var_name, mathml)
295
570
                .with_context(|| 
format!0
("When trying to {} starting at id={} in {} mode",
296
0
                                                command, start_node.attribute_value("id").unwrap_or_default(), nav_mode));
297
    }
298
571
}
299
300
// FIX: think of a better place to put this, and maybe a better interface
301
/// Note: mathml can be any node. It isn't really used but some Element needs to be part of Evaluate().
302
/// If the context variable has String, Number, or Boolean xpath value, return it as a string. Otherwise it is an error
303
4.55k
pub fn context_get_variable<'c>(context: &sxd_xpath::Context<'c>, var_name: &str, mathml: Element<'c>) -> Result<String> {
304
    // This is slightly roundabout because Context doesn't expose a way to get the values.
305
    // Instead, we create an "Evaluation", which is just one level of indirection.
306
    use sxd_xpath::nodeset::Node;
307
4.55k
    let evaluation = Evaluation::new(context, Node::Element(mathml));
308
4.55k
    return match evaluation.value_of(var_name.into()) {
309
4.55k
        Some(value) => match value {
310
1.74k
            Value::String(s) => Ok(s.clone()),
311
1.20k
            Value::Number(f) => Ok(f.to_string()),
312
1.09k
            Value::Boolean(b) => Ok(format!("{b}")),    // "true" or "false"
313
509
            Value::Nodeset(nodes) => {
314
509
                if nodes.size() == 1 &&
315
509
                   let Some(attr) = nodes.document_order_first().unwrap().attribute() {
316
509
                        return Ok(attr.value().to_string());
317
0
                    };
318
0
                let mut error_message = format!("Variable '{var_name}' set somewhere in navigate.yaml is nodeset and not an attribute: ");
319
0
                if nodes.size() == 0 {
320
0
                    error_message += &format!("0 nodes (false) -- {} set to non-existent node in\n{}",
321
0
                                              var_name, mml_to_string(mathml));
322
0
                } else {
323
0
                    let singular = nodes.size()==1;
324
0
                    error_message += &format!("{} node{}. {}:",
325
0
                            nodes.size(),
326
0
                            if singular {""} else {"s"},
327
0
                            if singular {"Node is"} else {"Nodes are"});
328
0
                    nodes.document_order()
329
0
                        .iter()
330
0
                        .enumerate()
331
0
                        .for_each(|(i, node)| {
332
0
                            match node {
333
0
                                sxd_xpath::nodeset::Node::Element(mathml) =>
334
0
                                    error_message += &format!("#{}:\n{}",i, mml_to_string(*mathml)),
335
0
                                _ => error_message += &format!("'{node:?}'"),
336
                            }   
337
0
                        })    
338
                };
339
0
                bail!(error_message);
340
            },
341
        },
342
0
        None => bail!("Could not find value for navigation variable '{}'", var_name),
343
    }
344
4.55k
}
345
346
/// Wrapper around context_get_variable to get an integer variable
347
1.70k
fn context_get_int_variable<'c>(context: &sxd_xpath::Context<'c>, var_name: &str, mathml: Element<'c>) -> Result<usize> {
348
1.70k
    let value = context_get_variable(context, var_name, mathml)
?0
;
349
1.70k
    return match value.parse::<usize>() {
350
1.70k
        Ok(i) => Ok(i),
351
0
        Err(e) => bail!("Could not parse navigation variable '{}' with value '{}' as integer: {}", var_name, value, e),
352
    }
353
1.70k
}
354
355
/// Given a key code along with the modifier keys, the current node is moved accordingly (or value reported in some cases).]
356
/// The spoken text for the new current node is returned.
357
0
pub fn do_mathml_navigate_key_press(mathml: Element,
358
0
            key: usize, shift_key: bool, control_key: bool, alt_key: bool, meta_key: bool) -> Result<String> {
359
0
    let (command, param) = key_press_to_command_and_param(key, shift_key, control_key, alt_key, meta_key)?;
360
0
    return do_navigate_command_and_param(mathml, command, param);
361
0
}
362
363
2
fn do_navigate_command_and_param(mathml: Element, command: NavigationCommand, param: NavigationParam) -> Result<String> {
364
2
    return do_navigate_command_string(mathml, navigation_command_string(command, param));
365
2
}
366
367
549
pub fn do_navigate_command_string(mathml: Element, nav_command: &'static str) -> Result<String> {   
368
    // first check to see if nav file has been changed -- don't bother checking in loop below
369
549
    NAVIGATION_RULES.with(|rules| {
370
549
        rules.borrow_mut().read_files()
371
549
    })
?0
;
372
373
549
    if mathml.children().is_empty() {
374
0
        bail!("MathML has not been set -- can't navigate");
375
549
    };
376
377
549
    return NAVIGATION_STATE.with(|nav_state| {
378
549
        let mut nav_state = nav_state.borrow_mut();
379
        // debug!("MathML: {}", mml_to_string(mathml));
380
549
        if nav_state.position_stack.is_empty() {
381
            // initialize to root node
382
47
            nav_state.push(NavigationPosition{
383
47
                current_node: mathml.attribute_value("id").unwrap().to_string(),
384
47
                current_node_offset: 0
385
47
            }, "None")
386
502
        };
387
388
549
        return NAVIGATION_RULES.with(|rules| {
389
549
            let rules = rules.borrow();
390
549
            let new_package = Package::new();
391
549
            let mut rules_with_context = SpeechRulesWithContext::new(&rules, new_package.as_document(), "", 0);
392
            
393
549
            nav_state.mode = rules.pref_manager.as_ref().borrow().pref_to_string("NavMode");
394
549
            nav_state.speak_overview = rules.pref_manager.as_ref().borrow().pref_to_string("Overview") == "true";
395
396
549
            nav_state.init_navigation_context(rules_with_context.get_context(), nav_command, nav_state.top());
397
            
398
            // start navigation off at the right node
399
549
            if nav_command == "MoveLastLocation" {
400
3
                nav_state.pop();
401
546
            }
402
403
            // If no speech happened for some calls, we try the call again (e.g, no speech for invisible times).
404
            // To prevent to infinite loop, we limit the number of tries
405
            const LOOP_LIMIT: usize = 3;
406
549
            let mut cumulative_speech = String::with_capacity(120);
407
569
            for loop_count in 
0..LOOP_LIMIT549
{
408
569
                match apply_navigation_rules(mathml, nav_command, &rules, &mut rules_with_context, &mut nav_state, loop_count) {
409
569
                    Ok( (speech, done)) => {
410
569
                        cumulative_speech = cumulative_speech + if loop_count==0 {
""549
} else {
" "20
} + speech.trim();
411
569
                        if done {
412
549
                            let (tts, rate) = {
413
549
                                let prefs = rules.pref_manager.borrow();
414
549
                                (prefs.pref_to_string("TTS"), prefs.pref_to_string("MathRate"))
415
549
                            };
416
549
                            if rate != "100" {
417
0
                                match tts.as_str() {
418
0
                                    "SSML"
419
0
                                        if !cumulative_speech.starts_with("<prosody rate") => {
420
0
                                            cumulative_speech = format!("<prosody rate='{}%'>{}</prosody>", &rate, &cumulative_speech);
421
0
                                        }
422
0
                                    "SAPI5"
423
0
                                        if !cumulative_speech.starts_with("<rate speed") => {
424
0
                                            cumulative_speech = format!(
425
0
                                                "<rate speed='{:.1}'>{}</rate>",
426
0
                                                10.0 * (0.01 * rate.parse::<f32>().unwrap_or(100.0)).log(3.0),
427
0
                                                cumulative_speech
428
0
                                            );
429
0
                                        }
430
0
                                    _ => (),  // do nothing
431
                                }
432
549
                            }
433
549
                                                return Ok( rules.pref_manager.borrow().get_tts()
434
549
                                            .merge_pauses(crate::speech::remove_optional_indicators(
435
549
                                                &cumulative_speech.replace(CONCAT_STRING, "")
436
549
                                                                    .replace(CONCAT_INDICATOR, "")                            
437
549
                                                            )
438
549
                                            .trim_start().trim_end_matches([' ', ',', ';'])) );
439
20
                        }
440
                    },
441
0
                    Err(e) => {
442
0
                        return Err(e);
443
                    }
444
                }
445
            }
446
0
            bail!("Internal error: Navigation exceeded limit of number of times no speech generated
447
                   when attempting to {} in {} mode start at id={} in this MathML:\n{}.",
448
0
                   nav_command, nav_state.mode, nav_state.top().unwrap().0.current_node, mml_to_string(mathml));
449
549
        });
450
549
    });
451
452
570
    fn get_start_node<'m>(mathml: Element<'m>, nav_state: &RefMut<NavigationState>) -> Result<Element<'m>>  {
453
570
        let element = match nav_state.top() {
454
            None => {
455
0
                let nav_position = NavigationPosition { current_node: mathml.attribute_value("id").unwrap().to_string(), current_node_offset: 0 };
456
0
                get_node_by_id(mathml, &nav_position)
457
            },
458
570
            Some( (position, _) ) => get_node_by_id(mathml, position),
459
        };
460
461
570
        return match element {
462
569
            Some(node) => Ok(node),
463
            None => {
464
1
                bail!("Internal Error: didn't find id/offset '{:?}' while attempting to start navigation. MathML is\n{}",
465
1
                      nav_state.top().map(|t| t.0), mml_to_string(mathml));
466
            }
467
        };
468
570
    }
469
470
471
472
569
    fn apply_navigation_rules<'c, 'm:'c>(mathml: Element<'m>, nav_command: &'static str,
473
569
            rules: &Ref<SpeechRules>, rules_with_context: &mut SpeechRulesWithContext<'c, '_, 'm>, nav_state: &mut RefMut<NavigationState>,
474
569
            loop_count: usize) -> Result<(String, bool)> {
475
        {
476
569
            let context = rules_with_context.get_context();
477
569
            context.set_variable("MatchCounter", loop_count as f64);
478
569
            nav_state.mode = context_get_variable(context, "NavMode", mathml)
?0
;
479
        }
480
481
569
        let mut add_literal = nav_state.mode == "Character";
482
569
        let (intent, nav_intent) = if add_literal {
483
206
            (mathml, mathml)
484
        } else {
485
363
            let intent = crate::speech::intent_from_mathml(mathml, rules_with_context.get_document())
?0
;
486
363
            (intent, add_fixity_children(copy_mathml(intent)))
487
        };
488
489
569
        let mut properties = "";
490
569
        if add_literal {
491
206
            properties  = mathml.attribute_value("data-intent-property").unwrap_or_default();
492
206
            if properties.contains(":literal:") {
493
0
                add_literal = false;
494
206
            } else {
495
206
                mathml.set_attribute_value("data-intent-property", (":literal:".to_string() + properties).as_str());
496
206
            };
497
363
        }
498
        // we should always find the start node.
499
        // however, if we were navigating by character, then switched the NavMode, the intent tree might not have that node in it
500
569
        let start_node = match get_start_node(nav_intent, nav_state) {
501
568
            Ok(node) => node,
502
            Err(_) => {
503
                // find the node in the other tree (probably mathml) and walk up to find a parent that has an id in both
504
1
                debug!("Could not find start_node in nav_intent -- trying other_tree");
505
1
                let other_tree = if nav_state.mode == "Character" {
nav_intent0
} else {mathml};
506
1
                let mut found_node = get_start_node(other_tree, nav_state)
?0
;
507
2
                while name(found_node) != "math" {
508
2
                    found_node = get_parent(found_node);
509
                    // debug!("found_node:\n{}", mml_to_string(found_node));
510
2
                    let temp_pos = NavigationPosition {
511
2
                        current_node: found_node.attribute_value("id").unwrap_or_default().to_string().clone(),
512
2
                        current_node_offset: found_node.attribute_value(ID_OFFSET).unwrap_or_default().parse::<usize>().unwrap_or_default(),
513
2
                    };
514
2
                    if let Some(
intent_node1
) = get_node_by_id(nav_intent, &temp_pos) {
515
1
                        found_node = intent_node;
516
1
                        break;
517
1
                    }
518
                }
519
1
                found_node
520
            }
521
        };
522
523
        // debug!("intent=\n{}", mml_to_string(intent));
524
        // debug!("nav intent=\n{}", mml_to_string(nav_intent));
525
        // debug!("start_node id={}\n{}", nav_state.top().unwrap().0.current_node.as_str(), mml_to_string(start_node));
526
        // if name(start_node) != "math" {
527
        //     let mut parent= get_parent(start_node);
528
        //     if name(parent) != "math" {
529
        //         parent = get_parent(parent);
530
        //     }
531
        //     debug!("parent or grandparent of start_node:\n{}", mml_to_string(parent));
532
        // }
533
569
        let offset = context_get_int_variable(rules_with_context.get_context(), "NavNodeOffset", intent)
?0
;
534
569
        rules_with_context.set_nav_node_offset(offset);
535
569
        debug!("starting nav_position: {}, start node ={}", 
nav_state.top()0
.
unwrap0
().0,
name0
(
start_node0
));
536
537
569
        let raw_speech_string = rules_with_context.match_pattern::<String>(start_node)
538
569
                    .context("Pattern match/replacement failure during math navigation!")
?0
;
539
569
        let speech = rules.pref_manager.borrow().get_tts()
540
569
                    .merge_pauses(crate::speech::remove_optional_indicators(
541
569
                        &raw_speech_string.replace(CONCAT_STRING, "")
542
569
                                                .replace(CONCAT_INDICATOR, "")                            
543
569
                                    )
544
569
                    .trim());
545
        // debug!("Nav Speech: {}", speech);
546
547
        // FIX: add things that need to do a speech replacement based on some marker for "where am i" and others that loop ([Speak: id])???
548
        // what else needs to be done/set???
549
550
        // transfer some values that might have been set into the prefs
551
569
        let offset = context_get_int_variable(rules_with_context.get_context(), "NavNodeOffset", intent)
?0
;
552
569
        rules_with_context.set_nav_node_offset(offset);
553
569
        let context = rules_with_context.get_context();
554
569
        nav_state.speak_overview = context_get_variable(context, "Overview", intent)
?0
== "true";
555
569
        nav_state.mode = context_get_variable(context, "NavMode", intent)
?0
;
556
569
        rules.pref_manager.as_ref().borrow_mut().set_user_prefs("NavMode", &nav_state.mode)
?0
;
557
558
569
        debug!("context value of NavNodeOffset: {:?}", 
context_get_variable0
(
context0
,
"NavNodeOffset"0
,
intent0
)
?0
);
559
569
        let nav_position = NavigationPosition {
560
569
                current_node: get_nav_node(context, "NavNode", intent, start_node, nav_command, &nav_state.mode)
?0
,
561
569
                current_node_offset: context_get_int_variable(context, "NavNodeOffset", intent)
?0
,
562
            };
563
564
        // after a command, we either read or describe the new location (part of state)
565
        // also some commands are DescribeXXX/ReadXXX, so we need to look at the commands also
566
569
        let use_read_rules = if nav_command.starts_with("Read") {
567
5
            true
568
564
        } else if nav_command.starts_with("Describe") {
569
3
            false
570
        } else {
571
561
            !nav_state.speak_overview
572
        };
573
574
569
        debug!("after match nav_position: {}", nav_position);
575
        // push the new location on the stack
576
569
        if nav_position != NavigationPosition::default() && &nav_position != nav_state.top().unwrap().0 {
577
483
            nav_state.push(nav_position.clone(), nav_command);
578
483
        
}86
579
580
569
        if nav_command.starts_with("SetPlacemarker") {
581
2
            let new_node_id = get_nav_node(context, "NavNode", intent, start_node, nav_command, &nav_state.mode)
?0
;
582
2
            nav_state.place_markers[convert_last_char_to_number(nav_command)] = NavigationPosition{
583
2
                current_node: new_node_id,
584
2
                current_node_offset: context_get_int_variable(context, "NavNodeOffset", intent)
?0
,
585
            }
586
567
        }
587
588
569
        let nav_mathml = get_node_by_id(intent, &nav_position);
589
569
        if nav_mathml.is_some() && context_get_variable(context, "SpeakExpression", intent)
?0
== "true" {
590
            // Speak/Overview of where we landed (if we are supposed to speak it) -- use intent, not nav_intent
591
            // Note: NavMode might have changed, so we need to recheck the mode to see if we use LiteralSpeak
592
519
            let literal_speak = nav_state.mode == "Character";
593
519
            let node_speech_result = speak(mathml, intent, &nav_position, literal_speak, use_read_rules);
594
519
            remove_literal_property(mathml, add_literal, properties);
595
519
            let node_speech = match node_speech_result {
596
519
                Ok(speech) => speech,
597
0
                Err(e) => {
598
0
                    if e.to_string() == crate::speech::NAV_NODE_SPEECH_NOT_FOUND {
599
0
                        bail!("Internal error: With {}/{} in {} mode, can't {} from expression with id '{}' inside:\n{}",
600
0
                              rules.pref_manager.as_ref().borrow().pref_to_string("Language"),
601
0
                              rules.pref_manager.as_ref().borrow().pref_to_string("SpeechStyle"),
602
0
                              &nav_state.mode, nav_command, &nav_position.current_node, mml_to_string(if literal_speak {mathml} else {intent}));
603
0
                    }
604
0
                    return Err(e);
605
                }
606
            };
607
608
            // debug!("node_speech: '{}', speech: '{}'\n", node_speech, speech);
609
519
            if node_speech.is_empty() {
610
                // try again in loop
611
20
                return Ok( (speech, false));
612
            } else {
613
499
                pop_stack(nav_state, loop_count, nav_command);
614
                // debug!("returning: '{}'", speech.clone() + " " + &node_speech);
615
499
                return Ok( (speech + " " + &node_speech, true) );
616
            }
617
        } else {
618
50
            remove_literal_property(mathml, add_literal, properties);
619
50
            pop_stack(nav_state, loop_count, nav_command);
620
50
            return Ok( (speech, true) );
621
        };
622
623
569
        fn remove_literal_property(mathml: Element, add_literal: bool, properties: &str) {
624
569
            if add_literal {
625
206
                if properties.is_empty() {
626
206
                    mathml.remove_attribute("data-intent-property");
627
206
                } else {
628
0
                    mathml.set_attribute_value("data-intent-property", properties);
629
0
                }
630
363
            }
631
569
        }
632
633
569
    }
634
635
636
549
    fn pop_stack(nav_state: &mut NavigationState, count: usize, nav_command: &'static str) {
637
        // save the final state and pop the intermediate states that did nothing
638
549
        let push_command_on_stack = (nav_command.starts_with("Move") && 
nav_command != "MoveLastLocation"355
) ||
nav_command197
.
starts_with197
("Zoom");
639
        // debug!("pop_stack: nav_command={}, count={}, push? {} stack=\n{}", nav_command, count, push_command_on_stack, nav_state);
640
549
        if count == 0 {
641
529
            if !push_command_on_stack && 
nav_command13
==
nav_state13
.top().unwrap().1 {
642
3
                nav_state.pop();    // remove ReadXXX, SetPlacemarker, etc. commands that don't change the state
643
526
            }
644
529
            return;
645
20
        }
646
20
        let (top_position, top_command) = nav_state.pop().unwrap();
647
20
        let mut count = count - 1;
648
        loop {
649
            // debug!("  ... loop count={}", count);
650
20
            nav_state.pop();
651
20
            if count == 0 {
652
20
                break;
653
0
            };
654
0
            count -= 1;
655
        };
656
20
        if push_command_on_stack {
657
19
            nav_state.push(top_position, top_command);
658
19
        
}1
659
        // debug!("END pop_stack: stack=\n{}", nav_state);
660
549
    }
661
549
}
662
663
/// Speak the intent tree at the nav_node_id if that id exists in the intent tree; otherwise use the mathml tree.
664
/// If full_read is true, we speak the tree, otherwise we use the overview rules.
665
/// If literal_speak is true, we use the literal speak rules (and use the mathml tree).
666
519
fn speak(mathml: Element, intent: Element, nav_position: &NavigationPosition, literal_speak: bool, full_read: bool) -> Result<String> {
667
519
    if full_read {
668
        // In something like x^3, we might be looking for the '3', but it will be "cubed", so we don't find it.
669
        // Or we might be on a "(" surrounding a matrix and that isn't part of the intent
670
        // We are probably safer in terms of getting the same speech if we retry intent starting at the nav node,
671
        //  but the node to speak is almost certainly trivial.
672
        // By speaking the non-intent tree, we are certain to speak on the next try
673
505
        if !literal_speak && 
get_node_by_id327
(intent, nav_position).
is_some327
() {
674
                // debug!("speak: nav_node_id={}, intent=\n{}", nav_node_id, mml_to_string(intent));
675
327
            match crate::speech::speak_mathml(intent, &nav_position.current_node, nav_position.current_node_offset) {
676
326
                Ok(speech) => return Ok(speech),
677
1
                Err(e) => {
678
1
                    if e.to_string() != crate::speech::NAV_NODE_SPEECH_NOT_FOUND {
679
0
                        return Err(e);
680
1
                    }
681
                    // else could be something like '3' in 'x^3' ("cubed")
682
                },
683
            }
684
178
        }
685
        // debug!("speak (literal): nav_node_id={}, mathml=\n{}", nav_node_id, mml_to_string(mathml));
686
179
        let speech = crate::speech::speak_mathml(mathml,
687
179
                &nav_position.current_node, nav_position.current_node_offset);
688
        // debug!("speech from speak: {:?}", speech);
689
179
        return speech;
690
    } else {
691
14
        return crate::speech::overview_mathml(mathml, &nav_position.current_node, nav_position.current_node_offset);
692
    }
693
519
}
694
695
696
// MathPlayer's interface mentions these, so we keep them.
697
// These (KeyboardEvent.keyCode) are consistent across platforms (mostly?) but are deprecated.
698
//   KeyboardEvent.code is recommended instead (a string)
699
const VK_LEFT: usize = 0x25;
700
const VK_RIGHT: usize = 0x27;
701
const VK_UP: usize = 0x26;
702
const VK_DOWN: usize = 0x28;
703
const VK_RETURN: usize = 0x0D;
704
const VK_SPACE: usize = 0x20;
705
const VK_HOME: usize = 0x24;
706
const VK_END: usize = 0x23;
707
const VK_BACK: usize = 0x08;
708
const VK_ESCAPE: usize = 0x1B;
709
710
// Utilities that returns one of four commands/params based on shift/control key combinations
711
712
enum NavigationCommand {
713
    Move,
714
    Zoom,
715
    MoveLastLocation,
716
    Read,
717
    Describe,
718
    ReadTo,
719
    Locate,
720
    ChangeNavMode,
721
    ToggleSpeakMode,
722
    SetPlacemarker,
723
    Exit,
724
    Last,
725
}
726
727
#[derive(PartialEq, PartialOrd, Clone, Copy)]
728
enum NavigationParam {
729
    Placemarker0,
730
    Placemarker1,
731
    Placemarker2,
732
    Placemarker3,
733
    Placemarker4,
734
    Placemarker5,
735
    Placemarker6,
736
    Placemarker7,
737
    Placemarker8,
738
    Placemarker9,
739
    Previous,
740
    Current,
741
    Next,
742
    Start,
743
    End,
744
    LineStart,
745
    LineEnd,
746
    CellPrevious,
747
    CellCurrent,
748
    CellNext,
749
    ColStart,
750
    ColEnd,
751
    CellUp,
752
    CellDown,
753
    Last 
754
}
755
756
757
0
fn choose_command(
758
0
  shift_key: bool,
759
0
  control_key: bool,
760
0
  none: NavigationCommand,
761
0
  shift: NavigationCommand,
762
0
  control: NavigationCommand,
763
0
  shift_control: NavigationCommand
764
0
) -> NavigationCommand {
765
0
     if shift_key && control_key {
766
0
    return shift_control;
767
0
    } else if control_key {
768
0
        return control;
769
0
    } else if shift_key {
770
0
    return shift;
771
  } else {
772
0
    return none;
773
    }
774
0
}
775
776
0
fn choose_param(
777
0
  shift_key: bool,
778
0
  control_key: bool,
779
0
  none: NavigationParam,
780
0
  shift: NavigationParam,
781
0
  control: NavigationParam,
782
0
  shift_control: NavigationParam
783
0
) -> NavigationParam {
784
0
    if shift_key && control_key {
785
0
    return shift_control;
786
0
    } else if control_key {
787
0
        return control;
788
0
    } else if shift_key {
789
0
    return shift;
790
  } else {
791
0
    return none;
792
    }
793
0
}
794
795
0
fn key_press_to_command_and_param(
796
0
    key: usize,
797
0
  shift_key: bool,
798
0
  control_key: bool,
799
0
  alt_key: bool,
800
0
  meta_key: bool,
801
0
) -> Result<(NavigationCommand, NavigationParam)> {
802
  // key press mapping should probably be stored externally (registry) with an app that allows changes
803
  // for now, we build in the defaults
804
805
    // this is a hack to map alt+ctl+arrow to ctl+arrow to change table mappings (github.com/NSoiffer/MathCAT/issues/105)
806
    // if this change sticks, choose_command() needs to be changed and this hack should go away
807
0
    let mut alt_key = alt_key;
808
0
    if alt_key && control_key && [VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN].contains(&key) {
809
0
        alt_key = false;
810
0
    }
811
0
  if alt_key || meta_key {
812
0
        bail!("Invalid argument to key_press_to_command_and_param");
813
0
    }
814
815
    let command;
816
    let param;
817
0
  match key {
818
0
        VK_LEFT => {
819
0
            command = choose_command(shift_key, control_key, NavigationCommand::Move,   NavigationCommand::Read, NavigationCommand::Move,     NavigationCommand::Describe);
820
0
            param =   choose_param(  shift_key, control_key, NavigationParam::Previous, NavigationParam::Previous, NavigationParam::CellPrevious, NavigationParam::Previous);
821
0
            },
822
0
        VK_RIGHT => {
823
0
            command = choose_command(shift_key, control_key, NavigationCommand::Move, NavigationCommand::Read, NavigationCommand::Move,    NavigationCommand::Describe);
824
0
            param =   choose_param(  shift_key, control_key, NavigationParam::Next, NavigationParam::Next, NavigationParam::CellNext, NavigationParam::Next);
825
0
            },
826
0
        VK_UP => {
827
0
            command = choose_command(shift_key, control_key, NavigationCommand::Zoom,      NavigationCommand::ChangeNavMode, NavigationCommand::Move,   NavigationCommand::Zoom);
828
0
            param =   choose_param(  shift_key, control_key, NavigationParam::Previous,  NavigationParam::Previous,      NavigationParam::CellUp, NavigationParam::Start);
829
0
            },
830
0
        VK_DOWN => {
831
0
            command = choose_command(shift_key, control_key, NavigationCommand::Zoom, NavigationCommand::ChangeNavMode, NavigationCommand::Move,     NavigationCommand::Zoom);
832
0
            param =   choose_param(  shift_key, control_key, NavigationParam::Next, NavigationParam::Next,          NavigationParam::CellDown, NavigationParam::End);
833
0
            },
834
0
        VK_RETURN => {
835
0
            command = choose_command(shift_key, control_key, NavigationCommand::Locate,  NavigationCommand::Last, NavigationCommand::Locate, NavigationCommand::Last);
836
0
            param =   choose_param(  shift_key, control_key, NavigationParam::Previous,NavigationParam::Last, NavigationParam::Last,    NavigationParam::Last);
837
0
            },
838
0
        VK_SPACE => {
839
0
            command = choose_command(shift_key, control_key, NavigationCommand::Read,   NavigationCommand::ToggleSpeakMode,    NavigationCommand::Read,        NavigationCommand::Describe);
840
0
            param =   choose_param(  shift_key, control_key, NavigationParam::Current, NavigationParam::Last,                NavigationParam::CellCurrent, NavigationParam::Current);
841
0
            },
842
    
843
0
        VK_HOME => {
844
0
            command = choose_command(shift_key, control_key, NavigationCommand::Move, NavigationCommand::Move,    NavigationCommand::Move,      NavigationCommand::ReadTo);
845
0
            param =   choose_param(  shift_key, control_key, NavigationParam::Start,NavigationParam::ColStart, NavigationParam::LineStart, NavigationParam::Start);
846
0
            },
847
0
        VK_END => {
848
0
            command = choose_command(shift_key, control_key, NavigationCommand::Move, NavigationCommand::Move,   NavigationCommand::Move,    NavigationCommand::ReadTo);
849
0
            param =   choose_param(  shift_key, control_key, NavigationParam::End,  NavigationParam::ColEnd, NavigationParam::LineEnd, NavigationParam::End);
850
0
            },
851
0
        VK_BACK => {
852
0
            command = NavigationCommand::MoveLastLocation;
853
0
            param = NavigationParam::Last;
854
0
            },
855
0
        VK_ESCAPE => {
856
0
            command = NavigationCommand::Exit;
857
0
            param = NavigationParam::Last;
858
0
            },
859
0
        0x30..=0x39 => {  // '0' ... '9'
860
0
            command = choose_command(shift_key, control_key, NavigationCommand::Move, NavigationCommand::Read, NavigationCommand::SetPlacemarker, NavigationCommand::Describe);
861
            static PLACE_MARKER: &[NavigationParam] = &[
862
                NavigationParam::Placemarker0,
863
                NavigationParam::Placemarker1,
864
                NavigationParam::Placemarker2,
865
                NavigationParam::Placemarker3,
866
                NavigationParam::Placemarker4,
867
                NavigationParam::Placemarker5,
868
                NavigationParam::Placemarker6,
869
                NavigationParam::Placemarker7,
870
                NavigationParam::Placemarker8,
871
                NavigationParam::Placemarker9,
872
            ];
873
0
            param = PLACE_MARKER[key-0x30];
874
        },
875
0
        _ => bail!("Unknown key press/command"),
876
    };
877
    
878
0
  return Ok( (command, param) );
879
0
}
880
881
// translate the key presses into commands
882
883
884
2
fn navigation_command_string(command: NavigationCommand, param: NavigationParam) -> &'static str {
885
2
  match command {
886
      NavigationCommand::Move => {
887
1
            return match param {
888
0
                NavigationParam::Previous => "MovePrevious",
889
0
                NavigationParam::Next => "MoveNext",
890
1
                NavigationParam::Start => "MoveStart",
891
0
                NavigationParam::End => "MoveEnd",
892
0
                NavigationParam::LineStart => "MoveLineStart",
893
0
                NavigationParam::LineEnd => "MoveLineEnd",
894
0
                NavigationParam::CellPrevious => "MoveCellPrevious",
895
0
                NavigationParam::CellNext => "MoveCellNext",
896
0
                NavigationParam::CellUp => "MoveCellUp",
897
0
                NavigationParam::CellDown => "MoveCellDown",
898
0
                NavigationParam::ColStart => "MoveColumnStart",
899
0
                NavigationParam::ColEnd => "MoveColumnEnd",
900
                _ => {
901
0
                    if param < NavigationParam::Placemarker0 || param > NavigationParam::Placemarker9 {
902
0
                        panic!("Internal Error: Found illegal value for param of NavigationCommand::Move");
903
0
                    }
904
                    static MOVE_TO: &[&str] = &["MoveTo0","MoveTo1","MoveTo2","MoveTo3","MoveTo4","MoveTo5","MoveTo6","MoveTo7","MoveTo8","MoveTo9"];
905
0
                    return MOVE_TO[(param as usize) - (NavigationParam::Placemarker0 as usize)];
906
                }
907
            }
908
        },
909
        NavigationCommand::Zoom => {
910
1
            return match param {
911
0
                NavigationParam::Next => "ZoomIn",
912
1
                NavigationParam::Previous => "ZoomOut",
913
0
                NavigationParam::Start => "ZoomOutAll",
914
0
                NavigationParam::End => "ZoomInAll",
915
0
                _  => panic!("Illegal param for NavigationCommand::Zoom"),
916
            }
917
        },
918
        NavigationCommand::MoveLastLocation => {
919
0
            return "MoveLastLocation";
920
        },
921
        NavigationCommand::Read => {
922
0
            return match param {
923
0
                NavigationParam::Previous => "ReadPrevious",
924
0
                NavigationParam::Next => "ReadNext",
925
0
                NavigationParam::Current => "ReadCurrent",
926
0
                NavigationParam::CellCurrent => "ReadCellCurrent",
927
0
                NavigationParam::Start => "ReadStart",
928
0
                NavigationParam::End => "ReadEnd",
929
0
                NavigationParam::LineStart => "ReadLineStart",
930
0
                NavigationParam::LineEnd => "ReadLineEnd",
931
                _ => {
932
0
                    if param < NavigationParam::Placemarker0 || param > NavigationParam::Placemarker9 {
933
0
                        panic!("Internal Error: Found illegal value for param of NavigationCommand::Move");
934
0
                    }
935
                    static READ_PLACE_MARKERS: &[&str] = &["Read0","Read1","Read2","Read3","Read4","Read5","Read6","Read7","Read8","Read9"];
936
0
                    return READ_PLACE_MARKERS[(param as usize) - (NavigationParam::Placemarker0 as usize)];
937
                },
938
            }
939
        },
940
        NavigationCommand::Describe => {
941
0
            return match param {
942
0
                NavigationParam::Previous => "DescribePrevious",
943
0
                NavigationParam::Next => "DescribeNext",
944
0
                NavigationParam::Current => "DescribeCurrent",
945
                _ => {
946
0
                    if param < NavigationParam::Placemarker0 || param > NavigationParam::Placemarker9 {
947
0
                        panic!("Internal Error: Found illegal value for param of NavigationCommand::Describe");
948
0
                    }
949
                    static DESCRIBE_PLACE_MARKERS: &[&str] = &["Describe0","Describe1","Describe2","Describe3","Describe4","Describe5","Describe6","Describe7","Describe8","Describe9"];
950
0
                    return DESCRIBE_PLACE_MARKERS[(param as usize) - (NavigationParam::Placemarker0 as usize)];
951
                }
952
            }
953
        },
954
        NavigationCommand::ReadTo => {
955
0
            todo!("ReadTo navigation command")
956
        },
957
        NavigationCommand::Locate => {
958
0
            if param ==NavigationParam::Previous {
959
0
                return "WhereAmI";
960
0
            } else if param ==NavigationParam::Last {
961
0
                return "WhereAmIAll";
962
0
            }
963
        },
964
        NavigationCommand::ChangeNavMode => {
965
0
            if param ==NavigationParam::Previous {
966
0
                return "ToggleZoomLockUp";
967
0
            } else if param ==NavigationParam::Next {
968
0
                return "ToggleZoomLockDown";
969
0
            }
970
        },
971
        NavigationCommand::ToggleSpeakMode => {
972
0
            return "ToggleSpeakMode";
973
        },
974
        NavigationCommand::SetPlacemarker => {
975
0
            if param < NavigationParam::Placemarker0 || param > NavigationParam::Placemarker9 {
976
0
                panic!("Internal Error: Found illegal value for param of NavigationCommand::SetPlacemarker");
977
0
            }
978
            static SET_PLACE_MARKER: &[&str] = &["SetPlacemarker0","SetPlacemarker1","SetPlacemarker2","SetPlacemarker3","SetPlacemarker4","SetPlacemarker5","SetPlacemarker6","SetPlacemarker7","SetPlacemarker8","SetPlacemarker9"];
979
0
            return SET_PLACE_MARKER[(param as usize) - (NavigationParam::Placemarker0 as usize)];
980
        },
981
        NavigationCommand::Exit => {
982
0
            return "Exit";
983
        },
984
        NavigationCommand::Last => {
985
0
            return "Error";
986
        }
987
    };
988
0
    return "Error";
989
2
}
990
991
#[cfg(test)]
992
mod tests {
993
    use super::*;
994
    #[allow(unused_imports)]
995
    use crate::init_logger;
996
    use crate::interface::*;
997
998
    #[cfg(test)]
999
    /// Assert if result_id != '' and it doesn't match the id of the result of the move
1000
    /// Returns the speech from the command
1001
547
    fn test_command(command: &'static str, mathml: Element, result_id: &str) -> String {
1002
        // debug!("\nCommand: {}", command);
1003
547
        NAVIGATION_STATE.with(|nav_stack| {
1004
547
            let (start_id, _) = nav_stack.borrow().get_navigation_mathml_id(mathml);
1005
547
            match do_navigate_command_string(mathml, command) {
1006
0
                Err(e) => {
1007
0
                    panic!("\nStarting at '{}', '{} failed.\n{}",
1008
0
                                        start_id, command, &crate::interface::errors_to_string(&e))
1009
                },
1010
547
                Ok(nav_speech) => {
1011
547
                    let nav_speech = nav_speech.trim_end_matches(&[' ', ',', ';']);
1012
                    // debug!("Full speech: {}", nav_speech);
1013
547
                    if !result_id.is_empty() {
1014
547
                        let (id, _) = nav_stack.borrow().get_navigation_mathml_id(mathml);
1015
547
                        assert_eq!(result_id, id, "\nStarting at '{}', '{} failed.", start_id, command);
1016
0
                    }
1017
547
                    return nav_speech.to_string();
1018
                }
1019
            };
1020
547
        })
1021
547
    }
1022
1023
56
    fn init_default_prefs(mathml: &str, nav_mode_default: &str) {
1024
56
        set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
1025
56
        set_preference("NavMode", nav_mode_default).unwrap();
1026
56
        set_preference("NavVerbosity", "Verbose").unwrap();
1027
56
        set_preference("AutoZoomOut", "True").unwrap();
1028
56
        set_preference("Language", "en").unwrap();
1029
56
        set_preference("SpeechStyle", "SimpleSpeak").unwrap();
1030
56
        set_preference("Verbosity", "Medium").unwrap();
1031
56
        set_preference("Overview", "False").unwrap();
1032
56
        set_mathml(mathml).unwrap();
1033
56
    }
1034
1035
    #[test]
1036
1
    fn zoom_in() -> Result<()> {
1037
1
        let mathml_str = "<math id='math'><mfrac id='mfrac'>
1038
1
                <msup id='msup'><mi id='base'>b</mi><mn id='exp'>2</mn></msup>
1039
1
                <mi id='denom'>d</mi>
1040
1
            </mfrac></math>";
1041
1
        init_default_prefs(mathml_str, "Enhanced");
1042
1
        return MATHML_INSTANCE.with(|package_instance| {
1043
1
            let package_instance = package_instance.borrow();
1044
1
            let mathml = get_element(&package_instance);
1045
1
            test_command("ZoomIn", mathml, "msup");
1046
1
            test_command("ZoomIn", mathml, "base");
1047
1
            test_command("ZoomIn", mathml, "base");
1048
1
            return Ok( () );
1049
1
        });
1050
1
    }
1051
1052
    #[test]
1053
1
    fn test_init_navigate_move_right() -> Result<()> {
1054
        // this is how navigation typically starts up
1055
1
        let mathml_str = " <math display='block' id='id-0'>
1056
1
            <mrow id='id-1'>
1057
1
                <msup id='msup'><mi id='base'>b</mi><mn id='exp'>2</mn></msup>
1058
1
                <mo id='id-3'>=</mo>
1059
1
                <mrow id='id-4'>
1060
1
                    <mi id='id-5'>a</mi>
1061
1
                    <mo id='id-6'>-</mo>
1062
1
                    <mn id='id-7'>2</mn>
1063
1
                </mrow>
1064
1
            </mrow>
1065
1
        </math>";
1066
1
        init_default_prefs(mathml_str, "Enhanced");
1067
1
        debug!("--- Enhanced ---");
1068
1
        MATHML_INSTANCE.with(|package_instance| {
1069
1
            let package_instance = package_instance.borrow();
1070
1
            let mathml = get_element(&package_instance);
1071
1
            test_command("ZoomIn", mathml, "msup");
1072
1
            test_command("MoveNext", mathml, "id-3");
1073
1
        });
1074
1075
1
        init_default_prefs(mathml_str, "Simple");
1076
1
        debug!("--- Simple ---");
1077
1
        MATHML_INSTANCE.with(|package_instance: &RefCell<Package>| {
1078
1
            let package_instance = package_instance.borrow();
1079
1
            let mathml = get_element(&package_instance);
1080
1
            test_command("ZoomIn", mathml, "msup");
1081
1
            test_command("MoveNext", mathml, "id-3");
1082
1
        });
1083
        
1084
1
        init_default_prefs(mathml_str, "Character");
1085
1
        debug!("--- Character ---");
1086
1
        MATHML_INSTANCE.with(|package_instance| {
1087
1
            let package_instance = package_instance.borrow();
1088
1
            let mathml = get_element(&package_instance);
1089
1
            test_command("ZoomIn", mathml, "base");
1090
1
            test_command("MoveNext", mathml, "exp");
1091
1
        });
1092
1
        return Ok( () );
1093
1
    }
1094
    
1095
    #[test]
1096
1
    fn zoom_in_parens() -> Result<()> {
1097
        // (a+b)(c+d) + 1
1098
1
        let mathml_str = " <math display='block' id='id-0'>
1099
1
            <mrow id='id-1'>
1100
1
                <mrow id='id-2'>
1101
1
                    <mrow id='id-3'>
1102
1
                    <mo stretchy='false' id='id-4'>(</mo>
1103
1
                    <mrow id='id-5'>
1104
1
                        <mi id='id-6'>a</mi>
1105
1
                        <mo id='id-7'>+</mo>
1106
1
                        <mi id='id-8'>b</mi>
1107
1
                    </mrow>
1108
1
                    <mo stretchy='false' id='id-9'>)</mo>
1109
1
                    </mrow>
1110
1
                    <mo id='id-10'>&#x2062;</mo>
1111
1
                    <mrow id='id-11'>
1112
1
                    <mo stretchy='false' id='id-12'>(</mo>
1113
1
                    <mrow id='id-13'>
1114
1
                        <mi id='id-14'>c</mi>
1115
1
                        <mo id='id-15'>+</mo>
1116
1
                        <mi id='id-16'>d</mi>
1117
1
                    </mrow>
1118
1
                    <mo stretchy='false' id='id-17'>)</mo>
1119
1
                    </mrow>
1120
1
                </mrow>
1121
1
                <mo id='id-18'>+</mo>
1122
1
                <mn id='id-19'>1</mn>
1123
1
            </mrow>
1124
1
        </math>";
1125
1
        init_default_prefs(mathml_str, "Enhanced");
1126
1
        return MATHML_INSTANCE.with(|package_instance| {
1127
1
            let package_instance = package_instance.borrow();
1128
1
            let mathml = get_element(&package_instance);
1129
1
            set_preference("NavMode", "Enhanced")
?0
;
1130
1
            debug!("\n------EnhancedMode----------");
1131
1
            test_command("ZoomIn", mathml, "id-2");
1132
1
            test_command("ZoomIn", mathml, "id-5");
1133
1
            test_command("ZoomIn", mathml, "id-6");
1134
            
1135
            // repeat, but this time with "Simple
1136
1
            set_preference("NavMode", "Simple")
?0
;
1137
1
            debug!("\n------SimpleMode----------");
1138
1
            test_command("ZoomOutAll", mathml, "id-1");
1139
1
            test_command("ZoomIn", mathml, "id-4");
1140
1
            test_command("ZoomIn", mathml, "id-4");
1141
1
            return Ok( () );
1142
1
        });
1143
1
    }
1144
    
1145
    #[test]
1146
1
    fn zoom_in_all() -> Result<()> {
1147
1
        let mathml_str = "<math id='math'><mfrac id='mfrac'>
1148
1
                <msup id='msup'><mi id='base'>b</mi><mn id='exp'>2</mn></msup>
1149
1
                <mi id='denom'>d</mi>
1150
1
            </mfrac></math>";
1151
1
        init_default_prefs(mathml_str, "Enhanced");
1152
1
        return MATHML_INSTANCE.with(|package_instance| {
1153
1
            let package_instance = package_instance.borrow();
1154
1
            let mathml = get_element(&package_instance);
1155
1
            test_command("ZoomInAll", mathml, "base");
1156
1
            return Ok( () );
1157
1
        });
1158
1
    }
1159
1160
    
1161
    #[test]
1162
1
    fn zoom_out() -> Result<()> {
1163
1
        let mathml_str = "<math id='math'><mfrac id='mfrac'>
1164
1
                <msup id='msup'><mi id='base'>b</mi><mn id='exp'>2</mn></msup>
1165
1
                <mi id='denom'>d</mi>
1166
1
            </mfrac></math>";
1167
1
            init_default_prefs(mathml_str, "Enhanced");
1168
1
            return MATHML_INSTANCE.with(|package_instance| {
1169
1
            let package_instance = package_instance.borrow();
1170
1
            let mathml = get_element(&package_instance);
1171
1
            NAVIGATION_STATE.with(|nav_stack| {
1172
1
                nav_stack.borrow_mut().push(NavigationPosition{
1173
1
                    current_node: "base".to_string(),
1174
1
                    current_node_offset: 0
1175
1
                }, "None")
1176
1
            });
1177
1
            test_command("ZoomOut", mathml, "msup");
1178
1179
1
            let _nav_speech = do_navigate_command_and_param(mathml, NavigationCommand::Zoom, NavigationParam::Previous)
?0
;
1180
1
            NAVIGATION_STATE.with(|nav_stack| {
1181
1
                let (id, _) = nav_stack.borrow().get_navigation_mathml_id(mathml);
1182
1
                assert_eq!(id, "mfrac");
1183
1
            });
1184
1
            return Ok( () );
1185
1
        });
1186
1
    }
1187
    
1188
    #[test]
1189
1
    fn zoom_out_all() -> Result<()> {
1190
1
        let mathml_str = "<math id='math'><mfrac id='mfrac'>
1191
1
                <msup id='msup'><mi id='base'>b</mi><mn id='exp'>2</mn></msup>
1192
1
                <mi id='denom'>d</mi>
1193
1
            </mfrac></math>";
1194
1
            init_default_prefs(mathml_str, "Enhanced");
1195
1
            return MATHML_INSTANCE.with(|package_instance| {
1196
1
            let package_instance = package_instance.borrow();
1197
1
            let mathml = get_element(&package_instance);
1198
1
            NAVIGATION_STATE.with(|nav_stack| {
1199
1
                nav_stack.borrow_mut().push(NavigationPosition{
1200
1
                    current_node: "base".to_string(),
1201
1
                    current_node_offset: 0
1202
1
                }, "None")
1203
1
            });
1204
1205
1
            test_command("ZoomOutAll", mathml, "mfrac");
1206
1
            return Ok( () );
1207
1
        });
1208
1
    }
1209
    
1210
    #[test]
1211
1
    fn move_start_end() -> Result<()> {
1212
1
        let mathml_str = " <math display='block' id='id-0'>
1213
1
        <mrow id='id-1'>
1214
1
          <mi id='id-2'>x</mi>
1215
1
          <mo id='id-3'>=</mo>
1216
1
          <mrow id='id-4'>
1217
1
            <mi id='id-5'>a</mi>
1218
1
            <mo id='id-6'>-</mo>
1219
1
            <mn id='id-7'>2</mn>
1220
1
          </mrow>
1221
1
        </mrow>
1222
1
       </math>";
1223
1
       init_default_prefs(mathml_str, "Enhanced");
1224
1
       return MATHML_INSTANCE.with(|package_instance| {
1225
1
            let package_instance = package_instance.borrow();
1226
1
            let mathml = get_element(&package_instance);
1227
1
            NAVIGATION_STATE.with(|nav_stack| {
1228
1
                nav_stack.borrow_mut().push(NavigationPosition{
1229
1
                    current_node: "id-4".to_string(),
1230
1
                    current_node_offset: 0
1231
1
                }, "None")
1232
1
            });
1233
1234
1
           set_preference("NavMode", "Character")
?0
;
1235
1
            test_command("MoveStart", mathml, "id-2");
1236
1
            test_command("MoveEnd", mathml, "id-7");
1237
1
           set_preference("NavMode", "Simple")
?0
;
1238
1
            test_command("MoveStart", mathml, "id-2");
1239
1
            test_command("MoveEnd", mathml, "id-7");
1240
1
           set_preference("NavMode", "Enhanced")
?0
;
1241
1
            test_command("MoveStart", mathml, "id-2");
1242
1
            test_command("MovePrevious", mathml, "id-2");
1243
1
            test_command("MoveEnd", mathml, "id-4");
1244
1
            test_command("MoveNext", mathml, "id-4");
1245
1
            return Ok( () );
1246
1
        });
1247
1
    }
1248
    
1249
    #[test]
1250
1
    fn move_line_start_end() -> Result<()> {
1251
1
        let mathml_str = " <math display='block' id='id-0'>
1252
1
        <mfrac displaystyle='true' id='id-1'>
1253
1
          <mi id='id-2'>x</mi>
1254
1
          <mrow id='id-3'>
1255
1
            <msup id='id-4'>
1256
1
              <mi id='id-5'>y</mi>
1257
1
              <mn id='id-6'>2</mn>
1258
1
            </msup>
1259
1
            <mo id='id-7'>+</mo>
1260
1
            <mn id='id-8'>1</mn>
1261
1
          </mrow>
1262
1
        </mfrac>
1263
1
       </math>";
1264
1
       init_default_prefs(mathml_str, "Enhanced");
1265
1
       return MATHML_INSTANCE.with(|package_instance| {
1266
1
            let package_instance = package_instance.borrow();
1267
1
            let mathml = get_element(&package_instance);
1268
1
            NAVIGATION_STATE.with(|nav_stack| {
1269
1
                nav_stack.borrow_mut().push(NavigationPosition{
1270
1
                    current_node: "id-7".to_string(),
1271
1
                    current_node_offset: 0
1272
1
                }, "None")
1273
1
            });
1274
1275
1
           set_preference("NavMode", "Character")
?0
;
1276
1
            test_command("MoveLineStart", mathml, "id-5");
1277
1
            test_command("MoveLineEnd", mathml, "id-8");
1278
1
           set_preference("NavMode", "Simple")
?0
;
1279
1
            test_command("MoveLineStart", mathml, "id-4");
1280
1
            test_command("MoveLineEnd", mathml, "id-8");
1281
1
           set_preference("NavMode", "Enhanced")
?0
;
1282
1
            test_command("MoveLineStart", mathml, "id-4");
1283
1
            test_command("MoveLineEnd", mathml, "id-8");
1284
1
            test_command("MoveEnd", mathml, "id-3");
1285
1
            return Ok( () );
1286
1
        });
1287
1
    }
1288
    
1289
    #[test]
1290
1
    fn text_extremes_and_move_last_location() -> Result<()> {
1291
1
        let mathml_str = "<math id='math'><mfrac id='mfrac'>
1292
1
                <msup id='msup'><mi id='base'>b</mi><mn id='exp'>2</mn></msup>
1293
1
                <mi id='denom'>d</mi>
1294
1
            </mfrac></math>";
1295
1
            init_default_prefs(mathml_str, "Enhanced");
1296
1
            return MATHML_INSTANCE.with(|package_instance| {
1297
1
            let package_instance = package_instance.borrow();
1298
1
            let mathml = get_element(&package_instance);
1299
1
            NAVIGATION_STATE.with(|nav_stack| {
1300
1
                nav_stack.borrow_mut().push(NavigationPosition{
1301
1
                    current_node: "base".to_string(),
1302
1
                    current_node_offset: 0
1303
1
                }, "None")
1304
1
            });
1305
1306
1
            test_command("ZoomOutAll", mathml, "mfrac");
1307
1
            test_command("ZoomOut", mathml, "mfrac");
1308
1
            test_command("MoveLastLocation", mathml, "base");       // second zoom out should do nothing
1309
1310
1
            test_command("ZoomOut", mathml, "msup");
1311
1
            test_command("ZoomInAll", mathml, "base");
1312
1
            test_command("ZoomIn", mathml, "base");
1313
1
            test_command("MoveLastLocation", mathml, "msup");       // second zoom in should do nothing
1314
1315
1
            return Ok( () );
1316
1
        });
1317
1
    }
1318
    
1319
    #[test]
1320
1
    fn move_to_start() -> Result<()> {
1321
1
        let mathml_str = "<math id='math'><mfrac id='mfrac'>
1322
1
                <mrow id='num'><msup id='msup'><mi id='base'>b</mi><mn id='exp'>2</mn></msup><mo id='factorial'>!</mo></mrow>
1323
1
                <mi id='denom'>d</mi>
1324
1
            </mfrac></math>";
1325
1
            init_default_prefs(mathml_str, "Enhanced");
1326
1
            return MATHML_INSTANCE.with(|package_instance| {
1327
1
            let package_instance = package_instance.borrow();
1328
1
            let mathml = get_element(&package_instance);
1329
1
            NAVIGATION_STATE.with(|nav_stack| {
1330
1
                nav_stack.borrow_mut().push(NavigationPosition{
1331
1
                    current_node: "denom".to_string(),
1332
1
                    current_node_offset: 0
1333
1
                }, "None")
1334
1
            });
1335
1
            test_command("MoveLineStart", mathml, "denom");
1336
1337
1
            NAVIGATION_STATE.with(|nav_stack| {
1338
1
                nav_stack.borrow_mut().push(NavigationPosition{
1339
1
                    current_node: "factorial".to_string(),
1340
1
                    current_node_offset: 0
1341
1
                }, "None")
1342
1
            });
1343
1
            test_command("MoveLineStart", mathml, "msup");
1344
1345
1
            let _nav_speech = do_navigate_command_and_param(mathml, NavigationCommand::Move, NavigationParam::Start)
?0
;
1346
1
            NAVIGATION_STATE.with(|nav_stack| {
1347
1
                let (id, _) = nav_stack.borrow().get_navigation_mathml_id(mathml);
1348
1
                assert_eq!(id, "num");
1349
1
            });
1350
1
            return Ok( () );
1351
1
        });
1352
1
    }
1353
    
1354
    #[test]
1355
1
    fn move_right_sup() -> Result<()> {
1356
1
        let mathml_str = "<math display='block' id='id-0'>
1357
1
        <mrow id='id-1'>
1358
1
          <msup id='id-2'>
1359
1
            <mn id='id-3'>2</mn>
1360
1
            <mi id='id-4'>q</mi>
1361
1
          </msup>
1362
1
          <mo id='id-5'>-</mo>
1363
1
          <mi id='id-6'>x</mi>
1364
1
        </mrow>
1365
1
        </math>";
1366
1
        init_default_prefs(mathml_str, "Enhanced");
1367
1
        return MATHML_INSTANCE.with(|package_instance| {
1368
1
            let package_instance = package_instance.borrow();
1369
1
            let mathml = get_element(&package_instance);
1370
1
            NAVIGATION_STATE.with(|nav_stack| {
1371
1
                nav_stack.borrow_mut().push(NavigationPosition{
1372
1
                    current_node: "id-2".to_string(),
1373
1
                    current_node_offset: 0
1374
1
                }, "None")
1375
1
            });
1376
1
            set_preference("NavMode", "Enhanced")
?0
;
1377
1
            test_command("MoveNext", mathml, "id-5");
1378
1379
            // reset start and test Simple
1380
1
            NAVIGATION_STATE.with(|nav_stack| {
1381
1
                nav_stack.borrow_mut().push(NavigationPosition{
1382
1
                    current_node: "id-2".to_string(),
1383
1
                    current_node_offset: 0
1384
1
                }, "None")
1385
1
            });
1386
1
            set_preference("NavMode", "Simple")
?0
;
1387
1
            test_command("MoveNext", mathml, "id-5");
1388
1389
            // reset start and test Character
1390
1
            NAVIGATION_STATE.with(|nav_stack| {
1391
1
                nav_stack.borrow_mut().push(NavigationPosition{
1392
1
                    current_node: "id-3".to_string(),
1393
1
                    current_node_offset: 0
1394
1
                }, "None")
1395
1
            });
1396
1
            set_preference("NavMode", "Character")
?0
;
1397
1
            test_command("MoveNext", mathml, "id-4");
1398
1
            test_command("MoveNext", mathml, "id-5");
1399
1
            return Ok( () );
1400
1
        });
1401
1
    }
1402
1403
        
1404
    #[test]
1405
1
    fn move_msubsup_char() -> Result<()> {
1406
1
        let mathml_str = "<math display='block' id='id-0'>
1407
1
        <mrow id='id-1'>
1408
1
          <mn id='id-2'>1</mn>
1409
1
          <mo id='id-3'>+</mo>
1410
1
          <msubsup id='id-4'>
1411
1
            <mi id='id-5'>x</mi>
1412
1
            <mn id='id-6'>2</mn>
1413
1
            <mn id='id-7'>3</mn>
1414
1
          </msubsup>
1415
1
          <mo id='id-8'>+</mo>
1416
1
          <mn id='id-9'>4</mn>
1417
1
        </mrow>
1418
1
       </math>";
1419
1
        init_default_prefs(mathml_str, "Character");
1420
1
        return MATHML_INSTANCE.with(|package_instance| {
1421
1
            let package_instance = package_instance.borrow();
1422
1
            let mathml = get_element(&package_instance);
1423
1
            assert_eq!("zoomed in all of the way; 1", test_command("ZoomInAll", mathml, "id-2"));
1424
1
            assert_eq!("move right; plus", test_command("MoveNext", mathml, "id-3"));
1425
1
            assert_eq!("move right; in base; x", test_command("MoveNext", mathml, "id-5"));
1426
1
            assert_eq!("move right; in subscript; 2", test_command("MoveNext", mathml, "id-6"));
1427
1
            assert_eq!("move right; in superscript; 3", test_command("MoveNext", mathml, "id-7"));
1428
1
            assert_eq!("move right; out of superscript; plus", test_command("MoveNext", mathml, "id-8"));
1429
1
            assert_eq!("move left; in superscript; 3", test_command("MovePrevious", mathml, "id-7"));
1430
1
            assert_eq!("move left; in subscript; 2", test_command("MovePrevious", mathml, "id-6"));
1431
1
            assert_eq!("move left; in base; x", test_command("MovePrevious", mathml, "id-5"));
1432
1
            assert_eq!("move left; out of base; plus", test_command("MovePrevious", mathml, "id-3"));
1433
1434
1
            return Ok( () );
1435
1
        });
1436
1
    }
1437
        
1438
    #[test]
1439
1
    fn zoom_logbase() -> Result<()> {
1440
1
        let mathml_str = "<math display='block' id='id-0'>
1441
1
            <mrow displaystyle='true' id='id-1'>
1442
1
                <msub id='id-2'>
1443
1
                    <mi id='id-3'>log</mi>
1444
1
                    <mn id='id-4'>2</mn>
1445
1
                </msub>
1446
1
                <mo data-changed='added' id='id-5'>&#x2061;</mo>
1447
1
                <mi id='id-6'>x</mi>a
1448
1
            </mrow>
1449
1
            </math>";
1450
1
        init_default_prefs(mathml_str, "Enhanced");
1451
1
        return MATHML_INSTANCE.with(|package_instance| {
1452
1
            let package_instance = package_instance.borrow();
1453
1
            let mathml = get_element(&package_instance);
1454
1
            assert_eq!("zoom in; the log base 2", test_command("ZoomIn", mathml, "id-2"));
1455
1
            assert_eq!("zoom in; in base; 2", test_command("ZoomIn", mathml, "id-4"));
1456
1
            assert_eq!("zoomed in all of the way; 2", test_command("ZoomIn", mathml, "id-4"));
1457
1
            debug!("Now zooming out");
1458
1
            assert_eq!("zoom out; out of base; the log base 2", test_command("ZoomOut", mathml, "id-2"));
1459
1
            assert_eq!("zoom out; the log base 2, of x", test_command("ZoomOut", mathml, "id-1"));
1460
1
            assert_eq!("zoomed out all of the way; the log base 2, of x", test_command("ZoomOut", mathml, "id-1"));
1461
1
            return Ok( () );
1462
1
        });
1463
1
    }
1464
        
1465
    #[test]
1466
1
    fn zoom_logbase_power() -> Result<()> {
1467
1
        let mathml_str = "<math display='block' id='id-0'>
1468
1
            <mrow displaystyle='true' id='id-1'>
1469
1
                <msubsup id='id-2'>
1470
1
                    <mi id='id-3'>log</mi>
1471
1
                    <mn id='id-4'>2</mn>
1472
1
                    <mn id='id-5'>3</mn>
1473
1
                </msubsup>
1474
1
                <mo data-changed='added' id='id-6'>&#x2061;</mo>
1475
1
                <mi id='id-7'>x</mi>
1476
1
            </mrow>
1477
1
            </math>";
1478
1
        init_default_prefs(mathml_str, "Enhanced");
1479
1
        return MATHML_INSTANCE.with(|package_instance| {
1480
1
            let package_instance = package_instance.borrow();
1481
1
            let mathml = get_element(&package_instance);
1482
1
            assert_eq!("zoom in; the log base 2, cubed", test_command("ZoomIn", mathml, "id-2"));
1483
1
            assert_eq!("zoom in; in base; the log base 2", test_command("ZoomIn", mathml, "id-2-log-base"));
1484
1
            assert_eq!("zoom in; in base; 2", test_command("ZoomIn", mathml, "id-4"));
1485
1
            assert_eq!("zoomed in all of the way; 2", test_command("ZoomIn", mathml, "id-4"));
1486
1
            debug!("Now zooming out");
1487
1
            assert_eq!("zoom out; out of base; the log base 2", test_command("ZoomOut", mathml, "id-2-log-base"));
1488
1
            assert_eq!("zoom out; out of base; the log base 2, cubed", test_command("ZoomOut", mathml, "id-2"));
1489
1
            assert_eq!("zoom out; the log base 2, cubed of x", test_command("ZoomOut", mathml, "id-1"));
1490
1
            assert_eq!("zoomed out all of the way; the log base 2, cubed of x", test_command("ZoomOut", mathml, "id-1"));
1491
1
            return Ok( () );
1492
1
        });
1493
1
    }
1494
        
1495
    #[test]
1496
1
    fn zoom_msubsup() -> Result<()> {
1497
        // msubsup is trickier because it creates an intent within an intent, so offsets need to be handled properly
1498
1
        let mathml_str = "<math id='math'><msubsup id='msubsup'><mi id='base'>𝑥</mi><mn id='sub'>1</mn><mn id='sup'>2</mn></msubsup></math>";
1499
1
        init_default_prefs(mathml_str, "Enhanced");
1500
1
        return MATHML_INSTANCE.with(|package_instance| {
1501
1
            let package_instance = package_instance.borrow();
1502
1
            let mathml = get_element(&package_instance);
1503
1
            set_preference("NavMode", "Enhanced").unwrap();
1504
1
            debug!("Enhanced mode");
1505
1
            do_commands(mathml)
?0
;
1506
1
            set_preference("NavMode", "Simple").unwrap();
1507
1
            debug!("Simple mode");
1508
1
            do_commands(mathml)
?0
;
1509
1
            set_preference("NavMode", "Character").unwrap();
1510
1
            debug!("Character mode");
1511
1
            assert_eq!("zoom in; in base; x", test_command("ZoomIn", mathml, "base"));
1512
1
            assert_eq!("zoom out; out of base; x sub 1 super 2 end super", test_command("ZoomOut", mathml, "msubsup"));
1513
1
            return Ok( () );
1514
1515
        /// Enhanced and Simple mode should behave the same
1516
2
        fn do_commands(mathml: Element) -> Result<()> {
1517
2
            assert_eq!("zoom in; in base; x sub 1", test_command("ZoomIn", mathml, "msubsup-indexed-by"));
1518
2
            assert_eq!("zoom in; in base; x", test_command("ZoomIn", mathml, "base"));
1519
2
            assert_eq!("zoomed in all of the way; x", test_command("ZoomIn", mathml, "base"));
1520
2
            debug!("Now zooming out");
1521
2
            assert_eq!("zoom out; out of base; x sub 1", test_command("ZoomOut", mathml, "msubsup-indexed-by"));
1522
2
            assert_eq!("zoom out; out of base; x sub 1, squared", test_command("ZoomOut", mathml, "msubsup"));
1523
2
            assert_eq!("zoomed out all of the way; x sub 1, squared", test_command("ZoomOut", mathml, "msubsup"));
1524
2
            return Ok( () );
1525
2
        }
1526
1
        });
1527
1
    }
1528
        
1529
    #[test]
1530
1
    fn move_mmultiscripts_char() -> Result<()> {
1531
1
        let mathml_str = "<math display='block' id='id-0'>
1532
1
            <mmultiscripts data-mjx-texclass='ORD' data-chem-formula='5' id='id-1'>
1533
1
                <mrow data-chem-formula='3' id='id-2'>
1534
1
                    <mo stretchy='false' id='id-3'>[</mo>
1535
1
                    <mmultiscripts data-chem-formula='3' id='id-4'>
1536
1
                        <mi data-chem-element='3' id='id-5'>Co</mi>
1537
1
                        <mn id='id-6'>6</mn>
1538
1
                        <none id='id-7'></none>
1539
1
                    </mmultiscripts>
1540
1
                    <mo stretchy='false' id='id-8'>]</mo>
1541
1
                </mrow>
1542
1
                <none id='id-9'></none>
1543
1
                <mrow id='id-10'>
1544
1
                    <mn id='id-11'>3</mn>
1545
1
                    <mo id='id-12'>+</mo>
1546
1
                </mrow>
1547
1
            </mmultiscripts>
1548
1
            </math>";
1549
1
            init_default_prefs(mathml_str, "Character");
1550
1
            return MATHML_INSTANCE.with(|package_instance| {
1551
1
            let package_instance = package_instance.borrow();
1552
1
            let mathml = get_element(&package_instance);
1553
1
            assert_eq!("zoomed in all of the way; in base; open bracket", test_command("ZoomInAll", mathml, "id-3"));
1554
1
            assert_eq!("move right; in base; cap c o", test_command("MoveNext", mathml, "id-5"));
1555
1
            assert_eq!("move right; in subscript; 6", test_command("MoveNext", mathml, "id-6"));
1556
1
            assert_eq!("move right; out of subscript; close bracket", test_command("MoveNext", mathml, "id-8"));
1557
1
            assert_eq!("move right; in superscript; 3", test_command("MoveNext", mathml, "id-11"));
1558
1
            assert_eq!("move right; plus", test_command("MoveNext", mathml, "id-12"));
1559
1
            assert_eq!("cannot move right, end of math", test_command("MoveNext", mathml, "id-12"));
1560
1
            assert_eq!("move left; 3", test_command("MovePrevious", mathml, "id-11"));
1561
1
            assert_eq!("move left; in base; close bracket", test_command("MovePrevious", mathml, "id-8"));
1562
1
            assert_eq!("move left; in subscript; 6", test_command("MovePrevious", mathml, "id-6"));
1563
1
            assert_eq!("move left; in base; cap c o", test_command("MovePrevious", mathml, "id-5"));
1564
1
            assert_eq!("move left; out of base; open bracket", test_command("MovePrevious", mathml, "id-3"));
1565
1566
1
            return Ok( () );
1567
1
        });
1568
1
    }
1569
1570
    #[test]
1571
1
    fn move_right_char() -> Result<()> {
1572
1
        let mathml_str = "<math id='id-0'>
1573
1
        <mrow displaystyle='true' id='id-1'>
1574
1
          <mi id='id-2'>x</mi>
1575
1
          <mo id='id-3'>=</mo>
1576
1
          <mrow id='id-4'>
1577
1
            <mfrac id='id-5'>
1578
1
              <mn id='id-6'>1</mn>
1579
1
              <mrow id='id-7'>
1580
1
                <mi id='id-8'>a</mi>
1581
1
                <mo id='id-9'>+</mo>
1582
1
                <mn id='id-10'>2</mn>
1583
1
              </mrow>
1584
1
            </mfrac>
1585
1
            <mo id='id-11'>+</mo>
1586
1
            <mrow id='id-12'>
1587
1
              <mn id='id-13'>3</mn>
1588
1
              <mo id='id-14'>&#x2062;</mo>
1589
1
              <mi id='id-15'>b</mi>
1590
1
            </mrow>
1591
1
          </mrow>
1592
1
        </mrow>
1593
1
        </math>";
1594
1
        init_default_prefs(mathml_str, "Character");
1595
1
        return MATHML_INSTANCE.with(|package_instance| {
1596
1
            let package_instance = package_instance.borrow();
1597
1
            let mathml = get_element(&package_instance);
1598
1
            test_command("ZoomInAll", mathml, "id-2");
1599
1
            test_command("MoveNext", mathml, "id-3");
1600
1
            test_command("MoveNext", mathml, "id-6");
1601
1
            test_command("MoveNext", mathml, "id-8");
1602
1
            test_command("MoveNext", mathml, "id-9");
1603
1
            test_command("MoveNext", mathml, "id-10");
1604
1
            test_command("MoveNext", mathml, "id-11");
1605
1
            test_command("MoveNext", mathml, "id-13");
1606
1
            test_command("MoveNext", mathml, "id-15");
1607
1
            test_command("MoveNext", mathml, "id-15");
1608
1609
1
            return Ok( () );
1610
1
        });
1611
1
    }
1612
1613
    #[test]
1614
1
    fn char_mode_paren_test() -> Result<()> {
1615
1
        let mathml_str = "<math display='block' id='id-0'>
1616
1
            <mrow displaystyle='true' id='id-1'>
1617
1
                <mrow id='id-2'>
1618
1
                    <mo id='id-3'>(</mo>
1619
1
                    <mi id='id-4'>a</mi>
1620
1
                    <mo id='id-5'>)</mo>
1621
1
                </mrow>
1622
1
                <mo id='id-6'>&#x2062;</mo>
1623
1
                <mrow id='id-7'>
1624
1
                    <mo id='id-8'>(</mo>
1625
1
                    <mi id='id-9'>b</mi>
1626
1
                    <mo id='id-10'>)</mo>
1627
1
                </mrow>
1628
1
            </mrow>
1629
1
        </math>";
1630
1
        init_default_prefs(mathml_str, "Character");
1631
1
        return MATHML_INSTANCE.with(|package_instance| {
1632
1
            let package_instance = package_instance.borrow();
1633
1
            let mathml = get_element(&package_instance);
1634
1
            debug!("Character mode");
1635
1
            do_commands(mathml)
?0
;
1636
1
            set_preference("NavMode", "Simple").unwrap();
1637
1
            debug!("Simple mode");
1638
1
            test_command("ZoomIn", mathml, "id-3");  // zooms to the first parenthesis
1639
1
            do_commands(mathml)
?0
;
1640
1
            set_preference("NavMode", "Enhanced").unwrap();
1641
1
            debug!("Enhanced mode");
1642
1
            test_command("ZoomIn", mathml, "id-4");
1643
1
            test_command("MoveNext", mathml, "id-6");
1644
1
            test_command("MoveNext", mathml, "id-9");
1645
1
            test_command("MovePrevious", mathml, "id-6");
1646
1
            test_command("MovePrevious", mathml, "id-4");
1647
1648
1
            return Ok( () );
1649
1
        });
1650
1651
        /// Simple and Character mode should behave the same
1652
2
        fn do_commands(mathml: Element) -> Result<()> {
1653
2
            test_command("ZoomIn", mathml, "id-3");
1654
2
            test_command("MoveNext", mathml, "id-4");
1655
2
            test_command("MoveNext", mathml, "id-5");
1656
2
            test_command("MoveNext", mathml, "id-8");
1657
2
            test_command("MoveNext", mathml, "id-9");
1658
2
            test_command("MoveNext", mathml, "id-10");
1659
2
            test_command("MovePrevious", mathml, "id-9");
1660
2
            test_command("MovePrevious", mathml, "id-8");
1661
2
            test_command("MovePrevious", mathml, "id-5");
1662
2
            test_command("ZoomOutAll", mathml, "id-1");
1663
2
            return Ok( () );
1664
2
        }
1665
1
    }
1666
1667
    #[test]
1668
1
    fn char_mode_trig_test() -> Result<()> {
1669
1
        let mathml_str = "<math id='id-0'>
1670
1
            <mrow id='id-1'>
1671
1
            <mi id='id-2'>sin</mi>
1672
1
            <mo id='id-3'>&#x2061;</mo>
1673
1
            <mrow id='id-4'>
1674
1
                <mo id='id-5'>(</mo>
1675
1
                <mi id='id-6'>x</mi>
1676
1
                <mo id='id-7'>)</mo>
1677
1
            </mrow>
1678
1
            </mrow>
1679
1
        </math>";
1680
1
        init_default_prefs(mathml_str, "Simple");
1681
1
        return MATHML_INSTANCE.with(|package_instance| {
1682
1
            let package_instance = package_instance.borrow();
1683
1
            let mathml = get_element(&package_instance);
1684
1
            do_commands(mathml)
?0
;
1685
1
            set_preference("NavMode", "Simple").unwrap();
1686
1
            do_commands(mathml)
?0
;
1687
1
            set_preference("NavMode", "Enhanced").unwrap();
1688
1
            test_command("ZoomIn", mathml, "id-2");
1689
1
            test_command("MoveNext", mathml, "id-6");
1690
1
            test_command("MovePrevious", mathml, "id-2");
1691
1692
1
            return Ok( () );
1693
1
        });
1694
1695
        
1696
        /// Simple and Character mode should behave the same
1697
2
        fn do_commands(mathml: Element) -> Result<()> {
1698
2
            test_command("ZoomIn", mathml, "id-2");
1699
2
            test_command("MoveNext", mathml, "id-5");
1700
2
            test_command("MoveNext", mathml, "id-6");
1701
2
            test_command("MoveNext", mathml, "id-7");
1702
2
            test_command("MovePrevious", mathml, "id-6");
1703
2
            test_command("MovePrevious", mathml, "id-5");
1704
2
            test_command("MovePrevious", mathml, "id-2");
1705
2
            test_command("ZoomOutAll", mathml, "id-1");
1706
2
            return Ok( () );
1707
2
        }
1708
1
    }
1709
    
1710
    #[test]
1711
1
    fn move_char_speech() -> Result<()> {
1712
1
        let mathml_str = "<math display='block' id='id-0'>
1713
1
                <mrow id='id-1'>
1714
1
                <mfrac id='id-2'>
1715
1
                    <mi id='id-3'>x</mi>
1716
1
                    <mi id='id-4'>y</mi>
1717
1
                </mfrac>
1718
1
                <mo id='id-5'>&#x2062;</mo>
1719
1
                <mi id='id-6'>z</mi>
1720
1
                </mrow>
1721
1
            </math>";
1722
1
            init_default_prefs(mathml_str, "Character");
1723
1
            return MATHML_INSTANCE.with(|package_instance| {
1724
1
            let package_instance = package_instance.borrow();
1725
1
            let mathml = get_element(&package_instance);
1726
1
            test_command("ZoomInAll", mathml, "id-3");
1727
1
            assert_eq!("move right; in denominator; y", test_command("MoveNext", mathml, "id-4"));
1728
1
            assert_eq!("move right; out of denominator; z", test_command("MoveNext", mathml, "id-6"));
1729
1
            assert_eq!("move left; in denominator; y", test_command("MovePrevious", mathml, "id-4"));
1730
1
            assert_eq!("move left; in numerator; x", test_command("MovePrevious", mathml, "id-3"));
1731
1732
1
            return Ok( () );
1733
1
        });
1734
1
    }
1735
    
1736
    #[test]
1737
1
    fn move_inside_leaves() -> Result<()> {
1738
1
        let mathml_str = "<math display='block' id='id-0'>
1739
1
                <mrow id='id-1'>
1740
1
                    <mfrac id='id-2'>
1741
1
                        <mi id='id-3'>top</mi>
1742
1
                        <mi id='id-4'>αβγ</mi>
1743
1
                    </mfrac>
1744
1
                </mrow>
1745
1
            </math>";
1746
1
        init_default_prefs(mathml_str, "Character");
1747
1
        return MATHML_INSTANCE.with(|package_instance| {
1748
1
        let package_instance = package_instance.borrow();
1749
1
        let mathml = get_element(&package_instance);
1750
1
        test_command("ZoomInAll", mathml, "id-3");
1751
1
        assert_eq!("zoomed in to first character; t", test_command("ZoomIn", mathml, "id-3"));
1752
1
        assert_eq!("move right; o", test_command("MoveNext", mathml, "id-3"));
1753
1
        assert_eq!("move right; p", test_command("MoveNext", mathml, "id-3"));
1754
1
        assert_eq!("move right; in denominator; αβγ", test_command("MoveNext", mathml, "id-4"));
1755
1
        assert_eq!("zoomed in to first character; alpha", test_command("ZoomIn", mathml, "id-4"));
1756
1
        assert_eq!("move right; beta", test_command("MoveNext", mathml, "id-4"));
1757
1
        assert_eq!("move right; gamma", test_command("MoveNext", mathml, "id-4"));
1758
1
        assert_eq!("cannot move right, end of math", test_command("MoveNext", mathml, "id-4"));
1759
1
        assert_eq!("move left; beta", test_command("MovePrevious", mathml, "id-4"));
1760
1
        assert_eq!("zoom out; αβγ", test_command("ZoomOut", mathml, "id-4"));
1761
1762
1
        return Ok( () );
1763
1
        });
1764
1
    }
1765
    
1766
    #[test]
1767
1
    fn move_enhanced_times() -> Result<()> {
1768
1
        let mathml_str = "<math display='block' id='id-0'>
1769
1
        <mrow displaystyle='true' id='id-1'>
1770
1
          <mn id='id-2'>2</mn>
1771
1
          <mo id='id-3'>&#x2062;</mo>
1772
1
          <mrow id='id-4'>
1773
1
            <mo id='id-5'>(</mo>
1774
1
            <mrow id='id-6'>
1775
1
              <mn id='id-7'>1</mn>
1776
1
              <mo id='id-8'>-</mo>
1777
1
              <mi id='id-9'>x</mi>
1778
1
            </mrow>
1779
1
            <mo id='id-10'>)</mo>
1780
1
          </mrow>
1781
1
        </mrow>
1782
1
       </math>";
1783
1
        init_default_prefs(mathml_str, "Enhanced");
1784
1
        return MATHML_INSTANCE.with(|package_instance| {
1785
1
            let package_instance = package_instance.borrow();
1786
1
            let mathml = get_element(&package_instance);
1787
1
            test_command("ZoomIn", mathml, "id-2");
1788
1
            assert_eq!("move right; times", test_command("MoveNext", mathml, "id-3"));
1789
1
            assert_eq!("move right; 1 minus x", test_command("MoveNext", mathml, "id-6"));
1790
1
            assert_eq!("move left; times", test_command("MovePrevious", mathml, "id-3"));
1791
1
            assert_eq!("move left; 2", test_command("MovePrevious", mathml, "id-2"));
1792
1793
1
            return Ok( () );
1794
1
        });
1795
1
    }
1796
    
1797
    #[test]
1798
1
    fn move_simple_no_times() -> Result<()> {
1799
1
        let mathml_str = "<math display='block' id='id-0'>
1800
1
        <mrow displaystyle='true' id='id-1'>
1801
1
          <mn id='id-2'>2</mn>
1802
1
          <mo id='id-3'>&#x2062;</mo>
1803
1
          <mrow id='id-4'>
1804
1
            <mo id='id-5'>(</mo>
1805
1
            <mrow id='id-6'>
1806
1
              <mn id='id-7'>1</mn>
1807
1
              <mo id='id-8'>-</mo>
1808
1
              <mi id='id-9'>x</mi>
1809
1
            </mrow>
1810
1
            <mo id='id-10'>)</mo>
1811
1
          </mrow>
1812
1
        </mrow>
1813
1
       </math>";
1814
1
        init_default_prefs(mathml_str, "Simple");
1815
1
        set_preference("SpeechStyle", "ClearSpeak").unwrap();
1816
1
        return MATHML_INSTANCE.with(|package_instance| {
1817
1
            let package_instance = package_instance.borrow();
1818
1
            let mathml = get_element(&package_instance);
1819
1
            test_command("ZoomIn", mathml, "id-2");
1820
1
            assert_eq!("move right; open paren", test_command("MoveNext", mathml, "id-5"));
1821
1
            assert_eq!("move right; 1", test_command("MoveNext", mathml, "id-7"));
1822
1
            assert_eq!("move left; open paren", test_command("MovePrevious", mathml, "id-5"));
1823
1
            assert_eq!("move left; 2", test_command("MovePrevious", mathml, "id-2"));
1824
1825
1
            return Ok( () );
1826
1
        });
1827
1
    }
1828
    
1829
    
1830
    #[test]
1831
1
    fn move_cell() -> Result<()> {
1832
1
        let mathml_str = "<math id='nav-0'>
1833
1
        <mtable id='nav-1'>
1834
1
          <mtr id='nav-2'>
1835
1
            <mtd id='nav-3'> <mn id='nav-4'>1</mn></mtd>
1836
1
            <mtd id='nav-5'> <mn id='nav-6'>2</mn></mtd>
1837
1
            <mtd id='nav-7'><mn id='nav-8'>3</mn> </mtd>
1838
1
          </mtr>
1839
1
          <mtr id='nav-9'>
1840
1
            <mtd id='nav-10'>
1841
1
              <mrow id='nav-11'>
1842
1
                <mi id='nav-12'>x</mi>
1843
1
                <mo id='nav-13'>-</mo>
1844
1
                <mi id='nav-14'>y</mi>
1845
1
              </mrow>
1846
1
            </mtd>
1847
1
            <mtd id='nav-15'>
1848
1
              <mfrac id='nav-16'>
1849
1
                <mn id='nav-17'>1</mn>
1850
1
                <mn id='nav-18'>2</mn>
1851
1
              </mfrac>
1852
1
            </mtd>
1853
1
            <mtd id='nav-19'>
1854
1
              <mi id='nav-20'>z</mi>
1855
1
            </mtd>
1856
1
          </mtr>
1857
1
          <mtr id='nav-21'>
1858
1
            <mtd id='nav-22'><mn id='nav-23'>7</mn> </mtd>
1859
1
            <mtd id='nav-24'><mn id='nav-25'>8</mn> </mtd>
1860
1
            <mtd id='nav-26'> <mn id='nav-27'>9</mn></mtd>
1861
1
          </mtr>
1862
1
          <mtr id='nav-28'>
1863
1
            <mtd id='nav-29'>
1864
1
              <mrow id='nav-30'>
1865
1
                <mi id='nav-31'>sin</mi>
1866
1
                <mo id='nav-32'>&#x2061;</mo>
1867
1
                <mi id='nav-33'>x</mi>
1868
1
              </mrow>
1869
1
            </mtd>
1870
1
            <mtd id='nav-34'>
1871
1
              <msup id='nav-35'>
1872
1
                <mi id='nav-36'>e</mi>
1873
1
                <mi id='nav-37'>x</mi>
1874
1
              </msup>
1875
1
            </mtd>
1876
1
            <mtd id='nav-38'>
1877
1
              <mrow id='nav-39'>
1878
1
                <mn id='nav-40'>2</mn>
1879
1
                <mo id='nav-41'>-</mo>
1880
1
                <mi id='nav-42'>y</mi>
1881
1
              </mrow>
1882
1
            </mtd>
1883
1
          </mtr>
1884
1
        </mtable>
1885
1
       </math>";
1886
1
        init_default_prefs(mathml_str, "Enhanced");
1887
1
        return MATHML_INSTANCE.with(|package_instance| {
1888
1
            let package_instance = package_instance.borrow();
1889
1
            let mathml = get_element(&package_instance);
1890
1
            test_command("ZoomInAll", mathml, "nav-4");
1891
1
            test_command("MoveCellNext", mathml, "nav-6");
1892
1
            test_command("MoveCellNext", mathml, "nav-8");
1893
1
            test_command("MoveCellNext", mathml, "nav-8");
1894
1
            test_command("MoveCellDown", mathml, "nav-20");
1895
1
            test_command("MoveCellDown", mathml, "nav-27");
1896
1
            let speech = test_command("MoveCellDown", mathml, "nav-39");
1897
1
            assert_eq!(speech, "move down, row 4, column 3; 2 minus y");
1898
1
            let speech = test_command("MoveCellDown", mathml, "nav-39");
1899
1
            assert_eq!(speech, "no next row");
1900
1
            test_command("MoveCellPrevious", mathml, "nav-35");
1901
1
            test_command("ZoomIn", mathml, "nav-36");
1902
1
            test_command("MoveCellUp", mathml, "nav-25");
1903
1
            test_command("MoveCellUp", mathml, "nav-16");
1904
1
            test_command("MoveCellUp", mathml, "nav-6");
1905
1
            test_command("MoveCellUp", mathml, "nav-6");
1906
1907
1
            return Ok( () );
1908
1
        });
1909
1
    }
1910
    
1911
    #[test]
1912
1
    fn move_cell_char_mode() -> Result<()> {
1913
1
        let mathml_str = "<math id='nav-0'>
1914
1
        <mtable id='nav-1'>
1915
1
          <mtr id='nav-2'>
1916
1
            <mtd id='nav-3'> <mn id='nav-4'>1</mn></mtd>
1917
1
            <mtd id='nav-5'> <mn id='nav-6'>2</mn></mtd>
1918
1
            <mtd id='nav-7'><mn id='nav-8'>3</mn> </mtd>
1919
1
          </mtr>
1920
1
          <mtr id='nav-9'>
1921
1
            <mtd id='nav-10'>
1922
1
              <mrow id='nav-11'>
1923
1
                <mi id='nav-12'>x</mi>
1924
1
                <mo id='nav-13'>-</mo>
1925
1
                <mi id='nav-14'>y</mi>
1926
1
              </mrow>
1927
1
            </mtd>
1928
1
            <mtd id='nav-15'>
1929
1
              <mfrac id='nav-16'>
1930
1
                <mn id='nav-17'>1</mn>
1931
1
                <mn id='nav-18'>2</mn>
1932
1
              </mfrac>
1933
1
            </mtd>
1934
1
            <mtd id='nav-19'>
1935
1
              <mi id='nav-20'>z</mi>
1936
1
            </mtd>
1937
1
          </mtr>
1938
1
          <mtr id='nav-21'>
1939
1
            <mtd id='nav-22'><mn id='nav-23'>7</mn> </mtd>
1940
1
            <mtd id='nav-24'><mn id='nav-25'>8</mn> </mtd>
1941
1
            <mtd id='nav-26'> <mn id='nav-27'>9</mn></mtd>
1942
1
          </mtr>
1943
1
          <mtr id='nav-28'>
1944
1
            <mtd id='nav-29'>
1945
1
              <mrow id='nav-30'>
1946
1
                <mi id='nav-31'>sin</mi>
1947
1
                <mo id='nav-32'>&#x2061;</mo>
1948
1
                <mi id='nav-33'>x</mi>
1949
1
              </mrow>
1950
1
            </mtd>
1951
1
            <mtd id='nav-34'>
1952
1
              <msup id='nav-35'>
1953
1
                <mi id='nav-36'>e</mi>
1954
1
                <mi id='nav-37'>x</mi>
1955
1
              </msup>
1956
1
            </mtd>
1957
1
            <mtd id='nav-38'>
1958
1
              <mrow id='nav-39'>
1959
1
                <mn id='nav-40'>2</mn>
1960
1
                <mo id='nav-41'>-</mo>
1961
1
                <mi id='nav-42'>y</mi>
1962
1
              </mrow>
1963
1
            </mtd>
1964
1
          </mtr>
1965
1
        </mtable>
1966
1
       </math>";
1967
1
       init_default_prefs(mathml_str, "Character");
1968
1
       return MATHML_INSTANCE.with(|package_instance| {
1969
1
            let package_instance = package_instance.borrow();
1970
1
            let mathml = get_element(&package_instance);
1971
1
            NAVIGATION_STATE.with(|nav_stack| {
1972
1
                nav_stack.borrow_mut().push(NavigationPosition{
1973
1
                    current_node: "nav-8".to_string(),
1974
1
                    current_node_offset: 0
1975
1
                }, "None")
1976
1
            });
1977
1
            test_command("MoveNext", mathml, "nav-12");
1978
1
            test_command("MoveNext", mathml, "nav-13");
1979
1
            test_command("MoveNext", mathml, "nav-14");
1980
1
            test_command("MoveNext", mathml, "nav-17");
1981
1
            test_command("MovePrevious", mathml, "nav-14");
1982
1
            test_command("MoveCellNext", mathml, "nav-17");
1983
1
            test_command("MoveCellPrevious", mathml, "nav-14");
1984
1
            test_command("MovePrevious", mathml, "nav-13");
1985
1
            test_command("MovePrevious", mathml, "nav-12");
1986
1
            test_command("MoveCellPrevious", mathml, "nav-12");
1987
1
            test_command("MovePrevious", mathml, "nav-8");
1988
1
            test_command("MoveCellDown", mathml, "nav-20");
1989
1
            test_command("MoveCellDown", mathml, "nav-27");
1990
1
            test_command("MoveCellDown", mathml, "nav-40");
1991
1
            test_command("MoveCellDown", mathml, "nav-40");
1992
1
            test_command("MoveCellPrevious", mathml, "nav-37");
1993
1
            test_command("MoveCellUp", mathml, "nav-25");
1994
1995
1
            return Ok( () );
1996
1
        });
1997
1
    }
1998
    
1999
    #[test]
2000
1
    fn placemarker() -> Result<()> {
2001
1
        let mathml_str = "<math display='block' id='math'>
2002
1
        <mrow displaystyle='true' id='mrow'>
2003
1
          <mi id='a'>a</mi>
2004
1
          <mo id='plus-1'>+</mo>
2005
1
          <mi id='b'>b</mi>
2006
1
          <mo id='plus-2'>+</mo>
2007
1
          <mi id='c'>c</mi>
2008
1
        </mrow>
2009
1
        </math>";
2010
1
        init_default_prefs(mathml_str, "Character");
2011
1
        return MATHML_INSTANCE.with(|package_instance| {
2012
1
            let package_instance = package_instance.borrow();
2013
1
            let mathml = get_element(&package_instance);
2014
1
            test_command("MoveStart", mathml, "a");
2015
1
            test_command("SetPlacemarker0", mathml, "a");
2016
1
            test_command("MoveEnd", mathml, "c");
2017
1
            test_command("Read0", mathml, "c");
2018
1
            test_command("Describe0", mathml, "c");
2019
1
            test_command("SetPlacemarker1", mathml, "c");
2020
1
            test_command("MoveTo0", mathml, "a");
2021
1
            test_command("MoveTo1", mathml, "c");
2022
1
            test_command("MoveLastLocation", mathml, "a");
2023
            
2024
1
            return Ok( () );
2025
1
        });
2026
1
    }
2027
2028
    #[test]
2029
1
    fn where_am_i_all() -> Result<()> {
2030
1
        let mathml_str = "<math id='math'><mfrac id='mfrac'>
2031
1
                <msup id='msup'><mi id='base'>b</mi><mn id='exp'>2</mn></msup>
2032
1
                <mi id='denom'>d</mi>
2033
1
            </mfrac></math>";
2034
1
        init_default_prefs(mathml_str, "Enhanced");
2035
1
        set_preference("SpeechStyle", "ClearSpeak").unwrap();
2036
1
        return MATHML_INSTANCE.with(|package_instance| {
2037
1
            let package_instance = package_instance.borrow();
2038
1
            let mathml = get_element(&package_instance);
2039
1
            NAVIGATION_STATE.with(|nav_stack| {
2040
1
                nav_stack.borrow_mut().push(NavigationPosition{
2041
1
                    current_node: "exp".to_string(),
2042
1
                    current_node_offset: 0
2043
1
                }, "None")
2044
1
            });
2045
            // WhereAmIAll doesn't change the stack
2046
1
            let speech =test_command("WhereAmIAll", mathml, "exp");
2047
            // should be 2 "inside" strings corresponding to steps to the root
2048
1
            assert_eq!(speech, "2; inside; b squared; inside; the fraction with numerator; b squared; and denominator d");
2049
1
            return Ok( () );
2050
1
        });
2051
1
    }
2052
2053
    #[test]
2054
1
    fn auto_zoom_out_mrow() -> Result<()> {
2055
1
        let mathml_str = "<math id='math'>
2056
1
        <mrow id='id-1'>
2057
1
          <mrow id='id-2'>
2058
1
            <mrow id='2ax'>
2059
1
              <mn id='2'>2</mn>
2060
1
              <mo id='id-5'>&#x2062;</mo>
2061
1
              <mi id='a'>a</mi>
2062
1
              <mo id='id-7'>&#x2062;</mo>
2063
1
              <mi id='x'>x</mi>
2064
1
            </mrow>
2065
1
            <mo id='plus'>+</mo>
2066
1
            <mi id='b'>b</mi>
2067
1
          </mrow>
2068
1
          <mo id='equal'>=</mo>
2069
1
          <mn id='10'>10</mn>
2070
1
        </mrow>
2071
1
       </math>";
2072
1
        init_default_prefs(mathml_str, "Enhanced");
2073
1
        set_preference("AutoZoomOut", "False")
?0
;
2074
1
        return MATHML_INSTANCE.with(|package_instance| {
2075
1
            let package_instance = package_instance.borrow();
2076
1
            let mathml = get_element(&package_instance);
2077
1
            test_command("ZoomInAll", mathml, "2");
2078
1
            test_command("MoveNext", mathml, "a");
2079
1
            test_command("MoveNext", mathml, "x");
2080
1
            test_command("MoveNext", mathml, "plus");
2081
1
            test_command("MovePrevious", mathml, "2ax");
2082
1
            return Ok( () );
2083
1
        });
2084
1
    }
2085
2086
    #[test]
2087
1
    fn auto_zoom_out_fraction() -> Result<()> {
2088
1
        let mathml_str = "<math id='math'>
2089
1
            <mrow id='mrow'>
2090
1
                <mfrac id='frac'>
2091
1
                    <mrow id='num'><mi id='a'>a</mi><mo id='plus'>+</mo><mn id='1'>1</mn></mrow>
2092
1
                    <mrow id='denom'><mn id='2'>2</mn><mo id='invisible-times'>&#x2062;</mo><mi id='b'>b</mi></mrow>
2093
1
                </mfrac>
2094
1
                <mo id='minus'>-</mo>
2095
1
                <mn id='3'>3</mn>
2096
1
            </mrow>
2097
1
        </math>";
2098
1
        init_default_prefs(mathml_str, "Enhanced");
2099
1
        set_preference("AutoZoomOut", "False")
?0
;
2100
1
        return MATHML_INSTANCE.with(|package_instance| {
2101
1
            let package_instance = package_instance.borrow();
2102
1
            let mathml = get_element(&package_instance);
2103
1
            test_command("ZoomIn", mathml, "frac");
2104
1
            test_command("ZoomIn", mathml, "num");
2105
1
            test_command("MoveNext", mathml, "denom");
2106
1
            test_command("MoveNext", mathml, "denom");
2107
1
            test_command("MovePrevious", mathml, "num");
2108
1
            test_command("MovePrevious", mathml, "num");
2109
1
            test_command("ZoomOut", mathml, "frac");
2110
1
            test_command("MoveNext", mathml, "minus");
2111
1
            return Ok( () );
2112
1
        });
2113
1
    }
2114
2115
    #[test]
2116
1
    fn zoom_root() -> Result<()> {
2117
1
        let mathml_str = r#"<math display='block' id='id-0'>
2118
1
        <mrow id='id-1'>
2119
1
            <mo id='id-9'>±</mo>
2120
1
            <msqrt id='id-10'>
2121
1
                <mrow id='id-11'>
2122
1
                    <msup id='id-12'> <mi id='id-13'>b</mi> <mn id='id-14'>2</mn> </msup>
2123
1
                    <mo id='id-15'>-</mo>
2124
1
                    <mn id='id-17'>4</mn>
2125
1
                </mrow>
2126
1
            </msqrt>
2127
1
        </mrow>
2128
1
        </math>"#;
2129
2130
1
        test_mode(mathml_str, "Enhanced")
?0
;
2131
1
        test_mode(mathml_str, "Simple")
?0
;
2132
1
        test_mode(mathml_str, "Character")
?0
;
2133
1
        return Ok( () );
2134
2135
3
        fn test_mode(mathml_str: &str, mode: &str) -> Result<()> {
2136
3
            init_default_prefs(mathml_str, mode);
2137
3
            set_preference("AutoZoomOut", "False")
?0
;
2138
3
            return MATHML_INSTANCE.with(|package_instance| {
2139
3
                debug!("--- Testing mode {mode} ---");
2140
3
                let package_instance = package_instance.borrow();
2141
3
                let mathml = get_element(&package_instance);
2142
3
                test_command("ZoomIn", mathml, "id-9");
2143
3
                debug!("\nStart zoom in");
2144
3
                match mode {
2145
3
                    "Enhanced" => {
2146
1
                        test_command("MoveNext", mathml, "id-10");
2147
1
                        let speech = test_command("ZoomIn", mathml, "id-11");
2148
1
                        assert_eq!(speech, "zoom in; in root; b squared minus 4");  // only one arg, so don't say "in root"
2149
1
                        let speech = test_command("ZoomIn", mathml, "id-12");
2150
1
                        assert_eq!(speech, "zoom in; b squared");  // only one arg, so don't say "in root"
2151
1
                        let speech = test_command("ZoomIn", mathml, "id-13");
2152
1
                        assert_eq!(speech, "zoom in; in base; b");
2153
                    },
2154
2
                    "Simple" => {
2155
1
                        test_command("MoveNext", mathml, "id-10");
2156
1
                        let speech = test_command("ZoomIn", mathml, "id-12");
2157
1
                        assert_eq!(speech, "zoom in; in root; b squared");
2158
1
                        let speech = test_command("ZoomIn", mathml, "id-13");
2159
1
                        assert_eq!(speech, "zoom in; in base; b");
2160
                    },
2161
                    _ => { // "Character"
2162
1
                        let speech = test_command("MoveNext", mathml, "id-13");
2163
1
                        assert_eq!(speech, "move right; in root; in base; b");
2164
                    }
2165
                }
2166
3
                let squared_speech = if mode == "Character" {
"b super 2 end super"1
} else {
"b squared"2
};
2167
3
                let sqrt_speech = if mode == "Character" {
"root"1
} else {
"square root"2
};
2168
3
                let speech = test_command("ZoomOut", mathml, "id-12");
2169
3
                assert_eq!(speech, format!("zoom out; out of base; {squared_speech}"));
2170
3
                let speech = test_command("ZoomOut", mathml, "id-11");
2171
3
                assert_eq!(speech, format!("zoom out; {squared_speech} minus 4"));
2172
3
                let speech = test_command("ZoomOut", mathml, "id-10");
2173
3
                assert_eq!(speech, format!("zoom out; out of root; the {sqrt_speech} of {squared_speech} minus 4, end root",));
2174
3
                return Ok( () );
2175
3
            });
2176
3
        }
2177
1
    }
2178
2179
    #[test]
2180
1
    fn matrix_speech() -> Result<()> {
2181
1
        let mathml_str = r#"<math id='math'>
2182
1
            <mrow id='mrow'>
2183
1
            <mo id='open'>[</mo>
2184
1
            <mtable columnspacing='1em' rowspacing='4pt' id='table'>
2185
1
                <mtr id='row-1'>
2186
1
                    <mtd id='1-1'><mn id='id-6'>9</mn></mtd>
2187
1
                    <mtd id='1-2'><mrow id='id-8'><mo id='id-9'>-</mo><mn id='id-10'>13</mn></mrow></mtd>
2188
1
                </mtr>
2189
1
                <mtr id='row-2'>
2190
1
                    <mtd id='2-1'><mn id='id-13'>5</mn></mtd>
2191
1
                    <mtd id='2-2'><mo id='id-16'>-</mo><mn id='id-17'>6</mn></mtd>
2192
1
                </mtr>
2193
1
            </mtable>
2194
1
            <mo id='close'>]</mo>
2195
1
            </mrow>
2196
1
        </math>"#;
2197
1
        init_default_prefs(mathml_str, "Enhanced");
2198
1
        return MATHML_INSTANCE.with(|package_instance| {
2199
1
            let package_instance = package_instance.borrow();
2200
1
            let mathml = get_element(&package_instance);
2201
1
            test_command("ZoomIn", mathml, "row-1");
2202
1
            let speech = test_command("MoveNext", mathml, "row-2");
2203
1
            assert_eq!(speech, "move right; row 2; 5, negative 6");
2204
1
            let speech = test_command("ZoomIn", mathml, "id-13");
2205
1
            assert_eq!(speech, "zoom in; column 1; 5");
2206
1
            let speech = test_command("ZoomOut", mathml, "row-2");
2207
1
            assert_eq!(speech, "zoom out; row 2; 5, negative 6");
2208
1
            let speech = test_command("ZoomOut", mathml, "table");
2209
1
            assert_eq!(speech, "zoom out; the 2 by 2 matrix; row 1; 9, negative 13; row 2; 5, negative 6");
2210
1
        return Ok( () );
2211
1
        });
2212
1
    }
2213
2214
    #[test]
2215
1
    fn chem_speech() -> Result<()> {
2216
        // this comes from bug 218
2217
1
        let mathml_str = "<math display='block' id='id-0'>
2218
1
            <mrow data-chem-formula='5' id='id-1'>
2219
1
                <msub data-chem-formula='1' id='id-2'>
2220
1
                    <mi data-chem-element='1' id='id-3'>H</mi>
2221
1
                    <mn id='id-4'>2</mn>
2222
1
                </msub>
2223
1
                <mo data-chem-formula-op='0' id='id-5'>&#x2063;</mo>
2224
1
                <mi data-chem-element='1' id='id-6'>S</mi>
2225
1
                <mo data-chem-formula-op='0' id='id-7'>&#x2063;</mo>
2226
1
                <msub data-chem-formula='1' id='id-8'>
2227
1
                    <mi data-chem-element='1' id='id-9'>O</mi>
2228
1
                    <mn id='id-10'>4</mn>
2229
1
                </msub>
2230
1
            </mrow>
2231
1
        </math>";
2232
1
        init_default_prefs(mathml_str, "Enhanced");
2233
1
        return MATHML_INSTANCE.with(|package_instance| {
2234
1
            let package_instance = package_instance.borrow();
2235
1
            let mathml = get_element(&package_instance);
2236
1
            test_command("ZoomIn", mathml, "id-2");
2237
1
            let speech = test_command("MoveNext", mathml, "id-6");
2238
            // tables need to check their parent for proper speech
2239
1
            assert_eq!(speech, "move right; cap s");
2240
1
            return Ok( () );
2241
1
        });
2242
1
    }
2243
2244
    #[test]
2245
1
    fn determinant_speech() -> Result<()> {
2246
1
        let mathml_str = "<math id='math'>
2247
1
            <mrow id='mrow'>
2248
1
            <mo id='open'>|</mo>
2249
1
            <mtable columnspacing='1em' rowspacing='4pt' id='table'>
2250
1
                <mtr id='row-1'>
2251
1
                    <mtd id='1-1'><mn id='id-6'>9</mn></mtd>
2252
1
                    <mtd id='1-2'><mrow id='id-8'><mo id='id-9'>-</mo><mn id='id-10'>13</mn></mrow></mtd>
2253
1
                </mtr>
2254
1
                <mtr id='row-2'>
2255
1
                    <mtd id='2-1'><mn id='id-13'>5</mn></mtd>
2256
1
                    <mtd id='2-2'><mrow id='row2-negative'><mo id='id-16'>-</mo><mn id='id-17'>6</mn></mrow></mtd>
2257
1
                </mtr>
2258
1
            </mtable>
2259
1
            <mo id='close'>|</mo>
2260
1
            </mrow>
2261
1
        </math>";
2262
1
        init_default_prefs(mathml_str, "Enhanced");
2263
1
        set_preference("SpeechStyle", "ClearSpeak").unwrap();
2264
1
        return MATHML_INSTANCE.with(|package_instance| {
2265
1
            let package_instance = package_instance.borrow();
2266
1
            let mathml = get_element(&package_instance);
2267
1
            let speech = test_command("ZoomIn", mathml, "row-1");
2268
1
            assert_eq!(speech, "zoom in; row 1; 9, negative 13");
2269
1
            let speech = test_command("MoveNext", mathml, "row-2");
2270
1
            assert_eq!(speech, "move right; row 2; 5, negative 6");
2271
1
            let speech = test_command("MoveNext", mathml, "row-2");
2272
1
            assert_eq!(speech, "cannot move right, end of math");
2273
1
            let speech = test_command("ZoomIn", mathml, "id-13");
2274
1
            assert_eq!(speech, "zoom in; column 1; 5");
2275
1
            let speech = test_command("MoveNext", mathml, "row2-negative");
2276
1
            assert_eq!(speech, "move right; column 2, negative 6");
2277
1
            let speech = test_command("ZoomOutAll", mathml, "table");
2278
1
            assert_eq!(speech, "zoomed out all of the way; the 2 by 2 determinant; row 1; 9, negative 13; row 2; 5, negative 6");
2279
1
            return Ok( () );
2280
1
        });
2281
1
    }
2282
2283
    #[test]
2284
1
    fn cases_speech() -> Result<()> {
2285
1
        let mathml_str = "<math id='id-0'>
2286
1
        <mrow id='id-1'>
2287
1
          <mo id='open'>{</mo>
2288
1
          <mtable columnalign='left left' columnspacing='1em' displaystyle='false' rowspacing='.2em' id='table'>
2289
1
            <mtr id='row-1'>
2290
1
              <mtd id='id-5'><mrow id='id-6'><mrow id='id-7'><mo id='id-8'>-</mo><mi id='id-9'>x</mi></mrow><mo id='id-10'>,</mo></mrow></mtd>
2291
1
              <mtd id='id-11'><mrow id='id-12'><mrow id='id-13'><mtext id='id-14'>if</mtext><mo id='id-15'>&#x2062;</mo><mi id='id-16'>x</mi></mrow><mo id='id-17'>&lt;</mo><mn id='id-18'>0</mn></mrow></mtd>
2292
1
            </mtr>
2293
1
            <mtr id='row-2'>
2294
1
              <mtd id='id-20'><mrow id='id-21'><mrow id='id-22'><mo id='id-23'>+</mo><mi id='id-24'>x</mi></mrow><mo id='id-25'>,</mo></mrow></mtd>
2295
1
              <mtd id='id-26'><mrow id='id-27'><mrow id='id-28'><mtext id='id-29'>if</mtext><mo id='id-30'>&#x2062;</mo><mi id='id-31'>x</mi></mrow><mo id='id-32'>≥</mo><mn id='id-33'>0</mn></mrow></mtd>
2296
1
            </mtr>
2297
1
          </mtable>
2298
1
        </mrow>
2299
1
       </math>";
2300
1
        init_default_prefs(mathml_str, "Enhanced");
2301
1
        set_preference("SpeechStyle", "ClearSpeak").unwrap();
2302
1
        return MATHML_INSTANCE.with(|package_instance| {
2303
1
            let package_instance = package_instance.borrow();
2304
1
            let mathml = get_element(&package_instance);
2305
1
            test_command("ZoomIn", mathml, "row-1");
2306
1
            let speech = test_command("MovePrevious", mathml, "row-1");
2307
1
            assert_eq!(speech, "move left; start of math");
2308
1
            let speech = test_command("MoveNext", mathml, "row-2");
2309
1
            assert_eq!(speech, "move right; case 2; positive x comma; if x, is greater than or equal to 0");
2310
1
            let speech = test_command("ZoomOut", mathml, "table");
2311
1
            assert_eq!(speech, "zoom out; 2 cases; case 1; negative x comma; if x is less than 0; case 2; positive x comma; if x, is greater than or equal to 0");
2312
1
            let speech = test_command("ZoomIn", mathml, "row-1");
2313
1
            assert_eq!(speech, "zoom in; case 1; negative x comma; if x is less than 0");
2314
1
            set_preference("NavMode", "Character").unwrap();
2315
1
            let speech = test_command("MovePrevious", mathml, "open");
2316
1
            assert_eq!(speech, "move left; open brace");
2317
1
            return Ok( () );
2318
1
        });
2319
1
    }
2320
2321
    #[test]
2322
1
    fn base_superscript() -> Result<()> {
2323
        // bug #217 -- zoom into base of parenthesized script 
2324
1
        let mathml_str = "<math display='block' id='id-0'>
2325
1
            <msup id='id-1'>
2326
1
                <mrow id='id-2'>
2327
1
                    <mo stretchy='false' id='id-3'>(</mo>
2328
1
                    <mrow id='id-4'>
2329
1
                        <mn id='id-5'>2</mn>
2330
1
                        <mo id='id-6'>&#x2062;</mo>
2331
1
                        <mi id='id-7'>x</mi>
2332
1
                    </mrow>
2333
1
                    <mo stretchy='false' id='id-8'>)</mo>
2334
1
                </mrow>
2335
1
                <mn id='id-9'>2</mn>
2336
1
            </msup>
2337
1
        </math>";
2338
1
        init_default_prefs(mathml_str, "Enhanced");
2339
1
        set_preference("SpeechStyle", "ClearSpeak").unwrap();
2340
1
        return MATHML_INSTANCE.with(|package_instance| {
2341
1
            let package_instance = package_instance.borrow();
2342
1
            let mathml = get_element(&package_instance);
2343
1
            let speech = test_command("ZoomIn", mathml, "id-4");
2344
1
            assert_eq!(speech, "zoom in; in base; 2 x");
2345
1
            let speech = test_command("MoveNext", mathml, "id-9");
2346
1
            assert_eq!(speech, "move right; in exponent; 2");
2347
1
            return Ok( () );
2348
1
        });
2349
1
    }
2350
2351
    #[test]
2352
1
    fn binomial_intent() -> Result<()> {
2353
1
        let mathml_str = "<math display='block' id='id-0'>
2354
1
                    <mrow intent='binomial($n,$k)' id='id-1'>
2355
1
                        <mo id='id-2'>(</mo>
2356
1
                        <mfrac linethickness='0pt' id='id-3'>
2357
1
                            <mi arg='n' id='id-4'>n</mi>
2358
1
                            <mi arg='k' id='id-5'>k</mi>
2359
1
                        </mfrac>
2360
1
                    <mo id='id-6'>)</mo>
2361
1
                    </mrow>
2362
1
                </math>";
2363
1
        init_default_prefs(mathml_str, "Character");
2364
1
        set_preference("SpeechStyle", "ClearSpeak").unwrap();
2365
1
        return MATHML_INSTANCE.with(|package_instance| {
2366
1
            let package_instance = package_instance.borrow();
2367
1
            let mathml = get_element(&package_instance);
2368
1
            debug!("Character mode");
2369
1
            let speech = test_command("MoveStart", mathml, "id-2");
2370
1
            assert_eq!(speech, "move to start of math; open paren");
2371
1
            let speech = test_command("MoveNext", mathml, "id-4");
2372
            // I'm not keen on the use of numerator/denominator here, but character mode turns off intent
2373
1
            assert_eq!(speech, "move right; in numerator; n");
2374
1
            let speech = test_command("MoveNext", mathml, "id-5");
2375
1
            assert_eq!(speech, "move right; in denominator; k");
2376
1
            debug!("before zoom out");
2377
1
            let speech = test_command("ZoomOut", mathml, "id-3");
2378
1
            assert_eq!(speech, "zoom out; out of denominator; n over k");
2379
            // let speech = test_command("ZoomOut", mathml, "id-1");
2380
            // assert_eq!(speech, "zoom out; open paren n over k, close paren");
2381
2382
1
            set_preference("NavMode", "Simple").unwrap();
2383
1
            debug!("Simple mode");
2384
1
            let speech = test_command("ZoomIn", mathml, "id-4");
2385
1
            assert_eq!(speech, "zoom in; in part 1; n");
2386
1
            let speech = test_command("MoveNext", mathml, "id-5");
2387
1
            assert_eq!(speech, "move right; in part 2; k");
2388
1
            let speech = test_command("MoveNext", mathml, "id-5");
2389
1
            assert_eq!(speech, "cannot move right, end of math");
2390
1
            let speech = test_command("ZoomOut", mathml, "id-1-literal-0");
2391
1
            assert_eq!(speech, "zoom out; out of part 2; n choose k");
2392
2393
1
            set_preference("NavMode", "Enhanced").unwrap();
2394
1
            debug!("Enhanced mode");
2395
1
            let speech = test_command("ZoomIn", mathml, "id-4");
2396
1
            assert_eq!(speech, "zoom in; in part 1; n");
2397
1
            let speech = test_command("MoveNext", mathml, "id-5");
2398
1
            assert_eq!(speech, "move right; in part 2; k");
2399
1
            let speech = test_command("MoveNext", mathml, "id-5");
2400
1
            assert_eq!(speech, "cannot move right, end of math");
2401
1
            let speech = test_command("ZoomOut", mathml, "id-1-literal-0");
2402
1
            assert_eq!(speech, "zoom out; out of part 2; n choose k");
2403
2404
1
            return Ok( () );
2405
1
        });
2406
1
    }
2407
2408
    #[test]
2409
1
    fn matrix_literal_intent() -> Result<()> {
2410
1
        let mathml_str = r#"<math display='block' id='id-0'>
2411
1
            <mrow intent='$m' id='id-1'>
2412
1
                <mo id='id-2'>(</mo>
2413
1
                <mtable arg='m' intent='_diagonal:prefix(1,2,3)' id='id-3'>
2414
1
                <mtr id='id-4'>
2415
1
                    <mtd id='id-5'><mn id='id-6'>1</mn></mtd>
2416
1
                    <mtd id='id-7'><mn id='id-8'>0</mn></mtd>
2417
1
                    <mtd id='id-9'><mn id='id-10'>0</mn></mtd>
2418
1
                </mtr>
2419
1
                <mtr id='id-11'>
2420
1
                    <mtd id='id-12'><mn id='id-13'>0</mn></mtd>
2421
1
                    <mtd id='id-14'><mn id='id-15'>2</mn></mtd>
2422
1
                    <mtd id='id-16'><mn id='id-17'>0</mn></mtd>
2423
1
                </mtr>
2424
1
                <mtr id='id-18'>
2425
1
                    <mtd id='id-19'><mn id='id-20'>0</mn></mtd>
2426
1
                    <mtd id='id-21'><mn id='id-22'>0</mn></mtd>
2427
1
                    <mtd id='id-23'><mn id='id-24'>3</mn></mtd>
2428
1
                </mtr>
2429
1
                </mtable>
2430
1
                <mo id='id-25'>)</mo>
2431
1
            </mrow>
2432
1
        </math>"#;
2433
1
        init_default_prefs(mathml_str, "Simple");
2434
1
        return MATHML_INSTANCE.with(|package_instance| {
2435
1
            let package_instance = package_instance.borrow();
2436
1
            let mathml = get_element(&package_instance);
2437
1
            let speech = test_command("ZoomIn", mathml, "id-3-literal-1");
2438
1
            assert_eq!(speech, "zoom in; 1");
2439
1
            let speech = test_command("MoveNext", mathml, "id-3-literal-2");
2440
1
            assert_eq!(speech, "move right; 2");
2441
1
            let speech = test_command("MoveNext", mathml, "id-3-literal-3");
2442
1
            assert_eq!(speech, "move right; 3");
2443
1
            let speech = test_command("MoveNext", mathml, "id-3-literal-3");
2444
1
            assert_eq!(speech, "cannot move right, end of math");
2445
1
            let speech = test_command("ZoomOut", mathml, "id-3-literal-0");
2446
1
            assert_eq!(speech, "zoom out; diagonal 1 2 3");
2447
2448
1
            return Ok( () );
2449
1
        });
2450
1
    }
2451
2452
    #[test]
2453
1
    fn absolute_value() -> Result<()> {
2454
1
        let mathml_str = "<math id='math'>
2455
1
                <mrow id='expr'>
2456
1
                    <mn id='2'>2</mn>
2457
1
                    <mrow id='abs'>
2458
1
                        <mo id='start'>|</mo>
2459
1
                        <mi id='x'>x</mi>
2460
1
                        <mo id='end'>|</mo>
2461
1
                    </mrow>
2462
1
                </mrow>
2463
1
            </math>";
2464
1
        init_default_prefs(mathml_str, "Enhanced");
2465
1
        set_preference("SpeechStyle", "ClearSpeak").unwrap();
2466
1
        return MATHML_INSTANCE.with(|package_instance| {
2467
1
            let package_instance = package_instance.borrow();
2468
1
            let mathml = get_element(&package_instance);
2469
1
            let speech = test_command("ZoomIn", mathml, "2");
2470
1
            assert_eq!(speech, "zoom in; 2");
2471
1
            let speech = test_command("MoveNext", mathml, "abs");
2472
1
            assert_eq!(speech, "move right; the absolute value of x");
2473
1
            let speech = test_command("ZoomIn", mathml, "x");
2474
1
            assert_eq!(speech, "zoom in; in absolute value; x");
2475
1
            let speech = test_command("MoveNext", mathml, "x");
2476
1
            assert_eq!(speech, "cannot move right, end of math");
2477
1
            set_preference("NavMode", "Character").unwrap();
2478
1
            let speech = test_command("MoveNext", mathml, "end");
2479
1
            assert_eq!(speech, "move right; vertical line");
2480
1
            let speech = test_command("MoveLineStart", mathml, "2");
2481
1
            assert_eq!(speech, "move to start of line; 2");
2482
1
            let speech = test_command("MoveNext", mathml, "start");
2483
1
            assert_eq!(speech, "move right; vertical line");
2484
1
            return Ok( () );
2485
1
        });
2486
1
    }
2487
2488
    #[test]
2489
1
    fn read_and_describe_fraction() -> Result<()> {
2490
1
        let mathml_str = "<math id='math'>
2491
1
            <mrow id='mrow'>
2492
1
                <mfrac id='frac'>
2493
1
                    <mrow id='numerator'><mi>b</mi><mo>+</mo><mn>1</mn></mrow>
2494
1
                <mn id='denom'>3</mn>
2495
1
                </mfrac>
2496
1
                <mo id='minus'>-</mo>
2497
1
                <mn id='3'>3</mn>
2498
1
            </mrow>
2499
1
        </math>";
2500
1
        init_default_prefs(mathml_str, "Enhanced");
2501
1
        set_preference("SpeechStyle", "SimpleSpeak").unwrap();
2502
1
        return MATHML_INSTANCE.with(|package_instance| {
2503
1
            let package_instance = package_instance.borrow();
2504
1
            let mathml = get_element(&package_instance);
2505
1
            test_command("ZoomIn", mathml, "frac");
2506
1
            let speech = test_command("ReadCurrent", mathml, "frac");
2507
1
            assert_eq!(speech, "read current; fraction, b plus 1, over 3, end fraction");
2508
1
            let speech = test_command("DescribeCurrent", mathml, "frac");
2509
1
            assert_eq!(speech, "describe current; fraction");
2510
1
            return Ok( () );
2511
1
        });
2512
1
    }
2513
2514
2515
    #[test]
2516
1
    fn read_and_describe_mrow() -> Result<()> {
2517
1
        let mathml_str = "<math id='math'>
2518
1
            <mrow id='mrow'>
2519
1
                <mn>1</mn><mo>+</mo>
2520
1
                <mn>2</mn><mo>+</mo>
2521
1
                <mn>3</mn><mo>+</mo>
2522
1
                <mn>4</mn><mo>+</mo>
2523
1
                <mn>5</mn><mo>+</mo>
2524
1
                <mn>6</mn><mo>+</mo>
2525
1
                <mn>7</mn>
2526
1
            </mrow>
2527
1
        </math>";
2528
1
        init_default_prefs(mathml_str, "Enhanced");
2529
1
        set_preference("SpeechStyle", "SimpleSpeak").unwrap();
2530
1
        return MATHML_INSTANCE.with(|package_instance| {
2531
1
            let package_instance = package_instance.borrow();
2532
1
            let mathml = get_element(&package_instance);
2533
1
            let speech = test_command("ZoomOutAll", mathml, "mrow");
2534
1
            assert_eq!(speech, "zoomed out all of the way; 1 plus 2 plus 3 plus 4 plus 5 plus 6 plus 7");
2535
1
            let speech = test_command("ReadCurrent", mathml, "mrow");
2536
1
            assert_eq!(speech, "read current; 1 plus 2 plus 3 plus 4 plus 5 plus 6 plus 7");
2537
1
            let speech = test_command("DescribeCurrent", mathml, "mrow");
2538
1
            assert_eq!(speech, "describe current; 1 plus 2 plus 3 and so on");
2539
1
            return Ok( () );
2540
1
        });
2541
1
    }
2542
2543
2544
    #[test]
2545
1
    fn read_next_invisible_char() -> Result<()> {
2546
1
        let mathml_str = "<math id='id-0'>
2547
1
            <mrow id='id-1'>
2548
1
                <mi id='id-2'>x</mi>
2549
1
                <mo id='id-3'>&#x2062;</mo>
2550
1
                <mi id='id-4'>y</mi>
2551
1
            </mrow>
2552
1
            </math>";
2553
1
        init_default_prefs(mathml_str, "Simple");
2554
1
        set_preference("SpeechStyle", "SimpleSpeak").unwrap();
2555
1
        return MATHML_INSTANCE.with(|package_instance| {
2556
1
            let package_instance = package_instance.borrow();
2557
1
            let mathml = get_element(&package_instance);
2558
1
            let speech = test_command("ZoomIn", mathml, "id-2");
2559
1
            assert_eq!(speech, "zoom in; x");
2560
1
            let speech = test_command("ToggleZoomLockUp", mathml, "id-2");
2561
1
            assert_eq!(speech, "enhanced mode; x");
2562
1
            let speech = test_command("ReadNext", mathml, "id-2");
2563
1
            assert_eq!(speech, "read right; y");
2564
1
            return Ok( () );
2565
1
        });
2566
1
    }
2567
2568
    
2569
    #[test]
2570
1
    fn basic_language_test() -> Result<()> {
2571
        // this is basically a sanity check that all the language's navigation.yaml files are at least syntactically correct
2572
        // FIX: should look through the Languages dir and figure this is out
2573
1
        let mathml_str = "<math id='math'>
2574
1
                <mrow id='contents'>
2575
1
                    <mrow id='lhs'>
2576
1
                        <mrow id='term'>
2577
1
                            <mn id='2'>2</mn>
2578
1
                            <mo id='invisible-times'>&#x2062;</mo>
2579
1
                            <msup id='msup'>
2580
1
                                <mi id='x'>x</mi>
2581
1
                                <mn id='3'>3</mn>
2582
1
                            </msup>
2583
1
                        </mrow>
2584
1
                        <mo id='plus'>+</mo>
2585
1
                        <mn id='1'>1</mn>
2586
1
                    </mrow>
2587
1
                <mo id='id-11'>=</mo>
2588
1
                <mi id='id-12'>y</mi>
2589
1
                </mrow>
2590
1
            </math>";
2591
        
2592
1
        set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
2593
11
        for lang in 
get_supported_languages1
().
unwrap_or_default1
() {
2594
11
            test_language(&lang, mathml_str);
2595
11
        }
2596
1
        return Ok( () );
2597
2598
11
        fn test_language(lang: &str, mathml_str: &str) {
2599
11
            init_default_prefs(mathml_str, "Enhanced");
2600
11
            set_preference("Language", lang).unwrap();
2601
2602
11
            set_preference("NavMode", "Enhanced").unwrap();
2603
11
            MATHML_INSTANCE.with(|package_instance| {
2604
11
                let package_instance = package_instance.borrow();
2605
11
                let mathml = get_element(&package_instance);
2606
11
                test_command("ZoomInAll", mathml, "2");
2607
11
                test_command("MoveNext", mathml, "msup");
2608
11
                test_command("MoveNext", mathml, "plus");
2609
11
                test_command("MovePrevious", mathml, "term");
2610
11
                test_command("MovePrevious", mathml, "term");
2611
11
                test_command("ZoomOutAll", mathml, "contents");
2612
11
            });
2613
2614
11
            set_preference("NavMode", "Simple").unwrap();
2615
11
            MATHML_INSTANCE.with(|package_instance: &RefCell<Package>| {
2616
11
                let package_instance = package_instance.borrow();
2617
11
                let mathml = get_element(&package_instance);
2618
11
                test_command("ZoomInAll", mathml, "2");
2619
11
                test_command("MoveNext", mathml, "msup");
2620
11
                test_command("MoveNext", mathml, "plus");
2621
11
                test_command("MovePrevious", mathml, "msup");
2622
11
                test_command("MovePrevious", mathml, "2");
2623
11
                test_command("MovePrevious", mathml, "2");
2624
11
                test_command("ZoomOutAll", mathml, "contents");
2625
11
            });
2626
2627
11
            set_preference("NavMode", "Character").unwrap();
2628
11
            MATHML_INSTANCE.with(|package_instance| {
2629
11
                let package_instance = package_instance.borrow();
2630
11
                let mathml = get_element(&package_instance);
2631
11
                test_command("ZoomIn", mathml, "2");
2632
11
                test_command("MoveNext", mathml, "x");
2633
11
                test_command("MoveNext", mathml, "3");
2634
11
                test_command("MoveNext", mathml, "plus");
2635
11
                test_command("MovePrevious", mathml, "3");
2636
11
                test_command("MovePrevious", mathml, "x");
2637
11
                test_command("MovePrevious", mathml, "2");
2638
11
                test_command("MovePrevious", mathml, "2");
2639
11
            });
2640
            
2641
            // simple sanity check that "overview.yaml" doesn't have a syntax error
2642
11
            set_preference("Overview", "True").unwrap();
2643
11
            set_preference("NavMode", "Character").unwrap();
2644
11
            MATHML_INSTANCE.with(|package_instance| {
2645
11
                let package_instance = package_instance.borrow();
2646
11
                let mathml = get_element(&package_instance);
2647
11
                test_command("ZoomIn", mathml, "2");
2648
11
            });
2649
11
        }
2650
1
    }
2651
}