## 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/braille.rs

    Line
    Count
    Source
    1
    #![allow(clippy::needless_return)]
    2
    use strum_macros::Display;
    3
    use sxd_document::dom::{Element, ChildOfElement};
    4
    use sxd_document::Package;
    5
    use crate::definitions::SPEECH_DEFINITIONS;
    6
    use crate::errors::*;
    7
    use crate::pretty_print::mml_to_string;
    8
    use crate::prefs::PreferenceManager;
    9
    use std::cell::Ref;
    10
    use regex::{Captures, Regex, RegexSet};
    11
    use phf::{phf_map, phf_set};
    12
    use crate::speech::{BRAILLE_RULES, SpeechRulesWithContext, braille_replace_chars, make_quoted_string};
    13
    use crate::canonicalize::get_parent;
    14
    use std::borrow::Cow;
    15
    use std::ops::Range;
    16
    use std::sync::LazyLock;
    17
    use log::{debug, error};
    18
    19
    320
    fn is_ueb_prefix(ch: char) -> bool {
    20
    320
        matches!262(ch, '⠼' | '⠈' | '⠘' | '⠸' | '⠐' | '⠨' | '⠰' | '⠠')
    21
    320
    }
    22
    23
    /// Returns the braille *char* at the given position in the braille string.
    24
    971
    fn braille_at(braille: &str, index: usize) -> char {
    25
        // braille is always 3 bytes per char
    26
    971
        return braille[index..index+3].chars().next().unwrap();
    27
    28
    971
    }
    29
    30
    /// braille the MathML
    31
    /// If 'nav_node_id' is not an empty string, then the element with that id will have dots 7 & 8 turned on as per the pref
    32
    /// Returns the braille string (highlighted) along with the *character* start/end of the highlight (whole string if no highlight)
    33
    1.82k
    pub fn braille_mathml(mathml: Element, nav_node_id: &str) -> Result<(String, usize, usize)> {
    34
    1.82k
        return BRAILLE_RULES.with(|rules| {
    35
    1.82k
            rules.borrow_mut().read_files()?0;
    36
    1.82k
            let rules = rules.borrow();
    37
    1.82k
            let new_package = Package::new();
    38
    1.82k
            let mut rules_with_context = SpeechRulesWithContext::new(&rules, new_package.as_document(), nav_node_id, 0);
    39
    1.82k
            let braille_string = rules_with_context.match_pattern::<String>(mathml)
    40
    1.82k
                            .context("Pattern match/replacement failure!")?0;
    41
            // debug!("braille_mathml: braille string: {}", &braille_string);
    42
    1.82k
            let braille_string = braille_string.replace(' ', "");
    43
    1.82k
            let pref_manager = rules_with_context.get_rules().pref_manager.borrow();
    44
    1.82k
            let highlight_style = pref_manager.pref_to_string("BrailleNavHighlight");
    45
    1.82k
            let braille_code = pref_manager.pref_to_string("BrailleCode");
    46
    1.82k
            let braille = match braille_code.as_str() {
    47
    1.82k
                "Nemeth" => nemeth_cleanup888(pref_manager888, braille_string888),
    48
    941
                "UEB" => ueb_cleanup366(pref_manager366, braille_string366),
    49
    575
                "Vietnam" => vietnam_cleanup112(pref_manager112, braille_string112),
    50
    463
                "CMU" => cmu_cleanup372(pref_manager372, braille_string372),
    51
    91
                "Finnish" => finnish_cleanup0(pref_manager0, braille_string0),
    52
    91
                "Swedish" => swedish_cleanup0(pref_manager0, braille_string0),
    53
    91
                "LaTeX" => LaTeX_cleanup50(pref_manager50, braille_string50),
    54
    41
                "ASCIIMath" => ASCIIMath_cleanup(pref_manager, braille_string),
    55
    0
                "ASCIIMath-fi" => ASCIIMath_cleanup(pref_manager, braille_string),
    56
    0
                _ => braille_string.trim_matches('⠀').to_string(),    // probably needs cleanup if someone has another code, but this will have to get added by hand
    57
            };
    58
    59
            return Ok(
    60
    1.82k
                if highlight_style != "Off" {
    61
    520
                    highlight_braille_chars(braille, &braille_code, highlight_style == "All")
    62
                } else {
    63
    1.30k
                    let end = braille.len()/3;
    64
    1.30k
                    (braille, 0, end)
    65
                }
    66
            );
    67
    1.82k
        });
    68
    69
        /// highlight with dots 7 & 8 based on the highlight style
    70
        /// both the start and stop points will be extended to deal with indicators such as capitalization
    71
        /// if 'fill_range' is true, the interior will be highlighted
    72
        /// Returns the braille string (highlighted) along with the [start, end) *character* of the highlight (whole string if no highlight)
    73
    520
        fn highlight_braille_chars(braille: String, braille_code: &str, fill_range: bool) -> (String, usize, usize) {
    74
    520
            let mut braille = braille;
    75
            // some special (non-braille) chars weren't converted to having dots 7 & 8 to indicate navigation position
    76
            // they need to be added to the start
    77
    78
            // find start and end (byte) indexes of the highlighted region (braille chars have length=3 bytes)
    79
    520
            let start = braille.find(is_highlighted);
    80
    520
            let end = braille.rfind(is_highlighted);
    81
    520
            if start.is_none() {
    82
    57
                assert!(end.is_none());
    83
    57
                let end = braille.len();
    84
    57
                return (braille, 0, end/3);
    85
    463
            };
    86
    87
    463
            let start = start.unwrap();
    88
    463
            let mut end = end.unwrap() + 3;         // always exists if start exists ('end' is exclusive)
    89
            // debug!("braille highlight: start/end={}/{}; braille={}", start/3, end/3, braille);
    90
    463
            let mut start = highlight_first_indicator(&mut braille, braille_code, start, end);
    91
    463
            if let Some(new_range45) = expand_highlight(&mut braille, braille_code, start, end) {
    92
    45
                (start, end) = new_range
    93
    418
            }
    94
    95
    463
            if start == end {
    96
    0
                return (braille, start/3, end/3);
    97
    463
            }
    98
    99
    463
            if !fill_range {
    100
    459
                return (braille, start/3, end/3);
    101
    4
            }
    102
    103
    4
            let mut result = String::with_capacity(braille.len());
    104
    4
            result.push_str(&braille[..start]);
    105
    4
            let highlight_region =&mut braille[start..end];
    106
    8
            for ch in highlight_region4.chars4() {
    107
    8
                result.push( highlight(ch) );
    108
    8
            };
    109
    4
            result.push_str(&braille[end..]);
    110
    4
            return (result, start/3, end/3);
    111
    112
            /// Return the byte index of the first place to highlight
    113
    463
            fn highlight_first_indicator(braille: &mut String, braille_code: &str, start_index: usize, end_index: usize) -> usize {
    114
                // chars in the braille block range use 3 bytes -- we can use that to optimize the code some
    115
    463
                let first_ch = unhighlight(braille_at(braille, start_index));
    116
    117
                // need to highlight (optional) capital/number, language, and style (max 2 chars) also in that (rev) order
    118
    463
                let mut prefix_ch_index = std::cmp::max(0, start_index as isize - 5*3) as usize;
    119
    463
                if prefix_ch_index == 0 && braille_code == "UEB"194 {
    120
                    // don't count the word or passage mode as part of a indicator
    121
    46
                    if braille.starts_with("⠰⠰⠰") {
    122
    42
                        prefix_ch_index = 9;
    123
    42
                    } else if braille.starts_with("⠰⠰")4 {
    124
    0
                        prefix_ch_index = 6;
    125
    4
                    }
    126
    417
                }
    127
    463
                let indicators = &braille[prefix_ch_index..start_index];   // chars to be examined
    128
    463
                let i_byte_start = start_index - 3 * match braille_code {
    129
    463
                    "Nemeth" => i_start_nemeth129(indicators129, first_ch129),
    130
    334
                    _ => i_start_ueb(indicators),               // treat all the other like UEB because they probably have similar number and letter prefixes
    131
                };
    132
    463
                if i_byte_start < start_index {
    133
                    // remove old highlight as long as we don't wipe out the end highlight
    134
    59
                    if start_index < end_index {
    135
    59
                        let old_first_char_bytes = start_index..start_index+3;
    136
    59
                        let replacement_str = unhighlight(braille_at(braille, start_index)).to_string();
    137
    59
                        braille.replace_range(old_first_char_bytes, &replacement_str);
    138
    59
                    }0
    139
    140
                    // add new highlight
    141
    59
                    let new_first_char_bytes = i_byte_start..i_byte_start+3;
    142
    59
                    let replacement_str = highlight(braille_at(braille, i_byte_start)).to_string();
    143
    59
                    braille.replace_range(new_first_char_bytes, &replacement_str);
    144
    404
                }
    145
    146
    463
                return i_byte_start;
    147
    463
            }
    148
    149
            /// Return the byte indexes of the first and last place to highlight
    150
            /// Currently, this only does something for CMU braille
    151
    463
            fn expand_highlight(braille: &mut String, braille_code: &str, start_index: usize, end_index: usize) -> Option<(usize, usize)> {
    152
                // For CMU, we want to expand mrows to include the opening and closing grouping indicators if they exist
    153
    463
                if start_index == 0 || end_index402 == braille.len() || braille_code != "CMU"352 {
    154
    358
                    return None;
    155
    105
                }
    156
    157
    105
                let first_ch = unhighlight(braille_at(braille, start_index));
    158
    105
                let last_ch = unhighlight(braille_at(braille, end_index-3));
    159
                // We need to be careful not to expand the selection if we are already on a grouping indicator
    160
    105
                if first_ch == '⠢' && last_ch == '⠔'0{
    161
    0
                    return None;
    162
    105
                }
    163
    105
                let preceding_ch = braille_at(braille, start_index-3);
    164
    105
                if preceding_ch != '⠢' {
    165
    43
                    return None;
    166
    62
                }
    167
    168
    62
                let following_ch = braille_at(braille, end_index);
    169
    62
                if following_ch != '⠔' {
    170
    17
                    return None;
    171
    45
                }
    172
    173
    45
                let preceding_ch = highlight(preceding_ch);
    174
    45
                braille.replace_range(start_index-3..start_index+3, format!("{preceding_ch}{first_ch}").as_str());
    175
    45
                let following_ch = highlight(following_ch);
    176
    45
                braille.replace_range(end_index-3..end_index+3, format!("{last_ch}{following_ch}").as_str());
    177
    45
                return Some( (start_index-3, end_index + 3) );
    178
    463
            }
    179
    520
        }
    180
    181
        /// Given a position in a Nemeth string, what is the position character that starts it (e.g, the prev char for capital letter)
    182
    129
        fn i_start_nemeth(braille_prefix: &str, first_ch: char) -> usize {
    183
    0
            fn is_nemeth_number(ch: char) -> bool {
    184
    0
                matches!(ch, '⠂' | '⠆' | '⠒' | '⠲' | '⠢' | '⠖' | '⠶' | '⠦' | '⠔' | '⠴' | '⠨')
    185
    0
            }
    186
    129
            let mut n_chars = 0;
    187
    129
            let prefix = &mut braille_prefix.chars().rev().peekable();
    188
    129
            if prefix.peek() == Some(&'⠠') ||  // cap indicator
    189
    129
               (prefix.peek() == Some(&'⠼') && is_nemeth_number0(first_ch0)) ||  // number indicator
    190
    129
               [Some(&'⠸'), Some(&'⠈'), Some(&'⠨')].contains(&prefix.peek()) {         // bold, script/blackboard, italic indicator
    191
    1
                n_chars += 1;
    192
    1
                prefix.next();
    193
    128
            }
    194
    195
    129
            if [Some(&'⠰'), Some(&'⠸'), Some(&'⠨')].contains(&prefix.peek()) {   // English, German, Greek
    196
    0
                n_chars += 1;
    197
    129
            } else if prefix.peek() == Some(&'⠈') {
    198
    0
                let ch = prefix.next();                              // Russian/Greek Variant
    199
    0
                if ch == Some('⠈') || ch == Some('⠨') {
    200
    0
                    n_chars += 2;
    201
    0
                }
    202
    129
            } else if prefix.peek() == Some(&'⠠')  { // Hebrew
    203
    0
                let ch = prefix.next();                              // Russian/Greek Variant
    204
    0
                if ch == Some('⠠') {
    205
    0
                    n_chars += 2;
    206
    0
                }
    207
    129
            };
    208
    129
            return n_chars;
    209
    129
        }
    210
    211
        /// Given a position in a UEB string, what is the position character that starts it (e.g, the prev char for capital letter)
    212
    334
        fn i_start_ueb(braille_prefix: &str) -> usize {
    213
    334
            let prefix = &mut braille_prefix.chars().rev().peekable();
    214
    334
            let mut n_chars = 0;
    215
    392
            while let Some(ch320) = prefix.next() {
    216
    320
                if is_ueb_prefix(ch) {
    217
    58
                    n_chars += 1;
    218
    262
                } else if ch == '⠆' {
    219
    0
                    let n_typeform_chars = check_for_typeform(prefix);
    220
    0
                    if n_typeform_chars > 0 {
    221
    0
                        n_chars += n_typeform_chars;
    222
    0
                    } else {
    223
    0
                        break;
    224
                    }
    225
                } else {
    226
    262
                    break;
    227
                }
    228
            }
    229
    334
            return n_chars;
    230
    334
        }
    231
    232
    233
    0
        fn check_for_typeform(prefix: &mut dyn std::iter::Iterator<Item=char>) -> usize {
    234
    0
            fn is_ueb_typeform_prefix(ch: char) -> bool {
    235
    0
                matches!(ch, '⠈' | '⠘' | '⠸' | '⠨')
    236
    0
            }
    237
    238
    0
            if let Some(typeform_indicator) = prefix.next() {
    239
    0
                if is_ueb_typeform_prefix(typeform_indicator) {
    240
    0
                    return 2;
    241
    0
                } else if typeform_indicator == '⠼' &&
    242
    0
                          let Some(user_defined_typeform_indicator) = prefix.next() &&
    243
    0
                          (is_ueb_typeform_prefix(user_defined_typeform_indicator) || user_defined_typeform_indicator == '⠐') {
    244
    0
                            return 3;
    245
    0
                        }
    246
    0
            }
    247
    0
            return 0;
    248
    0
        }
    249
    1.82k
    }
    250
    251
    // FIX: if 8-dot braille is needed, perhaps the highlights can be shifted to a "highlighted" 256 char block in private space
    252
    //   they would need to be unshifted for the external world
    253
    11.0k
    fn is_highlighted(ch: char) -> bool {
    254
    11.0k
        let ch_as_u32 = ch as u32;
    255
    11.0k
        return (0x28C0..0x28FF).contains(&ch_as_u32) || ch == '𝑏'9.99k;           // 0x28C0..0x28FF all have dots 7 & 8 on
    256
    11.0k
    }
    257
    258
    159
    fn highlight(ch: char) -> char {
    259
        // safe because we have checked the range
    260
    159
        return unsafe{char::from_u32_unchecked(ch as u32 | 0xC0)};    // 0x28C0..0x28FF all have dots 7 & 8 on
    261
    159
    }
    262
    263
    3.12k
    fn unhighlight(ch: char) -> char {
    264
    3.12k
        let ch_as_u32 = ch as u32;
    265
    3.12k
        if (0x28C0..0x28FF).contains(&ch_as_u32) {              // 0x28C0..0x28FF all have dots 7 & 8 on
    266
    903
            return unsafe{char::from_u32_unchecked(ch_as_u32 & 0x283F)};  // safe because we have checked the range
    267
        } else {
    268
    2.22k
            return ch;
    269
        }
    270
    3.12k
    }
    271
    272
    use std::cell::RefCell;
    273
    thread_local!{
    274
        /// Count number of probes -- get a sense of how well algorithm is working (for debugging)
    275
        static N_PROBES: RefCell<usize> = const { RefCell::new(0) };
    276
    }
    277
    278
    279
    /// Given a 0-based braille position, return the id of the smallest MathML node enclosing it.
    280
    /// This node might be a leaf with an offset.
    281
    91
    pub fn get_navigation_node_from_braille_position(mathml: Element, position: usize) -> Result<(String, usize)> {
    282
        // This works via a "smart" binary search (the trees aren't binary or balanced, we estimate the child to look in):
    283
        //   braille the mathml with a nav node and see where 'position' is in relation to the start/end of the nav node
    284
        // Each call to find_navigation_node() returns a search state that tell us where to look next if not found
    285
        #[derive(Debug, Display)]
    286
        enum SearchStatus {
    287
            LookInParent,       // look up a level for exact match
    288
            LookLeft,           // went too far, backup
    289
            LookRight,          // continue searching right
    290
            Found,
    291
        }
    292
    293
        struct SearchState<'e> {
    294
            status: SearchStatus,
    295
            node: Element<'e>,
    296
            highlight_start: usize,     // if status is Found, then this is the offset within a leaf node
    297
            highlight_end: usize,       // if status is Found, this is ignored
    298
        }
    299
    300
        // save the current highlight state, set the state to be the end points so we can find the braille, then restore the state
    301
        // FIX: this can fail if there is 8-dot braille
    302
        use crate::interface::{get_preference, set_preference};
    303
    91
        let saved_highlight_style = get_preference("BrailleNavHighlight").unwrap();
    304
    91
        set_preference("BrailleNavHighlight", "EndPoints").unwrap();
    305
    306
    91
        N_PROBES.with(|n| {*n.borrow_mut() = 0});
    307
        // dive into the child of the <math> element (should only be one)
    308
    91
        let search_state = find_navigation_node(mathml, as_element(mathml.children()[0]), position)?0;
    309
    91
        set_preference("BrailleNavHighlight", saved_highlight_style.as_str()).unwrap();
    310
    311
        // we know the attr value exists because it was found internally
    312
        // FIX: what should be done if we never did the search?
    313
    91
        match search_state.status {
    314
            SearchStatus::Found | SearchStatus::LookInParent => {
    315
    86
                return Ok( (search_state.node.attribute_value("id").unwrap().to_string(), search_state.highlight_start) )
    316
            },
    317
            _ => {
    318
                // weird state -- return the entire expr
    319
    5
                match mathml.attribute_value("id") {
    320
    0
                    None => bail!("'id' is not present on mathml: {}", mml_to_string(mathml)),
    321
    5
                    Some(id) => return Ok( (id.to_string(), 0) ),
    322
                }
    323
            }
    324
        }
    325
    326
        /// find the navigation node that most tightly encapsulates the target position (0-based)
    327
        /// 'node' is the current node we are on inside of 'mathml'
    328
    465
        fn find_navigation_node<'e>(mathml: Element<'e>, node: Element<'e>, target_position: usize) -> Result<SearchState<'e>> {
    329
    465
            let node_id = match node.attribute_value("id") {
    330
    465
                Some(id) => id,
    331
    0
                None => bail!("'id' is not present on mathml: {}", mml_to_string(node)),
    332
            };
    333
    465
            N_PROBES.with(|n| {*n.borrow_mut() += 1});
    334
    465
            let (braille, char_start, char_end) = braille_mathml(mathml, node_id)?0;
    335
    465
            let mut status = None;
    336
            // debug!("find_navigation_node ({}, id={}): highlight=[{}, {});  target={}", name(node), node_id, char_start, char_end, target_position);
    337
    465
            if is_leaf(node) {
    338
    100
                if char_start == 0 && char_end10 == braille.len()/3 {
    339
    6
                    // nothing highlighted -- probably invisible char not represented in braille -- continue looking to the right
    340
    6
                    // debug!("  return due invisible char (?)' ");
    341
    6
                    status = Some(SearchStatus::LookRight);
    342
    94
                } else if char_start <= target_position && target_position < char_end88 {
    343
                    // FIX: need to handle multi-char leaves and set the offset (char_start) appropriately
    344
                    // debug!("  return due to target_position inside leaf: {} <= {} < {}", char_start, target_position, char_end);
    345
    58
                    return Ok( SearchState {
    346
    58
                        status: SearchStatus::Found,
    347
    58
                        node,
    348
    58
                        highlight_start: target_position - char_start,
    349
    58
                        highlight_end: 0,
    350
    58
                    });
    351
    36
                } else if name(node) == "mo" {
    352
                    // if there is whitespace before or after the operator, consider the operator to be a match
    353
    18
                    if (char_start > 0 && target_position == char_start - 1 &&
    354
    2
                        braille_at(&braille, 3*(char_start - 1)) == '⠀' && is_operator_that_adds_whitespace(node)) ||
    355
    16
                       (3*char_end < braille.len() && target_position == char_end &&
    356
    11
                        braille_at(&braille, 3*char_end) == '⠀' && is_operator_that_adds_whitespace2(node2)) {
    357
    4
                        return Ok( SearchState {
    358
    4
                            status: SearchStatus::Found,
    359
    4
                            node,
    360
    4
                            highlight_start: 0,
    361
    4
                            highlight_end: 0,
    362
    4
                        } );
    363
    14
                    }
    364
    18
                }
    365
    365
            }
    366
    403
            if status.is_none() {
    367
    397
                if target_position < char_start {
    368
    23
                    // debug!("  return due to target_position {} < start {}", target_position, char_start);
    369
    23
                    status = Some(SearchStatus::LookLeft);
    370
    374
                } else if target_position >= char_end {
    371
    49
                    // debug!("  return due to target_position {} >= end {}", target_position, char_end);
    372
    49
                    status = Some(SearchStatus::LookRight);
    373
    325
                }
    374
    6
            }
    375
    403
            if let Some(status78) = status {
    376
    78
                return Ok( SearchState {
    377
    78
                    status,
    378
    78
                    node,
    379
    78
                    highlight_start: char_start,
    380
    78
                    highlight_end: char_end,
    381
    78
                } );
    382
    325
            }
    383
    384
    325
            let children = node.children();
    385
    325
            let mut i_left_child = 0;                         // inclusive
    386
    325
            let mut i_right_child = children.len();           // exclusive
    387
    325
            let mut call_start = char_start;
    388
    325
            let mut guess_fn: Box<dyn Fn(usize, usize, usize, usize) -> usize> = Box::new(|i_left, i_right, start, target: usize| guess_child_node_ltr(&children, i_left, i_right, start, target));
    389
    398
            while i_left_child < i_right_child {
    390
    374
                let i_guess_child = guess_fn(i_left_child, i_right_child, call_start, target_position);
    391
    374
                let status = find_navigation_node(mathml, as_element(children[i_guess_child]), target_position)?0;
    392
                // debug!("  in {} loop: status: {}, child: left/guess/right {}/({},{})/{}; highlight=[{}, {})",
    393
                //         name(node), status.status,
    394
                //         i_left_child, i_guess_child, name(as_element(children[i_guess_child])),i_right_child,
    395
                //         status.highlight_start, status.highlight_end);
    396
    374
                match status.status {
    397
                    SearchStatus::Found => {
    398
    301
                        return Ok(status);
    399
                    },
    400
                    SearchStatus::LookInParent => {
    401
    0
                        let (_, start, end) = braille_mathml(mathml, node_id)?;
    402
                        // debug!("  parent ({}) braille: start/end={}/{};  target_position={}", name(node), start, end, target_position);
    403
    0
                        if start <= target_position && target_position < end {
    404
                            // debug!("  ..found: id={}", node_id);
    405
    0
                            return Ok( SearchState{
    406
    0
                                status: SearchStatus::Found,
    407
    0
                                node,
    408
    0
                                highlight_start: 0,
    409
    0
                                highlight_end: 0,
    410
    0
                            } );      // done or look up another level
    411
    0
                        }
    412
    0
                        return Ok(status);  // look up a level
    413
                    },
    414
                    SearchStatus::LookLeft => {
    415
    20
                        i_right_child = if i_guess_child == 0 {09} else {i_guess_child11};         // exclusive
    416
    20
                        call_start = status.highlight_start-1;
    417
    20
                        guess_fn = Box::new(|i_left, i_right, start, target| guess_child_node_rtl7(&children7, i_left7, i_right7, start7, target7));
    418
                    },
    419
                    SearchStatus::LookRight => {
    420
    53
                        i_left_child = i_guess_child+1;
    421
    53
                        call_start = status.highlight_end+1;
    422
    53
                        guess_fn = Box::new(|i_left, i_right, start, target| guess_child_node_ltr42(&children42, i_left42, i_right42, start42, target42));
    423
                    },
    424
                }
    425
            }
    426
            // debug!("Didn't child in node {}: left/right={}/{};  target_position={}", name(node), i_left_child, i_right_child, target_position);
    427
    428
            // if we get here, we didn't find it in the children
    429
            // debug!("..end of loop: look in parent of {} has start/end={}/{}", name(node), char_start, char_end);
    430
            return Ok( SearchState{
    431
    24
                status: if char_start <= target_position && target_position <= char_end {SearchStatus::Found} else {SearchStatus::LookInParent0},
    432
    24
                node,
    433
                highlight_start: 0,
    434
                highlight_end: 0,
    435
            } );
    436
    465
        }
    437
    438
    4
        fn is_operator_that_adds_whitespace(node: Element) -> bool {
    439
            use crate::definitions::BRAILLE_DEFINITIONS;
    440
    4
            if PreferenceManager::get().borrow().pref_to_string("UseSpacesAroundAllOperators") == "true" {
    441
    0
                return true;
    442
    4
            }
    443
    444
    4
            return BRAILLE_DEFINITIONS.with(|definitions| {
    445
    4
                let definitions = definitions.borrow();
    446
    4
                let comparison_operators = definitions.get_hashset("ComparisonOperators").unwrap();
    447
    4
                return comparison_operators.contains(as_text(node));
    448
    4
            });
    449
    4
        }
    450
    451
        /// look in children[i_left..i_right] for a count that exceeds target
    452
    367
        fn guess_child_node_ltr(children: &[ChildOfElement], i_left: usize, i_right: usize, start: usize, target: usize) -> usize {
    453
    367
            let mut estimated_position = start;
    454
            // number of chars to add for number indicators
    455
    367
            let n_number_indicator = if PreferenceManager::get().borrow().pref_to_string("BrailleCode") == "Nemeth" {0106} else {1261};   // Nemeth doesn't typically need number or letter indicators
    456
            #[allow(clippy::needless_range_loop)]  // I don't like enumerate/take/skip here
    457
    666
            for i in i_left..i_right367 {
    458
    666
                estimated_position += estimate_braille_chars(children[i], n_number_indicator);
    459
    666
                if estimated_position >= target {
    460
    344
                    return i;
    461
    322
                }
    462
            }
    463
    23
            return i_right-1;       // estimate was too large, return the last child as a guess
    464
    367
        }
    465
    466
        /// look in children[i_left..i_right].rev for a count that is less than target
    467
    7
        fn guess_child_node_rtl(children: &[ChildOfElement], i_left: usize, i_right: usize, start: usize, target: usize) -> usize {
    468
    7
            let mut estimated_position = start;
    469
    7
            let n_number_indicator = if PreferenceManager::get().borrow().pref_to_string("BrailleCode") == "Nemeth" {01} else {16};   // Nemeth doesn't typically need number or letter indicators
    470
    7
            for i in (i_left..i_right).rev() {
    471
    7
                estimated_position -= estimate_braille_chars(children[i], n_number_indicator);
    472
    7
                if estimated_position <= target {
    473
    7
                    return i;
    474
    0
                }
    475
            }
    476
    0
            return i_left;       // estimate was too small, return the first child as a guess
    477
    7
        }
    478
    479
    4.58k
        fn estimate_braille_chars(child: ChildOfElement, n_number_indicator: usize) -> usize {
    480
    4.58k
            let node = as_element(child);
    481
    4.58k
            let leaf_name = name(node);
    482
    4.58k
            if is_leaf(node) {
    483
    3.13k
                let text = as_text(node);
    484
                // len() is close since mn's probably have ASCII digits and lower case vars are common (count as) and other chars need extra braille chars
    485
                // don't want to count invisible chars since they don't display and would give a length = 3
    486
    3.13k
                if text == "\u{2061}" || text == "\u{2062}"  {       // invisible function apply/times (most common by far)
    487
    597
                    return 0;
    488
    2.53k
                }
    489
                // FIX: this assumption is bad for 8-dot braille
    490
    2.53k
                return match leaf_name {
    491
    2.53k
                    "mn" => n_number_indicator632 + text.len(),
    492
    1.90k
                    "mo" => 2741,  // could do better by actually brailling char, but that is more expensive
    493
    1.16k
                    _ => text.len(),
    494
                }
    495
    1.45k
            }
    496
    1.45k
            let mut estimate = if leaf_name == "mrow" {0924} else {node.children().len() + 1526};     // guess extra chars need for mfrac, msub, etc (start+intermediate+end).
    497
    1.45k
            if leaf_name == "msup" || leaf_name == "msub"1.19k || leaf_name == "msubsup"1.19k {
    498
    260
                estimate -= 1;   // opening superscript/subscript indicator not needed
    499
    1.19k
            }
    500
    3.91k
            for child in node1.45k.children1.45k() {
    501
    3.91k
                estimate += estimate_braille_chars(child, n_number_indicator);
    502
    3.91k
            }
    503
            // debug!("estimate_braille_chars for {}: {}", crate::canonicalize::element_summary(as_element(child)), estimate);
    504
    1.45k
            return estimate;
    505
    4.58k
        }
    506
    91
    }
    507
    508
    888
    fn nemeth_cleanup(pref_manager: Ref<PreferenceManager>, raw_braille: String) -> String {
    509
        // Typeface: S: sans-serif, B: bold, T: script/blackboard, I: italic, R: Roman
    510
        // Language: E: English, D: German, G: Greek, V: Greek variants, H: Hebrew, U: Russian
    511
        // Indicators: C: capital, N: number, P: punctuation, M: multipurpose
    512
        // Others:
    513
        //      W -- whitespace that should be kept (e.g, in a numeral)
    514
        //      𝑁 -- hack for special case of a lone decimal pt -- not considered a number but follows rules mostly
    515
        // SRE doesn't have H: Hebrew or U: Russian, so not encoded (yet)
    516
        // Note: some "positive" patterns find cases to keep the char and transform them to the lower case version
    517
        static NEMETH_INDICATOR_REPLACEMENTS: phf::Map<&str, &str> = phf_map! {
    518
            "S" => "⠠⠨",    // sans-serif
    519
            "B" => "⠸",     // bold
    520
            "𝔹" => "⠨",     // blackboard
    521
            "T" => "⠈",     // script
    522
            "I" => "⠨",     // italic (mapped to be the same a blackboard)
    523
            "R" => "",      // roman
    524
            "E" => "⠰",     // English
    525
            "D" => "⠸",     // German (Deutsche)
    526
            "G" => "⠨",     // Greek
    527
            "V" => "⠨⠈",    // Greek Variants
    528
            "H" => "⠠⠠",    // Hebrew
    529
            "U" => "⠈⠈",    // Russian
    530
            "C" => "⠠",     // capital
    531
            "P" => "⠸",     // punctuation
    532
            "𝐏" => "⠸",     // hack for punctuation after a roman numeral -- never removed
    533
            "L" => "",      // letter
    534
            "l" => "",      // letter inside enclosed list
    535
            "M" => "",      // multipurpose indicator
    536
            "m" => "⠐",     // required multipurpose indicator
    537
            "N" => "",      // potential number indicator before digit
    538
            "n" => "⠼",     // required number indicator before digit
    539
            "𝑁" => "",      // hack for special case of a lone decimal pt -- not considered a number but follows rules mostly
    540
            "W" => "⠀",     // whitespace
    541
            "w" => "⠀",     // whitespace from comparison operator
    542
            "," => "⠠⠀",    // comma
    543
            "b" => "⠐",     // baseline
    544
            "𝑏" => "⣐",     // highlight baseline (it's a hack)
    545
            "↑" => "⠘",     // superscript
    546
            "↓" => "⠰",     // subscript
    547
        };
    548
    549
        // Add an English Letter indicator. This involves finding "single letters".
    550
        // The green book has a complicated set of cases, but the Nemeth UEB Rule book (May 2020), 4.10 has a much shorter explanation:
    551
        //   punctuation or whitespace on the left and right ignoring open/close chars
    552
        //   https://nfb.org/sites/www.nfb.org/files/files-pdf/braille-certification/lesson-4--provisional-5-9-20.pdf
    553
    2
        static ADD_ENGLISH_LETTER_INDICATOR: LazyLock<Regex> = LazyLock::new(|| {
    554
    2
            Regex::new(r"(?P<start>^|W|P.[\u2800-\u28FF]?|,)(?P<open>[\u2800-\u28FF]?⠷)?(?P<letter>C?L.)(?P<close>[\u2800-\u28FF]?⠾)?(?P<end>W|P|,|$)").unwrap()
    555
    2
        });
    556
    557
        // Trim braille spaces before and after braille indicators
    558
        // In order: fraction, /, cancellation, letter, baseline
    559
        // Note: fraction over is not listed due to example 42(4) which shows a space before the "/"
    560
        static REMOVE_SPACE_BEFORE_BRAILLE_INDICATORS: LazyLock<Regex> =
    561
    2
            LazyLock::new(|| Regex::new(r"(⠄⠄⠄|⠤⠤⠤⠤)[Ww]+([⠼⠸⠪])").unwrap());
    562
        static REMOVE_SPACE_AFTER_BRAILLE_INDICATORS: LazyLock<Regex> =
    563
    2
            LazyLock::new(|| Regex::new(r"([⠹⠻Llb])[Ww]+(⠄⠄⠄|⠤⠤⠤⠤)").unwrap());
    564
    565
        // Hack to convert non-numeric '.' to numeric '.'
    566
        // The problem is that the numbers are hidden inside of mover -- this might be more general than rule 99_2.
    567
    2
        static DOTS_99_A_2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"𝑁⠨mN").unwrap());
    568
    569
        // Punctuation is one or two chars. There are (currently) only 3 2-char punct chars (—‘’) -- we explicitly list them below
    570
        static REMOVE_SPACE_BEFORE_PUNCTUATION_151: LazyLock<Regex> =
    571
    2
            LazyLock::new(|| Regex::new(r"w(P.[⠤⠦⠠]?|[\u2800-\u28FF]?⠾)").unwrap());
    572
        static REMOVE_SPACE_AFTER_PUNCTUATION_151: LazyLock<Regex> =
    573
    2
            LazyLock::new(|| Regex::new(r"(P.[⠤⠦⠠]?|[\u2800-\u28FF]?⠷)w").unwrap());
    574
    575
        // Multipurpose indicator insertion
    576
        // 149 -- consecutive comparison operators have no space -- instead a multipurpose indicator is used (doesn't require a regex)
    577
    578
        // 177.2 -- add after a letter and before a digit (or decimal pt) -- digits will start with N
    579
    2
        static MULTI_177_2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([Ll].)[N𝑁]").unwrap());
    580
    581
        // keep between numeric subscript and digit ('M' added by subscript rule)
    582
    2
        static MULTI_177_3: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([N𝑁].)M([N𝑁].)").unwrap());
    583
    584
        // Add after decimal pt for non-digits except for comma and punctuation
    585
        // Note: since "." can be in the middle of a number, there is not necessarily a "N"
    586
        // Although not mentioned in 177_5, don't add an 'M' before an 'm'
    587
        static MULTI_177_5: LazyLock<Regex> =
    588
    2
            LazyLock::new(|| Regex::new(r"([N𝑁]⠨)([^⠂⠆⠒⠲⠢⠖⠶⠦⠔N𝑁,Pm])").unwrap());
    589
    590
        // Pattern for rule II.9a (add numeric indicator at start of line or after a space)
    591
        // 1. start of line
    592
        // 2. optional minus sign (⠤)
    593
        // 3. optional typeface indicator
    594
        // 4. number (N)
    595
    2
        static NUM_IND_9A: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?P<start>^|[,Ww])(?P<minus>⠤?)N").unwrap());
    596
    597
        // Needed after section mark(§), paragraph mark(¶), #, or *
    598
    2
        static NUM_IND_9C: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(⠤?)(⠠⠷|⠠⠳|⠠⠈⠷)N").unwrap());
    599
    600
        // Needed after section mark(§), paragraph mark(¶), #, or *
    601
    2
        static NUM_IND_9D: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(⠈⠠⠎|⠈⠠⠏|⠨⠼|⠈⠼)N").unwrap());
    602
    603
        // Needed after a typeface change or interior shape modifier indicator
    604
    2
        static NUM_IND_9E: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?P<face>[SB𝔹TIR]+?)N").unwrap());
    605
    2
        static NUM_IND_9E_SHAPE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?P<mod>⠸⠫)N").unwrap());
    606
    607
        // Needed after hyphen that follows a word, abbreviation, or punctuation (caution about rule 11d)
    608
        // Note -- hyphen might encode as either "P⠤" or "⠤" depending on the tag used
    609
    2
        static NUM_IND_9F: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([Ll].[Ll].|P.)(P?⠤)N").unwrap());
    610
    611
        // Enclosed list exception
    612
        // Normally we don't add numeric indicators in enclosed lists (done in get_braille_nemeth_chars).
    613
        // The green book says "at the start" of an item, don't add the numeric indicator.
    614
        // The NFB list exceptions after function abbreviations and angles, but what this really means is "after a space"
    615
    2
        static NUM_IND_ENCLOSED_LIST: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"w([⠂⠆⠒⠲⠢⠖⠶⠦⠔⠴])").unwrap());
    616
    617
        // Punctuation chars (Rule 38.6 says don't use before ",", "hyphen", "-", "…")
    618
        // Never use punctuation indicator before these (38-6)
    619
        //      "…": "⠀⠄⠄⠄"
    620
        //      "-": "⠸⠤" (hyphen and dash)
    621
        //      ",": "⠠⠀"     -- spacing already added
    622
        // Rule II.9b (add numeric indicator after punctuation [optional minus[optional .][digit]
    623
        //  because this is run after the above rule, some cases are already caught, so don't
    624
        //  match if there is already a numeric indicator
    625
    2
        static NUM_IND_9B: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?P<punct>P..?)(?P<minus>⠤?)N").unwrap());
    626
    627
        // Before 79b (punctuation)
    628
    2
        static REMOVE_LEVEL_IND_BEFORE_SPACE_COMMA_PUNCT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?:[↑↓]+[b𝑏]?|[b𝑏])([Ww,P]|$)").unwrap());
    629
    630
        // Most commas have a space after them, but not when followed by a close quote (others?)
    631
    2
        static NO_SPACE_AFTER_COMMA: LazyLock<Regex> = LazyLock::new(|| Regex::new(r",P⠴").unwrap()); // captures both single and double close quote
    632
    2
        static REMOVE_LEVEL_IND_BEFORE_BASELINE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?:[↑↓mb𝑏]+)([b𝑏])").unwrap());
    633
    634
        // Except for the four chars above, the unicode rules always include a punctuation indicator.
    635
        // The cases to remove them (that seem relevant to MathML) are:
    636
        //   Beginning of line or after a space (V 38.1)
    637
        //   After a word (38.4)
    638
        //   2nd or subsequent punctuation (includes, "-", etc) (38.7)
    639
    2
        static REMOVE_AFTER_PUNCT_IND: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(^|[Ww]|[Ll].[Ll].)P(.)").unwrap());
    640
    2
        static REPLACE_INDICATORS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([SB𝔹TIREDGVHUP𝐏CLlMmb𝑏↑↓Nn𝑁Ww,])").unwrap());
    641
    2
        static COLLAPSE_SPACES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"⠀⠀+").unwrap());
    642
    643
    //   debug!("Before:  \"{}\"", raw_braille);
    644
        // replacements might overlap at boundaries (e.g., whitespace) -- need to repeat
    645
    888
        let mut start = 0;
    646
    888
        let mut result = String::with_capacity(raw_braille.len()+ raw_braille.len()/4);  // likely upper bound
    647
    923
        while let Some(matched35) = ADD_ENGLISH_LETTER_INDICATOR.find_at(&raw_braille, start) {
    648
    35
            result.push_str(&raw_braille[start..matched.start()]);
    649
    35
            let replacement = ADD_ENGLISH_LETTER_INDICATOR.replace(
    650
    35
                    &raw_braille[matched.start()..matched.end()], "${start}${open}E${letter}${close}");
    651
    35
            // debug!("matched='{}', start/end={}/{}; replacement: {}", &raw_braille[matched.start()..matched.end()], matched.start(), matched.end(), replacement);
    652
    35
            result.push_str(&replacement);
    653
    35
            // put $end back on because needed for next match (e.g., whitespace at end and then start of next match)
    654
    35
            // but it could also match because it was at the end, in which case "-1" is wrong -- tested after loop for that
    655
    35
            start = matched.end() - 1;
    656
    35
        }
    657
    888
        if !raw_braille.is_empty() && ( start < raw_braille.len()-1 || "WP,"8.contains8(raw_braille.chars()8.nth_back8(0).unwrap8()) ) {       // see comment about $end above
    658
    882
            result.push_str(&raw_braille[start..]);
    659
    882
        }6
    660
    //   debug!("ELIs:    \"{}\"", result);
    661
    662
    888
        let result = NUM_IND_ENCLOSED_LIST.replace_all(&result, "wn${1}");
    663
    664
        // Remove blanks before and after braille indicators
    665
    888
        let result = REMOVE_SPACE_BEFORE_BRAILLE_INDICATORS.replace_all(&result, "$1$2");
    666
    888
        let result = REMOVE_SPACE_AFTER_BRAILLE_INDICATORS.replace_all(&result, "$1$2");
    667
    668
    888
        let result = REMOVE_SPACE_BEFORE_PUNCTUATION_151.replace_all(&result, "$1");
    669
    888
        let result = REMOVE_SPACE_AFTER_PUNCTUATION_151.replace_all(&result, "$1");
    670
    //   debug!("spaces:  \"{}\"", result);
    671
    672
    888
        let result = DOTS_99_A_2.replace_all(&result, "N⠨mN");
    673
    674
        // Multipurpose indicator
    675
    888
        let result = result.replace("ww", "m"); // 149
    676
    888
        let result = MULTI_177_2.replace_all(&result, "${1}m${2}");
    677
    888
        let result = MULTI_177_3.replace_all(&result, "${1}m$2");
    678
    888
        let result = MULTI_177_5.replace_all(&result, "${1}m$2");
    679
    //   debug!("MULTI:   \"{}\"", result);
    680
    681
    888
        let result = NUM_IND_9A.replace_all(&result, "${start}${minus}n");
    682
        // debug!("IND_9A:  \"{}\"", result);
    683
    888
        let result = NUM_IND_9C.replace_all(&result, "${1}${2}n");
    684
    888
        let result = NUM_IND_9D.replace_all(&result, "${1}n");
    685
    888
        let result = NUM_IND_9E.replace_all(&result, "${face}n");
    686
    888
        let result = NUM_IND_9E_SHAPE.replace_all(&result, "${mod}n");
    687
    888
        let result = NUM_IND_9F.replace_all(&result, "${1}${2}n");
    688
    689
    //   debug!("IND_9F:  \"{}\"", result);
    690
    691
        // 9b: insert after punctuation (optional minus sign)
    692
        // common punctuation adds a space, so 9a handled it. Here we deal with other "punctuation"
    693
        // FIX other punctuation and reference symbols (9d)
    694
    888
        let result = NUM_IND_9B.replace_all(&result, "$punct${minus}n");
    695
    //   debug!("A PUNCT: \"{}\"", &result);
    696
    697
        // strip level indicators
    698
        // check first to remove level indicators before baseline, then potentially remove the baseline
    699
    888
        let mut result = REMOVE_LEVEL_IND_BEFORE_BASELINE.replace_all(&result, "$1");
    700
    //   debug!("Punct  : \"{}\"", &result);
    701
        // checks for punctuation char, so needs to before punctuation is stripped.
    702
        // if '𝑏' is removed, then the highlight needs to be shifted to the left in some cases
    703
    888
        let result = remove_baseline_before_space_or_punctuation(&mut result);
    704
    //   debug!("Removed: \"{}\"", &result);
    705
    706
    888
        let result = NO_SPACE_AFTER_COMMA.replace_all(&result, "⠠P⠴");
    707
    708
    888
        let result = REMOVE_AFTER_PUNCT_IND.replace_all(&result, "$1$2");
    709
    //   debug!("Punct38: \"{}\"", &result);
    710
    711
        // these typeforms need to get pulled from user-prefs as they are transcriber-defined
    712
    888
        let sans_serif = pref_manager.pref_to_string("Nemeth_SansSerif");
    713
    888
        let bold = pref_manager.pref_to_string("Nemeth_Bold");
    714
    888
        let double_struck = pref_manager.pref_to_string("Nemeth_DoubleStruck");
    715
    888
        let script = pref_manager.pref_to_string("Nemeth_Script");
    716
    888
        let italic = pref_manager.pref_to_string("Nemeth_Italic");
    717
    718
    7.57k
        let result888 = REPLACE_INDICATORS888.replace_all888(&result888, |cap: &Captures| {
    719
    7.57k
            let matched_char = &cap[0];
    720
    7.57k
            match matched_char {
    721
    7.57k
                "S" => &sans_serif2,
    722
    7.57k
                "B" => &bold47,
    723
    7.52k
                "𝔹" => &double_struck28,
    724
    7.49k
                "T" => &script6,
    725
    7.49k
                "I" => &italic2,
    726
    7.48k
                _ => match NEMETH_INDICATOR_REPLACEMENTS.get(&cap[0]) {
    727
    0
                    None => {error!("REPLACE_INDICATORS and NEMETH_INDICATOR_REPLACEMENTS are not in sync"); ""},
    728
    7.48k
                    Some(&ch) => ch,
    729
                }
    730
            }
    731
    7.57k
        });
    732
    733
        // Remove unicode blanks at start and end -- do this after the substitutions because ',' introduces spaces
    734
    888
        let result = result.trim_start_matches('⠀').trim_end_matches('⠀');
    735
    888
        let result = COLLAPSE_SPACES.replace_all(result, "⠀");
    736
    737
    888
        return result.to_string();
    738
    739
    888
        fn remove_baseline_before_space_or_punctuation<'a>(braille: &'a mut Cow<'a, str>) -> Cow<'a, str> {
    740
            // If the baseline highlight is at the end of the string and it is going to be deleted by the regex,
    741
            //   then we need to shift the highlight to the left if what is to it's left is not whitespace (which should never be a highlight end)
    742
            // This only happens when BrailleNavHighlight == "EndPoints".
    743
    888
            let highlight_style = PreferenceManager::get().borrow().pref_to_string("BrailleNavHighlight");
    744
    888
            if highlight_style == "EndPoints" &&
    745
    132
                let Some(last_highlighted129) = braille.rfind(is_highlighted) &&
    746
    129
                braille[last_highlighted..].starts_with('𝑏') {
    747
    7
                        let i_after_baseline = last_highlighted + '𝑏'.len_utf8();
    748
    7
                        if i_after_baseline == braille.len() || braille[i_after_baseline..]5.starts_with5(['W', 'w', ',', 'P']5) {
    749
                            // shift the highlight to the left after doing just the replacement (if any) that the regex below does
    750
                            // the shift runs until a non blank braille char is found
    751
    2
                            let mut bytes_deleted = 0;
    752
    2
                            let mut char_to_highlight = "".to_string();   // illegal value
    753
    2
                            for ch in braille[..last_highlighted].chars().rev() {
    754
    2
                                bytes_deleted += ch.len_utf8();
    755
    2
                                if (0x2801..0x28FF).contains(&(ch as u32)) {
    756
    2
                                    char_to_highlight = highlight(ch).to_string();
    757
    2
                                    break;
    758
    0
                                }
    759
                            }
    760
    2
                            braille.to_mut().replace_range(last_highlighted-bytes_deleted..last_highlighted+'𝑏'.len_utf8(),
    761
    2
                                                            &char_to_highlight);
    762
    5
                        }
    763
    881
                    }
    764
    888
            return REMOVE_LEVEL_IND_BEFORE_SPACE_COMMA_PUNCT.replace_all(braille, "$1");
    765
    766
    888
        }
    767
    888
    }
    768
    769
    // Typeface: S: sans-serif, B: bold, T: script/blackboard, I: italic, R: Roman
    770
    // Language: E: English, D: German, G: Greek, V: Greek variants, H: Hebrew, U: Russian
    771
    // Indicators: C: capital, N: number, P: punctuation, M: multipurpose
    772
    // Others:
    773
    //      W -- whitespace that should be kept (e.g, in a numeral)
    774
    //      𝑁 -- hack for special case of a lone decimal pt -- not considered a number but follows rules mostly
    775
    // Note: some "positive" patterns find cases to keep the char and transform them to the lower case version
    776
    static UEB_INDICATOR_REPLACEMENTS: phf::Map<&str, &str> = phf_map! {
    777
        "S" => "XXX",    // sans-serif -- from prefs
    778
        "B" => "⠘",     // bold
    779
        "𝔹" => "XXX",     // blackboard -- from prefs
    780
        "T" => "⠈",     // script
    781
        "I" => "⠨",     // italic
    782
        "R" => "",      // roman
    783
        // "E" => "⠰",     // English
    784
        "1" => "⠰",      // Grade 1 symbol
    785
        "𝟙" => "⠰⠰",     // Grade 1 word
    786
        "L" => "",       // Letter left in to assist in locating letters
    787
        "D" => "XXX",    // German (Deutsche) -- from prefs
    788
        "G" => "⠨",      // Greek
    789
        "V" => "⠨⠈",     // Greek Variants
    790
        // "H" => "⠠⠠",  // Hebrew
    791
        // "U" => "⠈⠈",  // Russian
    792
        "C" => "⠠",      // capital
    793
        "𝐶" => "⠠",      // capital that never should get word indicator (from chemical element)
    794
        "N" => "⠼",     // number indicator
    795
        "t" => "⠱",     // shape terminator
    796
        "W" => "⠀",     // whitespace
    797
        "𝐖"=> "⠀",     // whitespace (hard break -- basically, it separates exprs)
    798
        "s" => "⠆",     // typeface single char indicator
    799
        "w" => "⠂",     // typeface word indicator
    800
        "e" => "⠄",     // typeface & capital terminator
    801
        "o" => "",       // flag that what follows is an open indicator (used for standing alone rule)
    802
        "c" => "",       // flag that what follows is an close indicator (used for standing alone rule)
    803
        "b" => "",       // flag that what follows is an open or close indicator (used for standing alone rule)
    804
        "," => "⠂",     // comma
    805
        "." => "⠲",     // period
    806
        "-" => "-",     // hyphen
    807
        "—" => "⠠⠤",   // normal dash (2014) -- assume all normal dashes are unified here [RUEB appendix 3]
    808
        "―" => "⠐⠠⠤",  // long dash (2015) -- assume all long dashes are unified here [RUEB appendix 3]
    809
        "#" => "",      // signals end of script
    810
        // '(', '{', '[', '"', '\'', '“', '‘', '«',    // opening chars
    811
        // ')', '}', ']', '\"', '\'', '”', '’', '»',           // closing chars
    812
        // ',', ';', ':', '.', '…', '!', '?'                    // punctuation
    813
    814
    };
    815
    816
    // static LETTERS: phf::Set<char> = phf_set! {
    817
    //     '⠁', '⠃', '⠉', '⠙', '⠑', '⠋', '⠛', '⠓', '⠊', '⠚', '⠅', '⠇', '⠍',
    818
    //     '⠝', '⠕', '⠏', '⠟', '⠗', '⠎', '⠞', '⠥', '⠧', '⠺', '⠭', '⠽', '⠵',
    819
    // };
    820
    821
    2.39k
    fn is_letter_number(ch: char) -> bool {
    822
    2.39k
        matches!986(ch, '⠁' | '⠃' | '⠉' | '⠙' | '⠑' | '⠋' | '⠛' | '⠓' | '⠊' | '⠚')
    823
    2.39k
    }
    824
    825
    static SHORT_FORMS: phf::Set<&str> = phf_set! {
    826
        "L⠁L⠃", "L⠁L⠃L⠧", "L⠁L⠉", "L⠁L⠉L⠗", "L⠁L⠋",
    827
        "L⠁L⠋L⠝", "L⠁L⠋L⠺", "L⠁L⠛", "L⠁L⠛L⠌", "L⠁L⠇",
    828
         "L⠁L⠇L⠍", "L⠁L⠇L⠗", "L⠁L⠇L⠞", "L⠁L⠇L⠹", "L⠁L⠇L⠺",
    829
         "L⠃L⠇", "L⠃L⠗L⠇", "L⠉L⠙", "L⠙L⠉L⠇", "L⠙L⠉L⠇L⠛",
    830
         "L⠙L⠉L⠧", "L⠙L⠉L⠧L⠛", "L⠑L⠊", "L⠋L⠗", "L⠋L⠌", "L⠛L⠙",
    831
         "L⠛L⠗L⠞", "L⠓L⠍", "L⠓L⠍L⠋", "L⠓L⠻L⠋", "L⠊L⠍L⠍", "L⠇L⠇", "L⠇L⠗",
    832
         "L⠍L⠽L⠋", "L⠍L⠡", "L⠍L⠌", "L⠝L⠑L⠉", "L⠝L⠑L⠊", "L⠏L⠙",
    833
         "L⠏L⠻L⠉L⠧", "L⠏L⠻L⠉L⠧L⠛", "L⠏L⠻L⠓", "L⠟L⠅", "L⠗L⠉L⠧",
    834
         "L⠗L⠉L⠧L⠛", "L⠗L⠚L⠉", "L⠗L⠚L⠉L⠛", "L⠎L⠙", "L⠎L⠡", "L⠞L⠙",
    835
         "L⠞L⠛L⠗", "L⠞L⠍", "L⠞L⠝", "L⠭L⠋", "L⠭L⠎", "L⠽L⠗", "L⠽L⠗L⠋",
    836
         "L⠽L⠗L⠧L⠎", "L⠮L⠍L⠧L⠎", "L⠡L⠝", "L⠩L⠙", "L⠹L⠽L⠋", "L⠳L⠗L⠧L⠎",
    837
         "L⠺L⠙", "L⠆L⠉", "L⠆L⠋", "L⠆L⠓", "L⠆L⠇", "L⠆L⠝", "L⠆L⠎", "L⠆L⠞",
    838
         "L⠆L⠽", "L⠒L⠉L⠧", "L⠒L⠉L⠧L⠛", "L⠐L⠕L⠋"
    839
    };
    840
    841
    1.75k
    fn is_letter_prefix(ch: char) -> bool {
    842
    1.75k
        matches!1.61k(ch, 'B' | 'I' | '𝔹' | 'S' | 'T' | 'D' | 'C' | '𝐶' | '𝑐')
    843
    1.75k
    }
    844
    845
    // Trim braille spaces before and after braille indicators
    846
    // In order: fraction, /, cancellation, letter, baseline
    847
    // Note: fraction over is not listed due to example 42(4) which shows a space before the "/"
    848
    // static ref REMOVE_SPACE_BEFORE_BRAILLE_INDICATORS: Regex =
    849
    //     Regex::new(r"(⠄⠄⠄|⠤⠤⠤)W+([⠼⠸⠪])").unwrap();
    850
    2
    static REPLACE_INDICATORS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([1𝟙SB𝔹TIREDGVHP𝐶𝑐CLMNW𝐖swe,.-—―#ocb])").unwrap());
    851
    2
    static COLLAPSE_SPACES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"⠀⠀+").unwrap());
    852
    853
    35
    fn is_short_form(chars: &[char]) -> bool {
    854
    204
        let chars_as_string35 = chars35.iter35().map35(|ch| ch.to_string()).collect35::<String>();
    855
    35
        return SHORT_FORMS.contains(&chars_as_string);
    856
    35
    }
    857
    858
    366
    fn ueb_cleanup(pref_manager: Ref<PreferenceManager>, raw_braille: String) -> String {
    859
        // debug!("ueb_cleanup: start={}", raw_braille);
    860
    366
        let result = typeface_to_word_mode(&raw_braille);
    861
    366
        let result = capitals_to_word_mode(&result);
    862
    863
    366
        let use_only_grade1 = pref_manager.pref_to_string("UEB_START_MODE").as_str() == "Grade1";
    864
    865
        // '𝐖' is a hard break -- basically, it separates exprs
    866
    366
        let mut result = result.split('𝐖')
    867
    370
                            .map366(|str| pick_start_mode(str, use_only_grade1) + "W")
    868
    366
                            .collect::<String>();
    869
    366
        result.pop();   // we added a 'W' at the end that needs to be removed.
    870
    871
    366
        let result = result.replace("tW", "W");
    872
    873
        // these typeforms need to get pulled from user-prefs as they are transcriber-defined
    874
    366
        let double_struck = pref_manager.pref_to_string("UEB_DoubleStruck");
    875
    366
        let sans_serif = pref_manager.pref_to_string("UEB_SansSerif");
    876
    366
        let fraktur = pref_manager.pref_to_string("UEB_Fraktur");
    877
    366
        let greek_variant = pref_manager.pref_to_string("UEB_GreekVariant");
    878
    879
    3.77k
        let result366 = REPLACE_INDICATORS366.replace_all366(&result366, |cap: &Captures| {
    880
    3.77k
            let matched_char = &cap[0];
    881
    3.77k
            match matched_char {
    882
    3.77k
                "𝔹" => &double_struck0,
    883
    3.77k
                "S" => &sans_serif0,
    884
    3.77k
                "D" => &fraktur2,
    885
    3.77k
                "V" => &greek_variant0,
    886
    3.77k
                _ => match UEB_INDICATOR_REPLACEMENTS.get(matched_char) {
    887
    0
                    None => {error!("REPLACE_INDICATORS and UEB_INDICATOR_REPLACEMENTS are not in sync: missing '{matched_char}'"); ""},
    888
    3.77k
                    Some(&ch) => ch,
    889
                },
    890
            }
    891
    3.77k
        });
    892
    893
        // Remove unicode blanks at start and end -- do this after the substitutions because ',' introduces spaces
    894
        // let result = result.trim_start_matches('⠀').trim_end_matches('⠀');
    895
    366
        let result = COLLAPSE_SPACES.replace_all(&result, "⠀");
    896
    897
    366
        return result.to_string();
    898
    899
    370
        fn pick_start_mode(raw_braille: &str, use_only_grade1: bool) -> String {
    900
            // Need to decide what the start mode should be
    901
            // From http://www.brailleauthority.org/ueb/ueb_math_guidance/final_for_posting_ueb_math_guidance_may_2019_102419.pdf
    902
            //   Unless a math expression can be correctly represented with only a grade 1 symbol indicator in the first three cells
    903
            //   or before a single letter standing alone anywhere in the expression,
    904
            //   begin the expression with a grade 1 word indicator (or a passage indicator if the expression includes spaces)
    905
            // Apparently "only a grade 1 symbol..." means at most one grade 1 symbol based on some examples (GTM 6.4, example 4)
    906
            // debug!("before determining mode:  '{}'", raw_braille);
    907
    908
            // a bit ugly because we need to store the string if we have cap passage mode
    909
    370
            let raw_braille_string = if is_cap_passage_mode_good(raw_braille) {convert_to_cap_passage_mode3(raw_braille3)} else {String::default367()};
    910
    370
            let raw_braille = if raw_braille_string.is_empty() {raw_braille367} else {&raw_braille_string3};
    911
    370
            if use_only_grade1 {
    912
    1
                return remove_unneeded_mode_changes(raw_braille, UEB_Mode::Grade1, UEB_Duration::Passage);
    913
    369
            }
    914
    369
            let grade2 = remove_unneeded_mode_changes(raw_braille, UEB_Mode::Grade2, UEB_Duration::Symbol);
    915
    369
            debug!("Symbol mode:  '{}'", grade2);
    916
    917
    369
            if is_grade2_string_ok(&grade2) {
    918
    143
                return grade2;
    919
            } else {
    920
                // BANA says use g1 word mode if spaces are present, but that's not what their examples do
    921
                // A conversation with Ms. DeAndrea from BANA said that they mean use passage mode if ≥3 "segments" (≥2 blanks)
    922
                // The G1 Word mode might not be at the start (iceb.rs:omission_3_6_7)
    923
    226
                let grade1_word = try_grade1_word_mode(raw_braille);
    924
    226
                debug!("Word mode:    '{}'", grade1_word);
    925
    226
                if !grade1_word.is_empty() {
    926
    36
                    return grade1_word;
    927
                } else {
    928
    190
                    let grade1_passage = remove_unneeded_mode_changes(raw_braille, UEB_Mode::Grade1, UEB_Duration::Passage);
    929
    190
                    return "⠰⠰⠰".to_string() + &grade1_passage + "⠰⠄";
    930
                }
    931
            }
    932
    933
            /// Return true if at least five (= # of cap passage indicators) cap indicators and no lower case letters
    934
    370
            fn is_cap_passage_mode_good(braille: &str) -> bool {
    935
    370
                let mut n_caps = 0;
    936
    370
                let mut is_cap_mode = false;
    937
    370
                let mut cap_mode = UEB_Duration::Symbol;    // real value set when is_cap_mode is set to true
    938
    370
                let mut chars = braille.chars();
    939
    940
                // look CL or CCL for caps (CC runs until we get whitespace)
    941
                // if we find an L not in caps mode, we return false
    942
                // Note: caps can be C𝐶, whitespace can be W𝐖
    943
    2.03k
                while let Some(ch1.96k) = chars.next() {
    944
    1.96k
                    if ch == 'L' {
    945
    401
                        if !is_cap_mode {
    946
    288
                            return false;
    947
    113
                        }
    948
    113
                        chars.next();       // skip letter
    949
    113
                        if cap_mode == UEB_Duration::Symbol {
    950
    79
                            is_cap_mode = false;
    951
    79
                        }34
    952
    1.55k
                    } else if ch == 'C' || ch == '𝐶'1.49k {
    953
    107
                        if is_cap_mode {
    954
    16
                            if cap_mode == UEB_Duration::Symbol {
    955
    12
                                cap_mode = UEB_Duration::Word;
    956
    12
                            }4
    957
    91
                        } else {
    958
    91
                            is_cap_mode = true;
    959
    91
                            cap_mode = UEB_Duration::Symbol;
    960
    91
                        }
    961
    107
                        n_caps += 1;
    962
    1.45k
                    } else if ch == 'W' || ch == '𝐖'1.33k {
    963
    119
                        if is_cap_mode {
    964
    2
                            assert!(cap_mode == UEB_Duration::Word);
    965
    117
                        }
    966
    119
                        is_cap_mode = false;
    967
    1.33k
                    } else if ch == '1' && is_cap_mode117 {
    968
    3
                        break;
    969
    1.33k
                    }
    970
                }
    971
    82
                return n_caps > 4;
    972
    370
            }
    973
    974
    3
            fn convert_to_cap_passage_mode(braille: &str) -> String {
    975
    3
                return "⠠⠠⠠".to_string() + &braille.replace(['C', '𝐶'], "") + "⠠⠄";
    976
    3
            }
    977
    978
            /// Return true if the BANA or ICEB guidelines say it is ok to start with grade 2
    979
    369
            fn is_grade2_string_ok(grade2_braille: &str) -> bool {
    980
                // BANA says use grade 2 if there is not more than one grade one symbol or single letter standing alone.
    981
                // The exact quote from their guidance:
    982
                //    Unless a math expression can be correctly represented with only a grade 1 symbol indicator in the first three cells
    983
                //    or before a single letter standing alone anywhere in the expression,
    984
                //    begin the expression with a grade 1 word indicator
    985
                // Note: I modified this slightly to exclude the cap indicator in the count. That allows three more ICEB rule to pass and seems
    986
                //    like it is a reasonable thing to do.
    987
                // Another modification is allow a single G1 indicator to occur after whitespace later on
    988
                //    because ICEB examples show it and it seems better than going to passage mode if it is the only G1 indicator
    989
    990
                // Because of the 'L's which go away, we have to put a little more work into finding the first three chars
    991
    369
                let chars = grade2_braille.chars().collect::<Vec<char>>();
    992
    369
                let mut n_real_chars = 0;  // actually number of chars
    993
    369
                let mut found_g1 = false;
    994
    369
                let mut i = 0;
    995
    1.75k
                while i < chars.len() {
    996
    1.75k
                    let ch = chars[i];
    997
    1.75k
                    if ch == '1' && !275is_forced_grade1275(&chars, i) {
    998
    269
                        if found_g1 {
    999
    19
                            return false;
    1000
    250
                        }
    1001
    250
                        found_g1 = true;
    1002
    1.48k
                    } else if !"𝐶CLobc".contains(ch) {
    1003
    1.07k
                        if n_real_chars == 2 {
    1004
    347
                            i += 1;
    1005
    347
                            break;              // this is the third real char
    1006
    730
                        };
    1007
    730
                        n_real_chars += 1;
    1008
    407
                    }
    1009
    1.38k
                    i += 1
    1010
                }
    1011
    1012
                // if we find *another* g1 that isn't forced and isn't standing alone, we are done
    1013
                // I've added a 'follows whitespace' clause for test iceb.rs:omission_3_6_2 to the standing alone rule
    1014
                // we only allow one standing alone example -- not sure if BANA guidance has this limit, but GTM 11_5_5_3 seems better with it
    1015
                // Same for GTM 1_7_3_1 (passage mode is mentioned also)
    1016
    350
                let mut is_standing_alone_already_encountered = false;
    1017
    350
                let mut is_after_whitespace = false;
    1018
    2.43k
                while i < chars.len() {
    1019
    2.29k
                    let ch = chars[i];
    1020
    2.29k
                    if ch == 'W' {
    1021
    355
                        is_after_whitespace = true;
    1022
    1.93k
                    } else if ch == '1' && !239is_forced_grade1239(&chars, i) {
    1023
    235
                        if is_standing_alone_already_encountered ||
    1024
    226
                           ((found_g1 || !is_after_whitespace33) && !203is_single_letter_on_right203(&chars, i)) {
    1025
    207
                            return false;
    1026
    28
                        }
    1027
    28
                        found_g1 = true;
    1028
    28
                        is_standing_alone_already_encountered = true;
    1029
    1.70k
                    }
    1030
    2.08k
                    i += 1;
    1031
                }
    1032
    143
                return true;
    1033
    369
            }
    1034
    1035
            /// Return true if the sequence of chars forces a '1' at the `i`th position
    1036
            /// Note: `chars[i]` should be '1'
    1037
    930
            fn is_forced_grade1(chars: &[char], i: usize) -> bool {
    1038
                // A '1' is forced if 'a-j' follows a digit
    1039
    930
                assert_eq!(chars[i], '1', "'is_forced_grade1' didn't start with '1'");
    1040
                // check that a-j follows the '1' -- we have '1Lx' where 'x' is the letter to check
    1041
    930
                if i+2 < chars.len() && is_letter_number927(unhighlight927(chars[i+2]927)) {
    1042
                    // check for a number before the '1'
    1043
                    // this will be 'N' followed by LETTER_NUMBERS or the number ".", ",", or " "
    1044
    25
                    for j in (0..i12).rev12() {
    1045
    25
                        let ch = chars[j];
    1046
    25
                        if !(is_letter_number(unhighlight(ch)) || ".,W𝐖"14.contains14(ch14)) {
    1047
    12
                            return ch == 'N'
    1048
    13
                        }
    1049
                    }
    1050
    918
                }
    1051
    918
                return false;
    1052
    930
            }
    1053
    1054
    203
            fn is_single_letter_on_right(chars: &[char], i: usize) -> bool {
    1055
    205
                fn is_skip_char(ch: char) -> bool {
    1056
    205
                    matches!204(ch, 'B' | 'I' | '𝔹' | 'S' | 'T' | 'D' | 'C' | '𝐶' | 's' | 'w')
    1057
    205
                }
    1058
    1059
                // find the first char (if any)
    1060
    203
                let mut count = 0;      // how many letters
    1061
    203
                let mut i = i+1;
    1062
    209
                while i < chars.len() {
    1063
    205
                    let ch = chars[i];
    1064
    205
                    if !is_skip_char(ch) {
    1065
    204
                        if ch == 'L' {
    1066
    5
                            if count == 1 {
    1067
    0
                                return false;   // found a second letter in the sequence
    1068
    5
                            }
    1069
    5
                            count += 1;
    1070
                        } else {
    1071
    199
                            return count==1;
    1072
                        }
    1073
    5
                        i += 2;   // eat 'L' and actual letter
    1074
    1
                    } else {
    1075
    1
                        i += 1;
    1076
    1
                    }
    1077
                }
    1078
    4
                return true;
    1079
    203
            }
    1080
    1081
    226
            fn try_grade1_word_mode(raw_braille: &str) -> String {
    1082
                // this isn't quite right, but pretty close -- try splitting at 'W' (words)
    1083
                // only one of the parts can be in word mode and none of the others can have '1' unless forced
    1084
    226
                let mut g1_words = Vec::default();
    1085
    226
                let mut found_word_mode = false;
    1086
    622
                for raw_word in raw_braille226.split226('W') {
    1087
    622
                    let word = remove_unneeded_mode_changes(raw_word, UEB_Mode::Grade2, UEB_Duration::Symbol);
    1088
                    // debug!("try_grade1_word_mode: word='{}'", word);
    1089
    622
                    let word_chars = word.chars().collect::<Vec<char>>();
    1090
    622
                    let needs_word_mode = word_chars.iter().enumerate()
    1091
    1.12k
                        .any622(|(i, &ch) | ch == '1' && !416is_forced_grade1416(&word_chars, i));
    1092
    622
                    if needs_word_mode {
    1093
    416
                        if found_word_mode {
    1094
    190
                            return "".to_string();
    1095
    226
                        }
    1096
    226
                        found_word_mode = true;
    1097
    226
                        g1_words.push("⠰⠰".to_string() + &remove_unneeded_mode_changes(raw_word, UEB_Mode::Grade1, UEB_Duration::Word)
    1098
                        );
    1099
    206
                    } else {
    1100
    206
                        g1_words.push(word);
    1101
    206
                    }
    1102
                }
    1103
    36
                return if found_word_mode {g1_words.join("W")} else {""0.to_string0()};
    1104
    226
            }
    1105
    370
        }
    1106
    366
    }
    1107
    1108
    478
    fn typeface_to_word_mode(braille: &str) -> String {
    1109
    2
        static HAS_TYPEFACE: LazyLock<Regex> = LazyLock::new(|| Regex::new("[BI𝔹STD]").unwrap());
    1110
        // debug!("before typeface fix:  '{}'", braille);
    1111
    1112
    478
        let mut result = "".to_string();
    1113
    478
        let chars = braille.chars().collect::<Vec<char>>();
    1114
    478
        let mut word_mode = Vec::with_capacity(5);
    1115
    478
        let mut word_mode_end = Vec::with_capacity(5);
    1116
    478
        let mut i = 0;
    1117
    11.5k
        while i < chars.len() {
    1118
    11.0k
            let ch = chars[i];
    1119
    11.0k
            if HAS_TYPEFACE.is_match(ch.to_string().as_str()) {
    1120
    8
                let i_next_char_target = find_next_char(&chars[i+1..], ch);
    1121
    8
                if word_mode.contains(&ch) {
    1122
    3
                    if i_next_char_target.is_none() {
    1123
    2
                        word_mode.retain(|&item| item!=ch);  // drop the char since word mode is done
    1124
    2
                        word_mode_end.push(ch);   // add the char to signal to add end sequence
    1125
    1
                    }
    1126
                } else {
    1127
    5
                    result.push(ch);
    1128
    5
                    if i_next_char_target.is_some() {
    1129
    2
                        result.push('w');     // typeface word indicator
    1130
    2
                        word_mode.push(ch);      // starting word mode for this char
    1131
    3
                    } else {
    1132
    3
                        result.push('s');     // typeface single char indicator
    1133
    3
                    }
    1134
                }
    1135
    8
                i += 1; // eat "B", etc
    1136
    11.0k
            } else if ch == 'L' || ch == 'N'8.72k {
    1137
    3.70k
                result.push(chars[i]);
    1138
    3.70k
                result.push(chars[i+1]);
    1139
    3.70k
                if !word_mode_end.is_empty() && i+22 < chars.len() && !(chars[i+2] == 'W'1|| chars[i+2] == '𝐖'1) {
    1140
                    // add terminator unless word sequence is terminated by end of string or whitespace
    1141
    1
                    for &ch in &word_mode_end {
    1142
    1
                        result.push(ch);
    1143
    1
                        result.push('e');
    1144
    1
                    };
    1145
    1
                    word_mode_end.clear();
    1146
    3.70k
                }
    1147
    3.70k
                i += 2; // eat Ll/Nd
    1148
    7.30k
            } else {
    1149
    7.30k
                result.push(ch);
    1150
    7.30k
                i += 1;
    1151
    7.30k
            }
    1152
        }
    1153
    478
        return result;
    1154
    1155
    478
    }
    1156
    1157
    478
    fn capitals_to_word_mode(braille: &str) -> String {
    1158
        use std::iter::FromIterator;
    1159
        // debug!("before capitals fix:  '{}'", braille);
    1160
    1161
    478
        let mut result = "".to_string();
    1162
    478
        let chars = braille.chars().collect::<Vec<char>>();
    1163
    478
        let mut is_word_mode = false;
    1164
    478
        let mut i = 0;
    1165
        // look for a sequence of CLxCLy... and create CCLxLy...
    1166
    12.6k
        while i < chars.len() {
    1167
    12.1k
            let ch = chars[i];
    1168
    12.1k
            if ch == 'C' {
    1169
                // '𝑐' should only occur after a 'C', so we don't have top-level check for it
    1170
    256
                let mut next_non_cap = i+1;
    1171
    257
                while let Some(i_next1) = find_next_char(&chars[next_non_cap..], '𝑐') {
    1172
    1
                    next_non_cap += i_next + 1; // C/𝑐, L, letter
    1173
    1
                }
    1174
    256
                if find_next_char(&chars[next_non_cap..], 'C').is_some() { // next letter sequence "C..."
    1175
    63
                    if is_next_char_start_of_section_12_modifier(&chars[next_non_cap+1..]) {
    1176
                        // to me this is tricky -- section 12 modifiers apply to the previous item
    1177
                        // the last clause of the "item" def is the previous indivisible symbol" which ICEB 2.1 say is:
    1178
                        //   braille sign: one or more consecutive braille characters comprising a unit,
    1179
                        //     consisting of a root on its own or a root preceded by one or more
    1180
                        //     prefixes (also referred to as braille symbol)
    1181
                        // this means the capital indicator needs to be stated and can't be part of a word or passage
    1182
    1
                        is_word_mode = false;
    1183
    1
                        result.push_str(String::from_iter(&chars[i..next_non_cap]).as_str());
    1184
    1
                        i = next_non_cap;
    1185
    1
                        continue;
    1186
    62
                    }
    1187
    62
                    if is_word_mode {
    1188
    12
                        i += 1;     // skip the 'C'
    1189
    50
                    } else {
    1190
    50
                        // start word mode -- need an extra 'C'
    1191
    50
                        result.push('C');
    1192
    50
                        is_word_mode = true;
    1193
    50
                    }
    1194
    193
                } else if is_word_mode {
    1195
    50
                    i += 1;         // skip the 'C'
    1196
    143
                }
    1197
    255
                if chars[next_non_cap] == 'G' {
    1198
    8
                    // Greek letters are a bit exceptional in that the pattern is "CGLx" -- bump 'i'
    1199
    8
                    next_non_cap += 1;
    1200
    247
                }
    1201
    255
                if chars[next_non_cap] != 'L' {
    1202
    0
                    error!("capitals_to_word_mode: internal error: didn't find L after C in '{}'.",
    1203
    0
                           chars[i..next_non_cap+2].iter().collect::<String>().as_str());
    1204
    255
                }
    1205
    255
                let i_braille_char = next_non_cap + 2;
    1206
    255
                result.push_str(String::from_iter(&chars[i..i_braille_char]).as_str());
    1207
    255
                i = i_braille_char;
    1208
    11.9k
            } else if ch == 'L' {       // must be lowercase -- uppercase consumed above
    1209
                // assert!(LETTERS.contains(&unhighlight(chars[i+1]))); not true for other alphabets
    1210
    2.03k
                if is_word_mode {
    1211
    2
                    result.push('e');       // terminate Word mode (letter after caps)
    1212
    2
                    is_word_mode = false;
    1213
    2.03k
                }
    1214
    2.03k
                result.push('L');
    1215
    2.03k
                result.push(chars[i+1]);
    1216
    2.03k
                i += 2; // eat L, letter
    1217
    9.88k
            } else {
    1218
    9.88k
                is_word_mode = false;   // non-letters terminate cap word mode
    1219
    9.88k
                result.push(ch);
    1220
    9.88k
                i += 1;
    1221
    9.88k
            }
    1222
        }
    1223
    478
        return result;
    1224
    1225
    63
        fn is_next_char_start_of_section_12_modifier(chars: &[char]) -> bool {
    1226
            // first find the L and eat the char so that we are at the potential start of where the target lies
    1227
    63
            let chars_len = chars.len();
    1228
    63
            let mut i_cap = 0;
    1229
    126
            while chars[i_cap] != 'C' {     // we know 'C' is in the string, so no need to check for exceeding chars_len
    1230
    63
                i_cap += 1;
    1231
    63
            }
    1232
    73
            for i_end in i_cap+1..chars_len63 {
    1233
    73
                if chars[i_end] == 'L' {
    1234
                    // skip the next char to get to the real start, and then look for the modifier string or next L/N
    1235
                    // debug!("   after L '{}'", chars[i_end+2..].iter().collect::<String>());
    1236
    65
                    for i in i_end+2..chars_len63 {
    1237
    65
                        let ch = chars[i];
    1238
    65
                        if ch == '1' {
    1239
                            // Fix: there's probably a much better way to check if we have a match against one of "⠱", "⠘⠱", "⠘⠲", "⠸⠱", "⠐⠱ ", "⠨⠸⠱"
    1240
    5
                            if chars[i+1] == '⠱' {
    1241
    0
                                return true;
    1242
    5
                            } else if i+2 < chars_len {
    1243
    5
                                let mut str = chars[i+1].to_string();
    1244
    5
                                str.push(chars[i+2]);
    1245
    5
                                if str == "⠘⠱" || str == "⠘⠲" || str == "⠸⠱" || str == "⠐⠱" {
    1246
    1
                                    return true;
    1247
    4
                                } else if i+3 < chars_len {
    1248
    4
                                    str.push(chars[i+3]);
    1249
    4
                                    return str == "⠨⠸⠱";
    1250
    0
                                }
    1251
    0
                                return false;
    1252
    0
                            }
    1253
    60
                        }
    1254
    60
                        if ch == 'L' || ch == 'N'46 || !is_letter_prefix(ch)46 {
    1255
    48
                            return false;
    1256
    12
                        }
    1257
                    }
    1258
    10
                }
    1259
            }
    1260
    10
            return false;
    1261
    63
        }
    1262
    478
    }
    1263
    1264
    521
    fn find_next_char(chars: &[char], target: char) -> Option<usize> {
    1265
        // first find the L or N and eat the char so that we are at the potential start of where the target lies
    1266
        // debug!("Looking for '{}' in '{}'", target, chars.iter().collect::<String>());
    1267
    610
        for i_end in 0..chars.len()521 {
    1268
    610
            if chars[i_end] == 'L' || chars[i_end] == 'N'95 {
    1269
                // skip the next char to get to the real start, and then look for the target
    1270
                // stop when L/N signals past potential target or we hit some non L/N char (actual braille)
    1271
                // debug!("   after L/N '{}'", chars[i_end+2..].iter().collect::<String>());
    1272
    521
                for (i515, &ch515) in chars.iter().enumerate().skip(i_end+2) {
    1273
    515
                    if ch == 'L' || ch == 'N'368 || !is_letter_prefix(ch)366 {
    1274
    383
                        return None;
    1275
    132
                    } else if ch == target {
    1276
                        // debug!("   found target");
    1277
    67
                        return Some(i);
    1278
    65
                    }
    1279
                }
    1280
    89
            }
    1281
        }
    1282
    71
        return None;
    1283
    521
    }
    1284
    1285
    #[allow(non_camel_case_types)]
    1286
    #[derive(Debug, PartialEq, Copy, Clone)]
    1287
    enum UEB_Mode {
    1288
        Numeric,        // also includes Grade1
    1289
        Grade1,
    1290
        Grade2,
    1291
    }
    1292
    1293
    #[allow(non_camel_case_types)]
    1294
    #[derive(Debug, PartialEq, Copy, Clone)]
    1295
    enum UEB_Duration {
    1296
        // Standing alone: A braille symbol that is standing alone may have a contracted (grade 2) meaning.
    1297
        // A letter or unbroken sequence of letters is “standing alone” if the symbols before and after the letter or
    1298
        //   sequence are spaces, hyphens, dashes or any combination thereof, including some common punctuation.
    1299
        // Item: An “item” is defined as the next symbol or one of seven groupings listed in Rules of Unified English Braille, §11.4.1.
    1300
        Symbol,
    1301
    1302
        // The grade 1 word indicator sets grade 1 mode for the next word or symbol sequence.
    1303
        // A symbol sequence in UEB is defined as an unbroken string of braille signs,
    1304
        //   whether alphabetic or non-alphabetic, preceded and followed by a space.
    1305
        Word,
    1306
        Passage,
    1307
    }
    1308
    1309
    // used to determine standing alone (on left side)
    1310
    4.53k
    fn is_left_intervening_char(ch: char) -> bool {
    1311
    4.53k
        matches!4.34k(ch, 'B' | 'I' | '𝔹' | 'S' | 'T' | 'D' | 'C' | '𝐶' | 's' | 'w')
    1312
    4.53k
    }
    1313
    1314
    /// Return value for use_g1_word_mode()
    1315
    #[derive(Debug, PartialEq)]
    1316
    enum Grade1WordIndicator {
    1317
        NotInWord,        // no '𝟙' in the current/next word
    1318
        InWord,           // '𝟙' in the current/next word
    1319
        NotInChars,       // no '𝟙' in the entire string (optimization for common case)
    1320
    }
    1321
    1322
    1.89k
    fn remove_unneeded_mode_changes(raw_braille: &str, start_mode: UEB_Mode, start_duration: UEB_Duration) -> String {
    1323
        // FIX: need to be smarter about moving on wrt to typeforms/typefaces, caps, bold/italic. [maybe just let them loop through the default?]
    1324
    1.89k
        let mut mode = start_mode;
    1325
    1.89k
        let mut duration = start_duration;
    1326
    1.89k
        let mut start_g2_letter = None;    // used for start of contraction checks
    1327
    1.89k
        let mut i_g2_start = None;  // set to 'i' when entering G2 mode; None in other modes. '1' indicator goes here if standing alone
    1328
    1.89k
        let mut cap_word_mode = false;     // only set to true in G2 to prevent contractions
    1329
    1.89k
        let mut result = String::default();
    1330
    1.89k
        let chars = raw_braille.chars().collect::<Vec<char>>();
    1331
    1.89k
        let mut g1_word_indicator = Grade1WordIndicator::NotInChars;        // almost always true (and often irrelevant)
    1332
    1.89k
        if mode == UEB_Mode::Grade2 || duration == UEB_Duration::Symbol901 {
    1333
    991
            g1_word_indicator = use_g1_word_mode(&chars);
    1334
    991
            if g1_word_indicator == Grade1WordIndicator::InWord {
    1335
    1
                mode = UEB_Mode::Grade1;
    1336
    1
                if duration == UEB_Duration::Symbol {
    1337
    1
                    duration = UEB_Duration::Word;     // if Passage mode, leave as is
    1338
    1
                    result.push('𝟙')
    1339
    0
                }
    1340
    990
            }
    1341
    901
        }
    1342
    1.89k
        let mut i = 0;
    1343
    37.0k
        while i < chars.len() {
    1344
    35.1k
            let ch = chars[i];
    1345
    35.1k
            match mode {
    1346
                UEB_Mode::Numeric => {
    1347
                    // Numeric Mode: (from https://uebmath.aphtech.org/lesson1.0 and lesson4.0)
    1348
                    // Symbols that can appear within numeric mode include the ten digits, comma, period, simple fraction line,
    1349
                    // line continuation indicator, and numeric space digit symbols.
    1350
                    // A space or any other symbol not listed here terminates numeric mode.
    1351
                    // Numeric mode is also terminated by the "!" -- used after a script
    1352
                    //
    1353
                    // The numeric indicator also turns on grade 1 mode.
    1354
                    // When grade 1 mode is set by the numeric indicator,
    1355
                    //   grade 1 indicators are not used unless a single lower-case letter a-j immediately follows a digit.
    1356
                    // Grade 1 mode when set by the numeric indicator is terminated by a space, hyphen, dash, or a grade 1 indicator.
    1357
    3.31k
                    i_g2_start = None;
    1358
                    // debug!("Numeric: ch={}, duration: {:?}", ch, duration);
    1359
    3.31k
                    match ch {
    1360
                        'L' => {
    1361
                            // terminate numeric mode -- duration doesn't change
    1362
                            // let the default case handle pushing on the chars for the letter
    1363
    1.42k
                            if is_letter_number(unhighlight(chars[i+1])) {
    1364
    1.37k
                                result.push('1');   // need to distinguish a-j from a digit
    1365
    1.37k
                            }44
    1366
    1.42k
                            result.push(ch);
    1367
    1.42k
                            i += 1;
    1368
    1.42k
                            mode = UEB_Mode::Grade1;
    1369
                            // duration remains Word
    1370
                        },
    1371
                        '1' | '𝟙' => {
    1372
                            // numeric mode implies grade 1, so don't output indicator;
    1373
    107
                            i += 1;
    1374
    107
                            mode = UEB_Mode::Grade1;
    1375
    107
                            if start_duration == UEB_Duration::Passage {
    1376
    15
                                duration = UEB_Duration::Passage;      // otherwise it remains at Word
    1377
    92
                            }
    1378
                        },
    1379
                        '#' => {
    1380
                            // terminate numeric mode -- duration doesn't change
    1381
    738
                            i += 1;
    1382
    738
                            if i+1 < chars.len() && chars[i] == 'L'691 && is_letter_number22(unhighlight22(chars[i+1]22)) {
    1383
    9
                                // special case where the script was numeric and a letter follows, so need to put out G1 indicator
    1384
    9
                                result.push('1');
    1385
    9
                                // the G1 case should work with 'L' now
    1386
    729
                            }
    1387
    738
                            mode = UEB_Mode::Grade1;
    1388
                        },
    1389
    521
                        'N' => {
    1390
    521
                            // stay in the same mode (includes numeric "," and "." space) -- don't let default get these chars
    1391
    521
                            result.push(chars[i+1]);
    1392
    521
                            i += 2;
    1393
    521
                        },
    1394
                        _ => {
    1395
                            // moving out of numeric mode
    1396
    524
                            result.push(ch);
    1397
    524
                            i += 1;
    1398
    524
                            if "W𝐖-—―".contains(ch) {
    1399
    94
                                mode = start_mode;
    1400
    94
                                if mode == UEB_Mode::Grade2 {
    1401
    47
                                    start_g2_letter = None;        // will be set to real letter
    1402
    47
                                }
    1403
    94
                                if start_duration != UEB_Duration::Passage {
    1404
    47
                                    duration = UEB_Duration::Symbol;
    1405
    47
                                }
    1406
                            } else {
    1407
    430
                                mode = UEB_Mode::Grade1
    1408
                            }
    1409
                        },
    1410
                    }
    1411
                },
    1412
                UEB_Mode::Grade1 => {
    1413
                    // Grade 1 Mode:
    1414
                    // The numeric indicator also sets grade 1 mode.
    1415
                    // Grade 1 mode, when initiated by the numeric indicator, is terminated by a space, hyphen, dash or grade 1 terminator.
    1416
                    // Grade 1 mode is also set by grade 1 indicators.
    1417
    25.0k
                    i_g2_start = None;
    1418
                    // debug!("Grade 1: ch={}, duration: {:?}", ch, duration);
    1419
    25.0k
                    match ch {
    1420
    3.34k
                        'L' => {
    1421
    3.34k
                            // note: be aware of '#' case for Numeric because '1' might already be generated
    1422
    3.34k
                            // let prev_ch = if i > 1 {chars[i-1]} else {'1'};   // '1' -- anything beside ',' or '.'
    1423
    3.34k
                            // if duration == UEB_Duration::Symbol ||
    1424
    3.34k
                            //     ( ",. ".contains(prev_ch) && LETTER_NUMBERS.contains(&unhighlight(chars[i+1])) ) {
    1425
    3.34k
                            //     result.push('1');        // need to retain grade 1 indicator (RUEB 6.5.2)
    1426
    3.34k
                            // }
    1427
    3.34k
                            // let the default case handle pushing on the chars for the letter
    1428
    3.34k
                            result.push(ch);
    1429
    3.34k
                            i += 1;
    1430
    3.34k
                        },
    1431
                        '1' | '𝟙' => {
    1432
    2.35k
                            assert!(ch == '1' || duration != UEB_Duration::Symbol2);     // if '𝟙', should be Word or Passage duration
    1433
                            // nothing to do -- let the default case handle the following chars
    1434
    2.35k
                            i += 1;
    1435
                        },
    1436
    2.36k
                        'N' => {
    1437
    2.36k
                            result.push(ch);
    1438
    2.36k
                            result.push(chars[i+1]);
    1439
    2.36k
                            i += 2;
    1440
    2.36k
                            mode = UEB_Mode::Numeric;
    1441
    2.36k
                            duration = UEB_Duration::Word;
    1442
    2.36k
                        },
    1443
                        'W' | '𝐖' => {
    1444
                            // this terminates a word mode if there was one
    1445
    711
                            result.push(ch);
    1446
    711
                            i += 1;
    1447
    711
                            if start_duration != UEB_Duration::Passage {
    1448
    224
                                duration = UEB_Duration::Symbol;
    1449
    224
                                mode = UEB_Mode::Grade2;
    1450
    487
                            }
    1451
                        },
    1452
                        _ => {
    1453
    16.3k
                            result.push(ch);
    1454
    16.3k
                            i += 1;
    1455
    16.3k
                            if duration == UEB_Duration::Symbol && !is_letter_prefix(ch)1.34k {
    1456
    1.34k
                                mode = start_mode;
    1457
    14.9k
                            }
    1458
                        }
    1459
                    }
    1460
    25.0k
                    if mode == UEB_Mode::Grade2 {
    1461
    1.56k
                        start_g2_letter = None;        // will be set to real letter
    1462
    23.5k
                    }
    1463
    1464
                },
    1465
                UEB_Mode::Grade2 => {
    1466
                    // note: if we ended up using a '1', it only extends to the next char, which is also dealt with, so mode doesn't change
    1467
    6.79k
                   if i_g2_start.is_none() {
    1468
    2.58k
                       i_g2_start = Some(i);
    1469
    2.58k
                       cap_word_mode = false;
    1470
    4.21k
                   }
    1471
                    // debug!("Grade 2: ch={}, duration: {:?}", ch, duration);
    1472
    6.79k
                    match ch {
    1473
                        'L' => {
    1474
    1.44k
                            if start_g2_letter.is_none() {
    1475
    1.34k
                                start_g2_letter = Some(i);
    1476
    1.34k
                            }97
    1477
    1.44k
                            let (is_alone, right_matched_chars, n_letters) = stands_alone(&chars, i);
    1478
                            // GTM 1.2.1 says we only need to use G1 for single letters or sequences that are a shortform (e.g, "ab")
    1479
    1.44k
                            if is_alone && (n_letters == 1400 || is_short_form28(&right_matched_chars[..2*n_letters]28)) {
    1480
    373
                                // debug!("  is_alone -- pushing '1'");
    1481
    373
                                result.push('1');
    1482
    373
                                mode = UEB_Mode::Grade1;
    1483
    1.07k
                            }
    1484
                            // debug!("  pushing {:?}", right_matched_chars);
    1485
    3.13k
                            right_matched_chars1.44k.iter1.44k().for_each1.44k(|&ch| result.push(ch));
    1486
    1.44k
                            i += right_matched_chars.len();
    1487
                        },
    1488
                        'C' => {
    1489
                            // Want 'C' before 'L'; Could be CC for word cap -- if so, eat it and move on
    1490
                            // Note: guaranteed that there is a char after the 'C', so chars[i+1] is safe
    1491
    99
                            if chars[i+1] == 'C' {
    1492
    14
                                cap_word_mode = true;
    1493
    14
                                i += 1;
    1494
    14
                            } else {
    1495
    85
                                let is_greek = chars[i+1] == 'G';
    1496
    85
                                let (is_alone, right_matched_chars, n_letters) = stands_alone(&chars, if is_greek {i+22} else {i+183});
    1497
                                // GTM 1.2.1 says we only need to use G1 for single letters or sequences that are a shortform (e.g, "ab")
    1498
    85
                                if is_alone && (n_letters == 122 || is_short_form7(&right_matched_chars[..2*n_letters]7)) {
    1499
    16
                                    // debug!("  is_alone -- pushing '1'");
    1500
    16
                                    result.push('1');
    1501
    16
                                    mode = UEB_Mode::Grade1;
    1502
    69
                                }
    1503
    85
                                if cap_word_mode {
    1504
    14
                                    result.push('C');   // first 'C' if cap word
    1505
    71
                                }
    1506
    85
                                result.push('C');
    1507
    85
                                if is_greek {
    1508
    2
                                    result.push('G');
    1509
    2
                                    i += 1;
    1510
    83
                                }
    1511
    85
                                start_g2_letter = Some(i);
    1512
                                // debug!("  pushing 'C' + {:?}", right_matched_chars);
    1513
    256
                                right_matched_chars85.iter85().for_each85(|&ch| result.push(ch));
    1514
    85
                                i += 1 + right_matched_chars.len();
    1515
                            }
    1516
                        },
    1517
    1.34k
                        '1' => {
    1518
    1.34k
                            result.push(ch);
    1519
    1.34k
                            i += 1;
    1520
    1.34k
                            mode = UEB_Mode::Grade1;
    1521
    1.34k
                            duration = UEB_Duration::Symbol;
    1522
    1.34k
                        },
    1523
                        '𝟙' => {
    1524
                            // '𝟙' should have forced G1 Word mode
    1525
    0
                            error!("Internal error: '𝟙' found in G2 mode: index={i} in '{raw_braille}'");
    1526
    0
                            i += 1;
    1527
                        }
    1528
    582
                        'N' => {
    1529
    582
                            result.push(ch);
    1530
    582
                            result.push(chars[i+1]);
    1531
    582
                            i += 2;
    1532
    582
                            mode = UEB_Mode::Numeric;
    1533
    582
                            duration = UEB_Duration::Word;
    1534
    582
                        },
    1535
                        _ => {
    1536
    3.32k
                            if let Some(start505) = start_g2_letter {
    1537
    505
                                if !cap_word_mode {
    1538
    504
                                    result = handle_contractions(&chars[start..i], result);
    1539
    504
                                }1
    1540
    505
                                cap_word_mode = false;
    1541
    505
                                start_g2_letter = None;     // not start of char sequence
    1542
    2.81k
                            }
    1543
    3.32k
                            result.push(ch);
    1544
    3.32k
                            i += 1;
    1545
    3.32k
                            if !is_left_intervening_char(ch) {
    1546
    3.29k
                                cap_word_mode = false;
    1547
    3.29k
                                i_g2_start = Some(i);
    1548
    3.29k
                            }29
    1549
    1550
                        }
    1551
                    }
    1552
    6.79k
                    if mode != UEB_Mode::Grade2 && !cap_word_mode2.31k &&
    1553
    2.30k
                       let Some(start883) = start_g2_letter {
    1554
    883
                            result = handle_contractions(&chars[start..i], result);
    1555
    883
                            start_g2_letter = None;     // not start of char sequence
    1556
    5.91k
                        }
    1557
                },
    1558
            }
    1559
    1560
    35.1k
            if (ch == 'W' || ch == '𝐖'34.0k) && g1_word_indicator != Grade1WordIndicator::NotInChars1.13k &&
    1561
    602
               (mode == UEB_Mode::Grade2 || duration == UEB_Duration::Symbol0) {
    1562
    602
                g1_word_indicator = use_g1_word_mode(&chars[i..]);
    1563
    602
                if g1_word_indicator == Grade1WordIndicator::InWord {
    1564
    1
                    mode = UEB_Mode::Grade1;
    1565
    1
                    if duration == UEB_Duration::Symbol {
    1566
    1
                        duration = UEB_Duration::Word;     // if Passage mode, leave as is
    1567
    1
                        result.push('𝟙')
    1568
    0
                    }
    1569
    601
                }
    1570
    34.5k
            }
    1571
        }
    1572
    1.89k
        if mode == UEB_Mode::Grade2 &&
    1573
    289
           let Some(start31) = start_g2_letter {
    1574
    31
                result = handle_contractions(&chars[start..i], result);
    1575
    1.86k
            }
    1576
    1577
    1.89k
        return result;
    1578
    1579
    1580
    1.59k
        fn use_g1_word_mode(chars: &[char]) -> Grade1WordIndicator {
    1581
            // debug!("use_g1_word_mode: chars='{:?}'", chars);
    1582
    19.5k
            for &ch in chars1.59k {
    1583
    19.5k
                if ch == 'W' || ch == '𝐖'18.9k {
    1584
    601
                    return Grade1WordIndicator::NotInWord;       // reached a word boundary
    1585
    18.9k
                }
    1586
    18.9k
                if ch == '𝟙' {
    1587
    2
                    return Grade1WordIndicator::InWord;        // need word mode in this "word"
    1588
    18.9k
                }
    1589
            }
    1590
    990
            return Grade1WordIndicator::NotInChars;               //
    1591
    1.59k
        }
    1592
    1.89k
    }
    1593
    1594
    /// Returns a tuple:
    1595
    ///   true if the ith char "stands alone" (UEB 2.6)
    1596
    ///   the chars on the right that are part of the standing alone sequence
    1597
    ///   the number of letters in that sequence
    1598
    /// This basically means a letter sequence surrounded by white space with some potentially intervening chars
    1599
    /// The intervening chars can be typeform/cap indicators, along with various forms of punctuation
    1600
    /// The ith char should be an "L"
    1601
    /// This assumes that there is whitespace before and after the character string
    1602
    1.52k
    fn stands_alone(chars: &[char], i: usize) -> (bool, &[char], usize) {
    1603
        // scan backward and check the conditions for "standing-alone"
    1604
        // we scan forward and check the conditions for "standing-alone"
    1605
    1.52k
        assert_eq!(chars[i], 'L', "'stands_alone' starts with non 'L'");
    1606
        // debug!("stands_alone: i={}, chars: {:?}", i, chars);
    1607
    1.52k
        if !left_side_stands_alone(&chars[0..i]) {
    1608
    977
            return (false, &chars[i..i+2], 0);
    1609
    552
        }
    1610
    1611
    552
        let (mut is_alone, n_letters, n_right_matched) = right_side_stands_alone(&chars[i+2..]);
    1612
        // debug!("left is alone, right is alone: {}, : n_letters={}, n_right_matched={}", is_alone, n_letters, n_right_matched);
    1613
    1614
    552
        if is_alone && n_letters == 1425 {
    1615
    390
            let ch = chars[i+1];
    1616
    390
            if ch=='⠁' || ch=='⠊'389 || ch=='⠕'387 {      // a, i, o
    1617
    3
                is_alone = false;
    1618
    387
            }
    1619
    162
        }
    1620
    552
        return (is_alone, &chars[i..i+2+n_right_matched], n_letters);
    1621
    1622
        /// chars before 'L'
    1623
    1.52k
        fn left_side_stands_alone(chars: &[char]) -> bool {
    1624
            // scan backwards to skip letters and intervening chars
    1625
            // once we hit an intervening char, only intervening chars are allowed if standing alone
    1626
    1.52k
            let mut intervening_chars_mode = false; // true when we are on the final stretch
    1627
    1.52k
            let mut i = chars.len();
    1628
    1.86k
            while i > 0 {
    1629
    1.38k
                i -= 1;
    1630
    1.38k
                let ch = chars[i];
    1631
    1.38k
                let prev_ch = if i > 0 {chars[i-1]1.34k} else {' '45};  // ' ' is a char not in input
    1632
                // debug!("  left alone: prev/ch {}/{}", prev_ch, ch);
    1633
    1.38k
                if (!intervening_chars_mode && prev_ch == 'L'1.10k) ||
    1634
    1.30k
                   (prev_ch == 'o' || prev_ch == 'b'1.21k) {
    1635
    174
                    intervening_chars_mode = true;
    1636
    174
                    i -= 1;       // ignore 'Lx' and also ignore 'ox'
    1637
    1.21k
                } else if is_left_intervening_char(ch) {
    1638
    161
                    intervening_chars_mode = true;
    1639
    161
                } else {
    1640
    1.05k
                    return "W𝐖-—―".contains(ch);
    1641
                }
    1642
            }
    1643
    1644
    475
            return true;
    1645
    1.52k
        }
    1646
    1647
        // chars after character we are testing
    1648
    552
        fn right_side_stands_alone(chars: &[char]) -> (bool, usize, usize) {
    1649
            // see RUEB 2.6.3
    1650
    355
            fn is_right_intervening_char(ch: char) -> bool {
    1651
    355
                matches!342(ch, 'B' | 'I' | '𝔹' | 'S' | 'T' | 'D' | 'C' | '𝐶' | 's' | 'w' | 'e')
    1652
    355
            }
    1653
            // scan forward to skip letters and intervening chars
    1654
            // once we hit an intervening char, only intervening chars are allowed if standing alone ('c' and 'b' are part of them)
    1655
    552
            let mut intervening_chars_mode = false; // true when we are on the final stretch
    1656
    552
            let mut i = 0;
    1657
    552
            let mut n_letters = 1;      // we have skipped the first letter
    1658
    725
            while i < chars.len() {
    1659
    515
                let ch = chars[i];
    1660
                // debug!("  right alone: ch/next {}/{}", ch, if i+1<chars.len() {chars[i+1]} else {' '});
    1661
    515
                if !intervening_chars_mode && ch == 'L'502 {
    1662
    140
                    n_letters += 1;
    1663
    140
                    i += 1;       // ignore 'Lx' and also ignore 'ox'
    1664
    375
                } else if ch == 'c' || ch == 'b'355 {
    1665
    20
                    i += 1;       // ignore 'Lx' and also ignore 'ox'
    1666
    355
                } else if is_right_intervening_char(ch) {
    1667
    13
                    intervening_chars_mode = true;
    1668
    13
                } else {
    1669
    342
                    return if "W𝐖-—―".contains(ch) {(true, n_letters, i)215} else {(false, n_letters, i)127};
    1670
                }
    1671
    173
                i += 1;
    1672
            }
    1673
    1674
    210
            return (true, n_letters, chars.len());
    1675
    552
        }
    1676
    1.52k
    }
    1677
    1678
    1679
    /// Return a modified result if chars can be contracted.
    1680
    /// Otherwise, the original string is returned
    1681
    1.41k
    fn handle_contractions(chars: &[char], mut result: String) -> String {
    1682
        struct Replacement {
    1683
            pattern: String,
    1684
            replacement: &'static str
    1685
        }
    1686
    1687
        const ASCII_TO_UNICODE: &[char] = &[
    1688
            '⠀', '⠮', '⠐', '⠼', '⠫', '⠩', '⠯', '⠄', '⠷', '⠾', '⠡', '⠬', '⠠', '⠤', '⠨', '⠌',
    1689
            '⠴', '⠂', '⠆', '⠒', '⠲', '⠢', '⠖', '⠶', '⠦', '⠔', '⠱', '⠰', '⠣', '⠿', '⠜', '⠹',
    1690
            '⠈', '⠁', '⠃', '⠉', '⠙', '⠑', '⠋', '⠛', '⠓', '⠊', '⠚', '⠅', '⠇', '⠍', '⠝', '⠕',
    1691
            '⠏', '⠟', '⠗', '⠎', '⠞', '⠥', '⠧', '⠺', '⠭', '⠽', '⠵', '⠪', '⠳', '⠻', '⠘', '⠸',
    1692
        ];
    1693
    1694
    36
        fn to_unicode_braille(ascii: &str) -> String {
    1695
    36
            let mut unicode = String::with_capacity(4*ascii.len());   // 'L' + 3 bytes for braille char
    1696
    82
            for ch in ascii36.as_bytes36() {
    1697
    82
                unicode.push('L');
    1698
    82
                unicode.push(ASCII_TO_UNICODE[(ch.to_ascii_uppercase() - 32) as usize])
    1699
            }
    1700
    36
            return unicode;
    1701
    36
        }
    1702
    1703
        // It would be much better from an extensibility point of view to read the table in from a file
    1704
    2
        static CONTRACTIONS: LazyLock<Vec<Replacement>> = LazyLock::new(|| { vec![
    1705
                // 10.3: Strong contractions
    1706
    2
                Replacement{ pattern: to_unicode_braille("and"), replacement: "L⠯"},
    1707
    2
                Replacement{ pattern: to_unicode_braille("for"), replacement: "L⠿"},
    1708
    2
                Replacement{ pattern: to_unicode_braille("of"), replacement: "L⠷"},
    1709
    2
                Replacement{ pattern: to_unicode_braille("the"), replacement: "L⠮"},
    1710
    2
                Replacement{ pattern: to_unicode_braille("with"), replacement: "L⠾"},
    1711
    1712
                // 10.8: final-letter group signs (this need to precede 'en' and any other shorter contraction)
    1713
    2
                Replacement{ pattern: "(?P<s>L.)L⠍L⠑L⠝L⠞".to_string(), replacement: "${s}L⠰L⠞" }, // ment
    1714
    2
                Replacement{ pattern: "(?P<s>L.)L⠞L⠊L⠕L⠝".to_string(), replacement: "${s}L⠰L⠝" } ,// tion
    1715
    1716
                // 10.4: Strong group signs
    1717
    2
                Replacement{ pattern: to_unicode_braille("ch"), replacement: "L⠡"},
    1718
    2
                Replacement{ pattern: to_unicode_braille("gh"), replacement: "L⠣"},
    1719
    2
                Replacement{ pattern: to_unicode_braille("sh"), replacement: "L⠩"},
    1720
    2
                Replacement{ pattern: to_unicode_braille("th"), replacement: "L⠹"},
    1721
    2
                Replacement{ pattern: to_unicode_braille("wh"), replacement: "L⠱"},
    1722
    2
                Replacement{ pattern: to_unicode_braille("ed"), replacement: "L⠫"},
    1723
    2
                Replacement{ pattern: to_unicode_braille("er"), replacement: "L⠻"},
    1724
    2
                Replacement{ pattern: to_unicode_braille("ou"), replacement: "L⠳"},
    1725
    2
                Replacement{ pattern: to_unicode_braille("ow"), replacement: "L⠪"},
    1726
    2
                Replacement{ pattern: to_unicode_braille("st"), replacement: "L⠌"},
    1727
    2
                Replacement{ pattern: "(?P<s>L.)L⠊L⠝L⠛".to_string(), replacement: "${s}L⠬" },  // 'ing', not at start
    1728
    2
                Replacement{ pattern: to_unicode_braille("ar"), replacement: "L⠜"},
    1729
    1730
                // 10.6.5: Lower group signs preceded and followed by letters
    1731
                // FIX: don't match if after/before a cap letter -- can't use negative pattern (?!...) in regex package
    1732
                // Note: removed cc because "arccos" shouldn't be contracted (10.11.1), but there is no way to know about compound words
    1733
                // Add it back after implementing a lookup dictionary of exceptions
    1734
    2
                Replacement{ pattern: "(?P<s>L.)L⠑L⠁(?P<e>L.)".to_string(), replacement: "${s}L⠂${e}" },  // ea
    1735
    2
                Replacement{ pattern: "(?P<s>L.)L⠃L⠃(?P<e>L.)".to_string(), replacement: "${s}L⠆${e}" },  // bb
    1736
                // Replacement{ pattern: "(?P<s>L.)L⠉L⠉(?P<e>L.)".to_string(), replacement: "${s}L⠒${e}" },  // cc
    1737
    2
                Replacement{ pattern: "(?P<s>L.)L⠋L⠋(?P<e>L.)".to_string(), replacement: "${s}L⠖${e}" },  // ff
    1738
    2
                Replacement{ pattern: "(?P<s>L.)L⠛L⠛(?P<e>L.)".to_string(), replacement: "${s}L⠶${e}" },  // gg
    1739
    1740
                // 10.6.8: Lower group signs ("in" also 10.5.4 lower word signs)
    1741
                // FIX: these need restrictions about only applying when upper dots are present
    1742
    2
                Replacement{ pattern: to_unicode_braille("en"), replacement: "⠢"},
    1743
    2
                Replacement{ pattern: to_unicode_braille("in"), replacement: "⠔"},
    1744
    1745
            ]
    1746
    2
        });
    1747
    1748
    2
        static CONTRACTION_PATTERNS: LazyLock<RegexSet> = LazyLock::new(|| init_patterns(&CONTRACTIONS));
    1749
    1
        static CONTRACTION_REGEX: LazyLock<Vec<Regex>> = LazyLock::new(|| init_regex(&CONTRACTIONS));
    1750
    1751
    1.41k
        let mut chars_as_str = chars.iter().collect::<String>();
    1752
        // debug!("  handle_contractions: examine '{}'", &chars_as_str);
    1753
    1.41k
        let matches = CONTRACTION_PATTERNS.matches(&chars_as_str);
    1754
    1.41k
        for i35 in matches.iter() {
    1755
    35
            let element = &CONTRACTIONS[i];
    1756
    35
            // debug!("  replacing '{}' with '{}' in '{}'", element.pattern, element.replacement, &chars_as_str);
    1757
    35
            result.truncate(result.len() - chars_as_str.len());
    1758
    35
            chars_as_str = CONTRACTION_REGEX[i].replace_all(&chars_as_str, element.replacement).to_string();
    1759
    35
            result.push_str(&chars_as_str);
    1760
    35
            // debug!("  result after replace '{}'", result);
    1761
    35
        }
    1762
    1.41k
        return result;
    1763
    1764
    1765
    1766
    2
        fn init_patterns(contractions: &[Replacement]) -> RegexSet {
    1767
    2
            let mut vec: Vec<&str> = Vec::with_capacity(contractions.len());
    1768
    50
            for contraction in contractions2 {
    1769
    50
                vec.push(&contraction.pattern);
    1770
    50
            }
    1771
    2
            return RegexSet::new(&vec).unwrap();
    1772
    2
        }
    1773
    1774
    1
        fn init_regex(contractions: &[Replacement]) -> Vec<Regex> {
    1775
    1
            let mut vec = Vec::with_capacity(contractions.len());
    1776
    25
            for contraction in contractions1 {
    1777
    25
                vec.push(Regex::new(&contraction.pattern).unwrap());
    1778
    25
            }
    1779
    1
            return vec;
    1780
    1
        }
    1781
    1.41k
    }
    1782
    1783
    1784
    1785
    1786
    static VIETNAM_INDICATOR_REPLACEMENTS: phf::Map<&str, &str> = phf_map! {
    1787
        "S" => "XXX",    // sans-serif -- from prefs
    1788
        "B" => "⠘",     // bold
    1789
        "𝔹" => "XXX",     // blackboard -- from prefs
    1790
        "T" => "⠈",     // script
    1791
        "I" => "⠨",     // italic
    1792
        "R" => "",      // roman
    1793
        // "E" => "⠰",     // English
    1794
        "1" => "⠠",     // Grade 1 symbol
    1795
        "L" => "",     // Letter left in to assist in locating letters
    1796
        "D" => "XXX",     // German (Deutsche) -- from prefs
    1797
        "G" => "⠰",     // Greek
    1798
        "V" => "XXX",    // Greek Variants
    1799
        // "H" => "⠠⠠",    // Hebrew
    1800
        // "U" => "⠈⠈",    // Russian
    1801
        "C" => "⠨",      // capital
    1802
        "𝑐" => "",       // second or latter braille cell of a capital letter
    1803
        "𝐶" => "⠨",      // capital that never should get word indicator (from chemical element)
    1804
        "N" => "⠼",     // number indicator
    1805
        "t" => "⠱",     // shape terminator
    1806
        "W" => "⠀",     // whitespace"
    1807
        "𝐖"=> "⠀",     // whitespace
    1808
        "s" => "⠆",     // typeface single char indicator
    1809
        "w" => "",     // typeface word indicator
    1810
        "e" => "",     // typeface & capital terminator
    1811
        "o" => "",       // flag that what follows is an open indicator (used for standing alone rule)
    1812
        "c" => "",     // flag that what follows is an close indicator (used for standing alone rule)
    1813
        "b" => "",       // flag that what follows is an open or close indicator (used for standing alone rule)
    1814
        "," => "⠂",     // comma
    1815
        "." => "⠲",     // period
    1816
        "-" => "-",     // hyphen
    1817
        "—" => "⠠⠤",   // normal dash (2014) -- assume all normal dashes are unified here [RUEB appendix 3]
    1818
        "―" => "⠐⠠⠤",  // long dash (2015) -- assume all long dashes are unified here [RUEB appendix 3]
    1819
        "#" => "",      // signals end of script
    1820
        "!" => "",      // Hack used to prevent some regular expression matches
    1821
    };
    1822
    1823
    112
    fn vietnam_cleanup(pref_manager: Ref<PreferenceManager>, raw_braille: String) -> String {
    1824
        // Deal with Vietnamese "rhymes" -- moving accents around
    1825
        // See "Vietnamese Uncontracted Braille Update in MathCAT" or maybe https://icanreadvietnamese.com/blog/14-rule-of-tone-mark-placement
    1826
        // Note: I don't know how to write (for example) I_E_RULE so that it excludes "qu" and "gi", so I use two rules
    1827
        // The first rule rewrites the patterns with "qu" and "gi" to add "!" to prevent a match of the second rule -- "!" is dropped later
    1828
    1
        static QU_GI_RULE_EXCEPTION: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(L⠟L⠥|L⠛L⠊)").unwrap());
    1829
    1
        static IUOY_E_RULE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"L(⠊|⠥|⠕|⠽)(L[⠔⠰⠢⠤⠠])L(⠑|⠣)").unwrap()); // ie, ue, oe, and ye rule
    1830
    1
        static UO_A_RULE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"L(⠥|⠕)(L[⠔⠰⠢⠤⠠])L(⠁|⠡|⠜)").unwrap()); // ua, oa rule
    1831
    1
        static UU_O_RULE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"L(⠥|⠳)(L[⠔⠰⠢⠤⠠])L(⠪|⠹)").unwrap()); // uo, ưo rule
    1832
    1
        static UYE_RULE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"L⠥L([⠔⠰⠢⠤⠠])L⠽L⠣").unwrap()); // uo, ưo rule
    1833
    1
        static UY_RULE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"L⠥L([⠔⠰⠢⠤⠠])L⠽").unwrap()); // uo, ưo rule
    1834
    1
        static REPLACE_INDICATORS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([1𝟙SB𝔹TIREDGVHP𝐶𝑐CLMNW𝐖swe,.-—―#ocb!])").unwrap());
    1835
        // debug!("vietnam_cleanup: start={}", raw_braille);
    1836
    112
        let result = typeface_to_word_mode(&raw_braille);
    1837
    112
        let result = capitals_to_word_mode(&result);
    1838
    1839
    112
        let result = result.replace("tW", "W");
    1840
    112
        let result = result.replace("CG", "⠸");    // capital Greek letters are problematic in Vietnam braille
    1841
    112
        let result = result.replace("CC", "⠸");    // capital word more is the same as capital Greek letters
    1842
        // debug!("   after typeface/caps={}", &result);
    1843
    1844
        // deal with "rhymes"
    1845
    112
        let result = QU_GI_RULE_EXCEPTION.replace_all(&result, "${1}!");
    1846
        // debug!("          after except={}", &result);
    1847
    112
        let result = IUOY_E_RULE.replace_all(&result, "${2}L${1}L${3}");
    1848
        // debug!("          after IUOY_E={}", &result);
    1849
    112
        let result = UO_A_RULE.replace_all(&result, "${2}L${1}L${3}");
    1850
        // debug!("          after   UO_A={}", &result);
    1851
    112
        let result = UU_O_RULE.replace_all(&result, "${2}L${1}L${3}");
    1852
        // debug!("          after   UO_O={}", &result);
    1853
    112
        let result = UYE_RULE.replace_all(&result, "${1}L⠥L⠽L⠣");  // longer match first
    1854
        // debug!("          after    UYE={}", &result);
    1855
    112
        let result = UY_RULE.replace_all(&result, "${1}L⠥L⠽");
    1856
        // debug!("          after     UY={}", &result);
    1857
    1858
        // these typeforms need to get pulled from user-prefs as they are transcriber-defined
    1859
    112
        let double_struck = pref_manager.pref_to_string("Vietnam_DoubleStruck");
    1860
    112
        let sans_serif = pref_manager.pref_to_string("Vietnam_SansSerif");
    1861
    112
        let fraktur = pref_manager.pref_to_string("Vietnam_Fraktur");
    1862
    112
        let greek_variant = pref_manager.pref_to_string("Vietnam_GreekVariant");
    1863
    1864
        // This reuses the code just for getting rid of unnecessary "L"s and "N"s
    1865
    112
        let result = remove_unneeded_mode_changes(&result, UEB_Mode::Grade1, UEB_Duration::Passage);
    1866
    1867
    1868
    1.23k
        let result112 = REPLACE_INDICATORS112.replace_all112(&result112, |cap: &Captures| {
    1869
    1.23k
            let matched_char = &cap[0];
    1870
    1.23k
            match matched_char {
    1871
    1.23k
                "𝔹" => &double_struck0,
    1872
    1.23k
                "S" => &sans_serif0,
    1873
    1.23k
                "D" => &fraktur0,
    1874
    1.23k
                "V" => &greek_variant0,
    1875
    1.23k
                _ => match VIETNAM_INDICATOR_REPLACEMENTS.get(matched_char) {
    1876
    0
                    None => {error!("REPLACE_INDICATORS and VIETNAM_INDICATOR_REPLACEMENTS are not in sync: missing '{matched_char}'"); ""},
    1877
    1.23k
                    Some(&ch) => ch,
    1878
                },
    1879
            }
    1880
    1.23k
        });
    1881
    1882
        // Remove unicode blanks at start and end -- do this after the substitutions because ',' introduces spaces
    1883
        // let result = result.trim_start_matches('⠀').trim_end_matches('⠀');
    1884
    112
        let result = COLLAPSE_SPACES.replace_all(&result, "⠀");
    1885
    1886
    112
        return result.to_string();
    1887
    112
    }
    1888
    1889
    1890
    static CMU_INDICATOR_REPLACEMENTS: phf::Map<&str, &str> = phf_map! {
    1891
        // "S" => "XXX",    // sans-serif -- from prefs
    1892
        "B" => "⠔",     // bold
    1893
        "𝔹" => "⠬",     // blackboard -- from prefs
    1894
        // "T" => "⠈",     // script
    1895
        "I" => "⠔",     // italic -- same as bold
    1896
        // "R" => "",      // roman
    1897
        // "E" => "⠰",     // English
    1898
        "1" => "⠐",     // Grade 1 symbol -- used here for a-j after number
    1899
        "L" => "",     // Letter left in to assist in locating letters
    1900
        "D" => "⠠",     // German (Gothic)
    1901
        "G" => "⠈",     // Greek
    1902
        "V" => "⠈⠬",    // Greek Variants
    1903
        // "H" => "⠠⠠",    // Hebrew
    1904
        // "U" => "⠈⠈",    // Russian
    1905
        "C" => "⠨",      // capital
    1906
        "𝐶" => "⠨",      // capital that never should get word indicator (from chemical element)
    1907
        "N" => "⠼",     // number indicator
    1908
        "𝑁" => "",      // continue number
    1909
        // "t" => "⠱",     // shape terminator
    1910
        "W" => "⠀",     // whitespace"
    1911
        "𝐖"=> "⠀",     // whitespace
    1912
        // "𝘄" => "⠀",    // add whitespace if char to the left has dots 1, 2, or 3 -- special rule handled separately, so commented out
    1913
        "s" => "",     // typeface single char indicator
    1914
        // "w" => "⠂",     // typeface word indicator
    1915
        // "e" => "⠄",     // typeface & capital terminator
    1916
        // "o" => "",       // flag that what follows is an open indicator (used for standing alone rule)
    1917
        // "c" => "",       // flag that what follows is an close indicator (used for standing alone rule)
    1918
        // "b" => "",       // flag that what follows is an open or close indicator (used for standing alone rule)
    1919
        "," => "⠂",     // comma
    1920
        "." => "⠄",     // period
    1921
        "-" => "⠤",     // hyphen
    1922
        "—" => "⠤⠤",   // normal dash (2014) -- assume all normal dashes are unified here [RUEB appendix 3]
    1923
        // "―" => "⠐⠤⠤",  // long dash (2015) -- assume all long dashes are unified here [RUEB appendix 3]
    1924
        "#" => "⠼",      // signals to end/restart of numeric mode (mixed fractions)
    1925
    };
    1926
    1927
    1928
    372
    fn cmu_cleanup(_pref_manager: Ref<PreferenceManager>, raw_braille: String) -> String {
    1929
    2
        static ADD_WHITE_SPACE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"𝘄(.)|𝘄$").unwrap());
    1930
    1931
        // debug!("cmu_cleanup: start={}", raw_braille);
    1932
        // let result = typeface_to_word_mode(&raw_braille);
    1933
    1934
        // let result = result.replace("tW", "W");
    1935
    372
        let result = raw_braille.replace("CG", "⠘")
    1936
    372
                                    .replace("𝔹C", "⠩")
    1937
    372
                                    .replace("DC", "⠰");
    1938
        // let result = result.replace("CC", "⠸");
    1939
    1940
        // these typeforms need to get pulled from user-prefs as they are transcriber-defined
    1941
        // let double_struck = pref_manager.pref_to_string("CMU_DoubleStruck");
    1942
        // let sans_serif = pref_manager.pref_to_string("CMU_SansSerif");
    1943
        // let fraktur = pref_manager.pref_to_string("CMU_Fraktur");
    1944
    1945
        // debug!("Before remove mode changes: '{}'", &result);
    1946
        // This reuses the code just for getting rid of unnecessary "L"s and "N"s
    1947
    372
        let result = remove_unneeded_mode_changes(&result, UEB_Mode::Grade1, UEB_Duration::Passage);
    1948
    372
        let result = result.replace("𝑁N", "");
    1949
        // debug!(" After remove mode changes: '{}'", &result);
    1950
    1951
    2.58k
        let result372 = REPLACE_INDICATORS372.replace_all372(&result372, |cap: &Captures| {
    1952
    2.58k
            match CMU_INDICATOR_REPLACEMENTS.get(&cap[0]) {
    1953
    0
                None => {error!("REPLACE_INDICATORS and CMU_INDICATOR_REPLACEMENTS are not in sync"); ""},
    1954
    2.58k
                Some(&ch) => ch,
    1955
            }
    1956
    2.58k
        });
    1957
    372
        let result = ADD_WHITE_SPACE.replace_all(&result, |cap: &Captures| {12
    1958
    12
            if cap.get(1).is_none() {
    1959
    2
                return "⠀".to_string();
    1960
            } else {
    1961
                // debug!("ADD_WHITE_SPACE match='{}', has left dots = {}", &cap[1], has_left_dots(cap[1].chars().next().unwrap()));
    1962
    10
                let mut next_chars = cap[1].chars();
    1963
    10
                let next_char = next_chars.next().unwrap();
    1964
    10
                assert!(next_chars.next().is_none());
    1965
    10
                return (if has_left_dots(next_char) {"⠀"9} else {""1}).to_string() + &cap[1];
    1966
            }
    1967
    12
        });
    1968
    1969
        // Remove unicode blanks at start and end -- do this after the substitutions because ',' introduces spaces
    1970
    372
        let result = COLLAPSE_SPACES.replace_all(&result, "⠀");
    1971
    372
        let result = result.trim_start_matches('⠀');            // don't trip end (e.g., see once::vector_11_2_5)
    1972
    372
        return result.to_string();
    1973
    1974
    10
        fn has_left_dots(ch: char) -> bool {
    1975
            // Unicode braille is set up so dot 1 is 2^0, dot 2 is 2^1, etc
    1976
    10
            return ( (ch as u32 - 0x2800) >> 4 ) > 0;
    1977
    10
        }
    1978
    372
    }
    1979
    1980
    1981
    1982
    static SWEDISH_INDICATOR_REPLACEMENTS: phf::Map<&str, &str> = phf_map! {
    1983
        // FIX: this needs cleaning up -- not all of these are used
    1984
        "S" => "XXX",    // sans-serif -- from prefs
    1985
        "B" => "⠨",     // bold
    1986
        "𝔹" => "XXX",     // blackboard -- from prefs
    1987
        "T" => "⠈",     // script
    1988
        "I" => "⠨",     // italic
    1989
        "R" => "",      // roman
    1990
        "1" => "⠱",     // Grade 1 symbol (used for number followed by a letter)
    1991
        "L" => "",     // Letter left in to assist in locating letters
    1992
        "D" => "XXX",     // German (Deutsche) -- from prefs
    1993
        "G" => "⠰",     // Greek
    1994
        "V" => "XXX",    // Greek Variants
    1995
        // "H" => "⠠⠠",    // Hebrew
    1996
        // "U" => "⠈⠈",    // Russian
    1997
        "C" => "⠠",      // capital
    1998
        "𝑐" => "",       // second or latter braille cell of a capital letter
    1999
        "𝐶" => "⠠",      // capital that never should get word indicator (from chemical element)
    2000
        "N" => "⠼",     // number indicator
    2001
        "t" => "⠱",     // shape terminator
    2002
        "W" => "⠀",     // whitespace"
    2003
        "𝐖"=> "⠀",     // whitespace
    2004
        "w" => "⠀",     // whitespace after function name
    2005
        "s" => "",     // typeface single char indicator
    2006
        "e" => "",     // typeface & capital terminator
    2007
        "E" => "⠱",     // empty base -- see index of radical
    2008
        "o" => "",       // flag that what follows is an open indicator (used for standing alone rule)
    2009
        "c" => "",     // flag that what follows is an close indicator (used for standing alone rule)
    2010
        "b" => "",       // flag that what follows is an open or close indicator (used for standing alone rule)
    2011
        "," => "⠂",     // comma
    2012
        "." => "⠲",     // period
    2013
        "-" => "-",     // hyphen
    2014
        "—" => "⠠⠤",   // normal dash (2014) -- assume all normal dashes are unified here [RUEB appendix 3]
    2015
        "―" => "⠐⠠⠤",  // long dash (2015) -- assume all long dashes are unified here [RUEB appendix 3]
    2016
        "#" => "",      // signals end of script
    2017
    2018
    };
    2019
    2020
    2021
    static FINNISH_INDICATOR_REPLACEMENTS: phf::Map<&str, &str> = phf_map! {
    2022
        // FIX: this needs cleaning up -- not all of these are used
    2023
        "S" => "XXX",    // sans-serif -- from prefs
    2024
        "B" => "⠨",     // bold
    2025
        "𝔹" => "XXX",     // blackboard -- from prefs
    2026
        "T" => "⠈",     // script
    2027
        "I" => "⠨",     // italic
    2028
        "R" => "",      // roman
    2029
        "E" => "⠰",     // English
    2030
        "1" => "⠀",     // Grade 1 symbol (used for number followed by a letter)
    2031
        "L" => "",     // Letter left in to assist in locating letters
    2032
        "D" => "XXX",     // German (Deutsche) -- from prefs
    2033
        "G" => "⠨",     // Greek
    2034
        "V" => "XXX",    // Greek Variants
    2035
        // "H" => "⠠⠠",    // Hebrew
    2036
        // "U" => "⠈⠈",    // Russian
    2037
        "C" => "⠠",      // capital
    2038
        "𝑐" => "",       // second or latter braille cell of a capital letter
    2039
        "𝐶" => "⠠",      // capital that never should get whitespace in front (from chemical element)
    2040
        "N" => "⠼",     // number indicator
    2041
        "n" => "⠼",     // number indicator for drop numbers (special case with close parens)
    2042
        "t" => "⠱",     // shape terminator
    2043
        "W" => "⠀",     // whitespace"
    2044
        "𝐖"=> "⠀",     // whitespace
    2045
        "s" => "⠆",     // typeface single char indicator
    2046
        "w" => "",     // typeface word indicator
    2047
        "e" => "",     // typeface & capital terminator
    2048
        "," => "⠂",     // comma
    2049
        "." => "⠲",     // period
    2050
        "-" => "-",     // hyphen
    2051
        "—" => "⠠⠤",   // normal dash (2014) -- assume all normal dashes are unified here [RUEB appendix 3]
    2052
        "―" => "⠐⠠⠤",  // long dash (2015) -- assume all long dashes are unified here [RUEB appendix 3]
    2053
        "(" => "⠦",     // Not really needed, but done for consistency with ")"
    2054
        ")" => "⠴",     // Needed for rules with drop numbers to avoid mistaking for dropped 0
    2055
        "↑" => "⠬",     // superscript
    2056
        "↓" => "⠡",     // subscript
    2057
        "#" => "",      // signals end of script
    2058
        "Z" => "⠐",     // signals end of index of root, integrand/lim from function ("zone change")
    2059
    2060
    };
    2061
    2062
    0
    fn finnish_cleanup(pref_manager: Ref<PreferenceManager>, raw_braille: String) -> String {
    2063
    0
        static REPLACE_INDICATORS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([SB𝔹TIREDGVHUP𝐏C𝐶LlMmb↑↓Nn𝑁WwZ,()])").unwrap());
    2064
        // Numbers need to end with a space, but sometimes there is one there for other reasons
    2065
    0
        static DROP_NUMBER_SEPARATOR: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(n.)\)").unwrap());
    2066
    0
        static NUMBER_MATCH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"((N.)+[^WN𝐶#↑↓Z])").unwrap());
    2067
    2068
        // debug!("finnish_cleanup: start={}", raw_braille);
    2069
    0
        let result = DROP_NUMBER_SEPARATOR.replace_all(&raw_braille, |cap: &Captures| {
    2070
            // match includes the char after the number -- insert the whitespace before it
    2071
            // debug!("DROP_NUMBER_SEPARATOR match='{}'", &cap[1]);
    2072
    0
            return cap[1].to_string() + "𝐶)";       // hack to use "𝐶" instead of dot 6 directly, but works for NUMBER_MATCH
    2073
    0
        });
    2074
    0
        let result = result.replace('n', "N");  // avoids having to modify remove_unneeded_mode_changes()
    2075
    0
        let result = NUMBER_MATCH.replace_all(&result, |cap: &Captures| {
    2076
            // match includes the char after the number -- insert the whitespace before it
    2077
            // debug!("NUMBER_MATCH match='{}'", &cap[1]);
    2078
    0
            let mut chars = cap[0].chars();
    2079
    0
            let last_char = chars.next_back().unwrap(); // unwrap safe since several chars were matched
    2080
    0
            return chars.as_str().to_string() + "W" + &last_char.to_string();
    2081
    0
        });
    2082
    2083
        // FIX: need to implement this -- this is just a copy of the Vietnam code
    2084
    0
        let result = result.replace("CG", "⠘")
    2085
    0
                                        .replace("𝔹C", "⠩")
    2086
    0
                                        .replace("DC", "⠰");
    2087
    2088
        // debug!("   after typeface/caps={}", &result);
    2089
    2090
        // these typeforms need to get pulled from user-prefs as they are transcriber-defined
    2091
    0
        let double_struck = pref_manager.pref_to_string("Vietnam_DoubleStruck");
    2092
    0
        let sans_serif = pref_manager.pref_to_string("Vietnam_SansSerif");
    2093
    0
        let fraktur = pref_manager.pref_to_string("Vietnam_Fraktur");
    2094
    0
        let greek_variant = pref_manager.pref_to_string("Vietnam_GreekVariant");
    2095
    2096
        // This reuses the code just for getting rid of unnecessary "L"s and "N"s
    2097
    0
        let result = remove_unneeded_mode_changes(&result, UEB_Mode::Grade1, UEB_Duration::Passage);
    2098
        // debug!("   remove_unneeded_mode_changes={}", &result);
    2099
    2100
    2101
    0
        let result = REPLACE_INDICATORS.replace_all(&result, |cap: &Captures| {
    2102
    0
            let matched_char = &cap[0];
    2103
    0
            match matched_char {
    2104
    0
                "𝔹" => &double_struck,
    2105
    0
                "S" => &sans_serif,
    2106
    0
                "D" => &fraktur,
    2107
    0
                "V" => &greek_variant,
    2108
    0
                _ => match FINNISH_INDICATOR_REPLACEMENTS.get(matched_char) {
    2109
    0
                    None => {error!("REPLACE_INDICATORS and SWEDISH_INDICATOR_REPLACEMENTS are not in sync: missing '{matched_char}'"); ""},
    2110
    0
                    Some(&ch) => ch,
    2111
                },
    2112
            }
    2113
    0
        });
    2114
    2115
        // Remove unicode blanks at start and end -- do this after the substitutions because ',' introduces spaces
    2116
        // let result = result.trim_start_matches('⠀').trim_end_matches('⠀');
    2117
    0
        let result = COLLAPSE_SPACES.replace_all(&result, "⠀");
    2118
    2119
    0
        return result.to_string();
    2120
    0
    }
    2121
    2122
    2123
    0
    fn swedish_cleanup(pref_manager: Ref<PreferenceManager>, raw_braille: String) -> String {
    2124
        // FIX: need to implement this -- this is just a copy of the Vietnam code
    2125
        // Empty bases are ok if they follow whitespace
    2126
    0
        static EMPTY_BASE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(^|[W𝐖w])E").unwrap());
    2127
        // debug!("swedish_cleanup: start={}", raw_braille);
    2128
    0
        let result = typeface_to_word_mode(&raw_braille);
    2129
    0
        let result = capitals_to_word_mode(&result);
    2130
    2131
    0
        let result = result.replace("CG", "⠘")
    2132
    0
                                        .replace("𝔹C", "⠩")
    2133
    0
                                        .replace("DC", "⠰");
    2134
    2135
        // debug!("   after typeface/caps={}", &result);
    2136
    2137
        // these typeforms need to get pulled from user-prefs as they are transcriber-defined
    2138
    0
        let double_struck = pref_manager.pref_to_string("Vietnam_DoubleStruck");
    2139
    0
        let sans_serif = pref_manager.pref_to_string("Vietnam_SansSerif");
    2140
    0
        let fraktur = pref_manager.pref_to_string("Vietnam_Fraktur");
    2141
    0
        let greek_variant = pref_manager.pref_to_string("Vietnam_GreekVariant");
    2142
    2143
        // This reuses the code just for getting rid of unnecessary "L"s and "N"s
    2144
    0
        let result = remove_unneeded_mode_changes(&result, UEB_Mode::Grade1, UEB_Duration::Passage);
    2145
        // debug!("   after removing mode changes={}", &result);
    2146
    2147
    2148
    0
        let result = EMPTY_BASE.replace_all(&result, "$1");
    2149
    0
        let result = REPLACE_INDICATORS.replace_all(&result, |cap: &Captures| {
    2150
    0
            let matched_char = &cap[0];
    2151
    0
            match matched_char {
    2152
    0
                "𝔹" => &double_struck,
    2153
    0
                "S" => &sans_serif,
    2154
    0
                "D" => &fraktur,
    2155
    0
                "V" => &greek_variant,
    2156
    0
                _ => match SWEDISH_INDICATOR_REPLACEMENTS.get(matched_char) {
    2157
    0
                    None => {error!("REPLACE_INDICATORS and SWEDISH_INDICATOR_REPLACEMENTS are not in sync: missing '{matched_char}'"); ""},
    2158
    0
                    Some(&ch) => ch,
    2159
                },
    2160
            }
    2161
    0
        });
    2162
    2163
        // Remove unicode blanks at start and end -- do this after the substitutions because ',' introduces spaces
    2164
        // let result = result.trim_start_matches('⠀').trim_end_matches('⠀');
    2165
    0
        let result = COLLAPSE_SPACES.replace_all(&result, "⠀");
    2166
    2167
    0
        return result.to_string();
    2168
    0
    }
    2169
    2170
    #[allow(non_snake_case)]
    2171
    50
    fn LaTeX_cleanup(_pref_manager: Ref<PreferenceManager>, raw_braille: String) -> String {
    2172
    1
        static REMOVE_SPACE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r" ([\^_,;)\]}])").unwrap()); // '^', '_', ',', ';', ')', ']', '}'
    2173
    1
        static COLLAPSE_SPACES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r" +").unwrap());
    2174
        // debug!("LaTeX_cleanup: start={}", raw_braille);
    2175
    50
        let result = raw_braille.replace('𝐖', " ");
    2176
        // let result = COLLAPSE_SPACES.replace_all(&raw_braille, "⠀");
    2177
    50
        let result = COLLAPSE_SPACES.replace_all(&result, " ");
    2178
        // debug!("After collapse: {}", &result);
    2179
    50
        let result = REMOVE_SPACE.replace_all(&result, "$1");
    2180
        // debug!("After remove: {}", &result);
    2181
        // let result = result.trim_matches('⠀');
    2182
    50
        let result = result.trim_matches(' ');
    2183
    2184
    50
        return result.to_string();
    2185
    50
    }
    2186
    2187
    #[allow(non_snake_case)]
    2188
    41
    fn ASCIIMath_cleanup(_pref_manager: Ref<PreferenceManager>, raw_braille: String) -> String {
    2189
    1
        static REMOVE_SPACE_BEFORE_OP: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"([\w\d]) +([^\w\d"]|[\^_,;)\]}])"#).unwrap());
    2190
    1
        static REMOVE_SPACE_AFTER_OP: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"([^\^_,;)\]}\w\d"]) +([\w\d])"#).unwrap());
    2191
    1
        static COLLAPSE_SPACES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r" +").unwrap());
    2192
        // debug!("ASCIIMath_cleanup: start={}", raw_braille);
    2193
    41
        let result  = raw_braille.replace("|𝐖__|", "|𝐰__|");    // protect the whitespace to prevent misinterpretation as lfloor
    2194
    41
        let result = result.replace('𝐖', " ");
    2195
    41
        let result = COLLAPSE_SPACES.replace_all(&result, " ");
    2196
        // debug!("After collapse: {}", &result);
    2197
    41
        let result = REMOVE_SPACE_BEFORE_OP.replace_all(&result, "$1$2");
    2198
    41
        let result = REMOVE_SPACE_AFTER_OP.replace_all(&result, "$1$2");
    2199
    41
        let result = result.replace('𝐰', " ");     // spaces around relational operators
    2200
    41
        let result = COLLAPSE_SPACES.replace_all(&result, " ");
    2201
        // debug!("After remove: {}", &result);
    2202
        // let result = result.trim_matches('⠀');
    2203
    41
        let result = result.trim_matches(' ');
    2204
    2205
    41
        return result.to_string();
    2206
    41
    }
    2207
    2208
    2209
    /************** Braille xpath functionality ***************/
    2210
    use crate::canonicalize::{as_element, as_text, name};
    2211
    use crate::xpath_functions::{is_leaf, validate_one_node, IsBracketed};
    2212
    use std::result::Result as StdResult;
    2213
    use sxd_document::dom::ParentOfChild;
    2214
    use sxd_xpath::function::Error as XPathError;
    2215
    use sxd_xpath::function::{Args, Function};
    2216
    use sxd_xpath::{context, nodeset::*, Value};
    2217
    2218
    pub struct NemethNestingChars;
    2219
    const NEMETH_FRAC_LEVEL: &str = "data-nemeth-frac-level";    // name of attr where value is cached
    2220
    const FIRST_CHILD_ONLY: &[&str] = &["mroot", "msub", "msup", "msubsup", "munder", "mover", "munderover", "mmultiscripts"];
    2221
    impl NemethNestingChars {
    2222
        // returns a 'repeat_char' corresponding to the Nemeth rules for nesting
    2223
        // note: this value is likely one char too long because the starting fraction is counted
    2224
    537
        fn nemeth_frac_value(node: Element, repeat_char: &str) -> String {
    2225
    537
            let children = node.children();
    2226
    537
            let name = name(node);
    2227
    537
            if is_leaf(node) {
    2228
    244
                return "".to_string();
    2229
    293
            } else if name == "mfrac" {
    2230
                // have we already computed the value?
    2231
    221
                if let Some(value152) = node.attribute_value(NEMETH_FRAC_LEVEL) {
    2232
    152
                    return value.to_string();
    2233
    69
                }
    2234
    2235
    69
                let num_value = NemethNestingChars::nemeth_frac_value(as_element(children[0]), repeat_char);
    2236
    69
                let denom_value = NemethNestingChars::nemeth_frac_value(as_element(children[1]), repeat_char);
    2237
    69
                let mut max_value = if num_value.len() > denom_value.len() {num_value8} else {denom_value61};
    2238
    69
                max_value += repeat_char;
    2239
    69
                node.set_attribute_value(NEMETH_FRAC_LEVEL, &max_value);
    2240
    69
                return max_value;
    2241
    72
            } else if FIRST_CHILD_ONLY.contains(&name) {
    2242
                // only look at the base -- ignore scripts/index
    2243
    10
                return NemethNestingChars::nemeth_frac_value(as_element(children[0]), repeat_char);
    2244
            } else {
    2245
    62
                let mut result = "".to_string();
    2246
    197
                for child in children62 {
    2247
    197
                    let value = NemethNestingChars::nemeth_frac_value(as_element(child), repeat_char);
    2248
    197
                    if value.len() > result.len() {
    2249
    19
                        result = value;
    2250
    178
                    }
    2251
                }
    2252
    62
                return result;
    2253
            }
    2254
    537
        }
    2255
    2256
    0
        fn nemeth_root_value(node: Element, repeat_char: &str) -> StdResult<String, XPathError> {
    2257
            // returns the correct number of repeat_chars to use
    2258
            // note: because the highest count is toward the leaves and
    2259
            //    because this is a loop and not recursive, caching doesn't work without a lot of overhead
    2260
    0
            let parent = node.parent().unwrap();
    2261
    0
            if let ParentOfChild::Element(e) =  parent {
    2262
    0
                let mut parent = e;
    2263
    0
                let mut result = "".to_string();
    2264
                loop {
    2265
    0
                    let name = name(parent);
    2266
    0
                    if name == "math" {
    2267
    0
                        return Ok( result );
    2268
    0
                    }
    2269
    0
                    if name == "msqrt" || name == "mroot" {
    2270
    0
                        result += repeat_char;
    2271
    0
                    }
    2272
    0
                    let parent_of_child = parent.parent().unwrap();
    2273
    0
                    if let ParentOfChild::Element(e) =  parent_of_child {
    2274
    0
                        parent = e;
    2275
    0
                    } else {
    2276
    0
                        return Err( sxd_xpath::function::Error::Other("Internal error in nemeth_root_value: didn't find 'math' tag".to_string()) );
    2277
                    }
    2278
                }
    2279
    0
            }
    2280
    0
            return Err( XPathError::Other("Internal error in nemeth_root_value: didn't find 'math' tag".to_string()) );
    2281
    0
        }
    2282
    }
    2283
    2284
    impl Function for NemethNestingChars {
    2285
    /**
    2286
     * Returns a string with the correct number of nesting chars (could be an empty string)
    2287
     * @param(node) -- current node
    2288
     * @param(char) -- char (string) that should be repeated
    2289
     * Note: as a side effect, an attribute with the value so repeated calls to this or a child will be fast
    2290
     */
    2291
    192
     fn evaluate<'d>(&self,
    2292
    192
                            _context: &context::Evaluation<'_, 'd>,
    2293
    192
                            args: Vec<Value<'d>>)
    2294
    192
                            -> StdResult<Value<'d>, XPathError>
    2295
        {
    2296
    192
            let mut args = Args(args);
    2297
    192
            args.exactly(2)?0;
    2298
    192
            let repeat_char = args.pop_string()?0;
    2299
    192
            let node = crate::xpath_functions::validate_one_node(args.pop_nodeset()?0, "NestingChars")?0;
    2300
    192
            if let Node::Element(el) = node {
    2301
    192
                let name = name(el);
    2302
                // it is likely a bug to call this one a non mfrac
    2303
    192
                if name == "mfrac" {
    2304
                    // because it is called on itself, the fraction is counted one too many times -- chop one off
    2305
                    // this is slightly messy because we are chopping off a char, not a byte
    2306
                    const BRAILLE_BYTE_LEN: usize = "⠹".len();      // all Unicode braille symbols have the same number of bytes
    2307
    192
                    return Ok( Value::String( NemethNestingChars::nemeth_frac_value(el, &repeat_char)[BRAILLE_BYTE_LEN..].to_string() ) );
    2308
    0
                } else if name == "msqrt" || name == "mroot" {
    2309
    0
                    return Ok( Value::String( NemethNestingChars::nemeth_root_value(el, &repeat_char)? ) );
    2310
                } else {
    2311
    0
                    return Err(XPathError::Other(format!("NestingChars chars should be used only on 'mfrac'. '{}' was passed in", name)));
    2312
                }
    2313
            } else {
    2314
                // not an element, so nothing to do
    2315
    0
                return Ok( Value::String("".to_string()) );
    2316
            }
    2317
    192
        }
    2318
    }
    2319
    2320
    pub struct BrailleChars;
    2321
    impl BrailleChars {
    2322
        // returns a string for the chars in the *leaf* node.
    2323
        // this string follows the Nemeth rules typefaces and deals with mathvariant
    2324
        //  which has partially turned chars to the alphanumeric block
    2325
    12.5k
        fn get_braille_chars(node: Element, code: &str, text_range: Option<Range<usize>>) -> StdResult<String, XPathError> {
    2326
    12.5k
            let result = match code {
    2327
    12.5k
                "Nemeth" => BrailleChars::get_braille_nemeth_chars5.99k(node5.99k, text_range5.99k),
    2328
    6.52k
                "UEB" => BrailleChars:: get_braille_ueb_chars2.28k(node2.28k, text_range2.28k),
    2329
    4.24k
                "CMU" => BrailleChars:: get_braille_cmu_chars3.70k(node3.70k, text_range3.70k),
    2330
    536
                "Vietnam" => BrailleChars:: get_braille_vietnam_chars(node, text_range),
    2331
    0
                "Swedish" => BrailleChars:: get_braille_ueb_chars(node, text_range),    // FIX: need to figure out what to implement
    2332
    0
                "Finnish" => BrailleChars:: get_braille_ueb_chars(node, text_range),    // FIX: need to figure out what to implement
    2333
    0
                _ => return Err(sxd_xpath::function::Error::Other(format!("get_braille_chars: unknown braille code '{code}'")))
    2334
            };
    2335
    12.5k
            return match result {
    2336
    12.5k
                Ok(string) => Ok(make_quoted_string(string)),
    2337
    0
                Err(err) => return Err(sxd_xpath::function::Error::Other(err.to_string())),
    2338
            }
    2339
    12.5k
        }
    2340
    2341
    5.99k
        fn get_braille_nemeth_chars(node: Element, text_range: Option<Range<usize>>) -> Result<String> {
    2342
            // To greatly simplify typeface/language generation, the chars have unique ASCII chars for them:
    2343
            // Typeface: S: sans-serif, B: bold, 𝔹: blackboard, T: script, I: italic, R: Roman
    2344
            // Language: E: English, D: German, G: Greek, V: Greek variants, H: Hebrew, U: Russian
    2345
            // Indicators: C: capital, L: letter, N: number, P: punctuation, M: multipurpose
    2346
    2
            static PICK_APART_CHAR: LazyLock<Regex> = LazyLock::new(|| {
    2347
    2
                Regex::new(r"(?P<face>[SB𝔹TIR]*)(?P<lang>[EDGVHU]?)(?P<cap>C?)(?P<letter>L?)(?P<num>[N]?)(?P<char>.)").unwrap()
    2348
    2
            });
    2349
    5.99k
            let math_variant = node.attribute_value("mathvariant");
    2350
            // FIX: cover all the options -- use phf::Map
    2351
    5.99k
            let  attr_typeface = match math_variant {
    2352
    5.76k
                None => "R",
    2353
    233
                Some(variant) => match variant {
    2354
    233
                    "bold" => "B"42,
    2355
    191
                    "italic" => "I"2,
    2356
    189
                    "double-struck" => "𝔹"27,
    2357
    162
                    "script" => "T"5,
    2358
    157
                    "fraktur" => "D"0,
    2359
    157
                    "sans-serif" => "S"1,
    2360
    156
                    _ => "R",       // normal and unknown
    2361
                },
    2362
            };
    2363
    5.99k
            let text = BrailleChars::substring(as_text(node), &text_range);
    2364
    5.99k
            let braille_chars = braille_replace_chars(&text, node)?0;
    2365
            // debug!("Nemeth chars: text='{}', braille_chars='{}'", &text, &braille_chars);
    2366
    2367
            // we want to pull the prefix (typeface, language) out to the front until a change happens
    2368
            // the same is true for number indicator
    2369
            // also true (sort of) for capitalization -- if all caps, use double cap in front (assume abbr or Roman Numeral)
    2370
    2371
            // we only care about this for numbers and identifiers/text, so we filter for only those
    2372
    5.99k
            let node_name = name(node);
    2373
    5.99k
            let is_in_enclosed_list = node_name != "mo" && BrailleChars::is_in_enclosed_list3.45k(node3.45k);
    2374
    5.99k
            let is_mn_in_enclosed_list = is_in_enclosed_list && node_name == "mn"120;
    2375
    5.99k
            let mut typeface = "R".to_string();     // assumption is "R" and if attr or letter is different, something happens
    2376
    5.99k
            let mut is_all_caps = true;
    2377
    5.99k
            let mut is_all_caps_valid = false;      // all_caps only valid if we did a replacement
    2378
    7.87k
            let result5.99k = PICK_APART_CHAR5.99k.replace_all5.99k(&braille_chars5.99k, |caps: &Captures| {
    2379
                // debug!("  face: {:?}, lang: {:?}, num {:?}, letter: {:?}, cap: {:?}, char: {:?}",
    2380
                //        &caps["face"], &caps["lang"], &caps["num"], &caps["letter"], &caps["cap"], &caps["char"]);
    2381
    7.87k
                let mut nemeth_chars = "".to_string();
    2382
    7.87k
                let char_face = if caps["face"].is_empty() {attr_typeface7.78k} else {&caps["face"]86};
    2383
    7.87k
                let typeface_changed =  typeface != char_face;
    2384
    7.87k
                if typeface_changed {
    2385
    86
                    typeface = char_face.to_string();   // needs to outlast this instance of the loop
    2386
    86
                    nemeth_chars += &typeface;
    2387
    86
                    nemeth_chars +=  &caps["lang"];
    2388
    7.78k
                } else {
    2389
    7.78k
                    nemeth_chars +=  &caps["lang"];
    2390
    7.78k
                }
    2391
                // debug!("  typeface changed: {}, is_in_list: {}; num: {}", typeface_changed, is_in_enclosed_list, !caps["num"].is_empty());
    2392
    7.87k
                if !caps["num"].is_empty() && (typeface_changed2.74k || !is_mn_in_enclosed_list2.72k) {
    2393
    2.58k
                    nemeth_chars += "N";
    2394
    5.28k
                }
    2395
    7.87k
                is_all_caps_valid = true;
    2396
    7.87k
                is_all_caps &= !&caps["cap"].is_empty();
    2397
    7.87k
                nemeth_chars += &caps["cap"];       // will be stripped later if all caps
    2398
    7.87k
                if is_in_enclosed_list {
    2399
    228
                    nemeth_chars += &caps["letter"].replace('L', "l");
    2400
    7.64k
                } else {
    2401
    7.64k
                    nemeth_chars += &caps["letter"];
    2402
    7.64k
                }
    2403
    7.87k
                nemeth_chars += &caps["char"];
    2404
    7.87k
                return nemeth_chars;
    2405
    7.87k
            });
    2406
            // debug!("  result: {}", &result);
    2407
    5.99k
            let mut text_chars = text.chars();     // see if more than one char
    2408
    5.99k
            if is_all_caps_valid && is_all_caps5.22k && text_chars.next()369.is_some369() &&  text_chars.next()369.is_some369() {
    2409
    7
                return Ok( "CC".to_string() + &result.replace('C', ""));
    2410
            } else {
    2411
    5.98k
                return Ok( result.to_string() );
    2412
            }
    2413
    5.99k
        }
    2414
    2415
    2.82k
        fn get_braille_ueb_chars(node: Element, text_range: Option<Range<usize>>) -> Result<String> {
    2416
            // Because in UEB typeforms and caps may extend for multiple tokens,
    2417
            //   this routine merely deals with the mathvariant attr.
    2418
            // Canonicalize has already transformed all chars it can to math alphanumerics, but not all have bold/italic
    2419
            // The typeform/caps transforms to (potentially) word mode are handled later.
    2420
    1
            static HAS_TYPEFACE: LazyLock<Regex> = LazyLock::new(|| Regex::new(".*?(double-struck|script|fraktur|sans-serif).*").unwrap());
    2421
    1
            static PICK_APART_CHAR: LazyLock<Regex> = LazyLock::new(|| {
    2422
    1
                Regex::new(r"(?P<bold>B??)(?P<italic>I??)(?P<face>[S𝔹TD]??)s??(?P<cap>C??)(?P<greek>G??)(?P<char>[NL].)").unwrap()
    2423
    1
            });
    2424
    2425
    2.82k
            let math_variant = node.attribute_value("mathvariant");
    2426
    2.82k
            let text = BrailleChars::substring(as_text(node), &text_range);
    2427
    2.82k
            let mut braille_chars = braille_replace_chars(&text, node)?0;
    2428
    2429
            // debug!("get_braille_ueb_chars: before/after unicode.yaml: '{}'/'{}'", text, braille_chars);
    2430
    2.82k
            if math_variant.is_none() {         // nothing we need to do
    2431
    2.71k
                return Ok(braille_chars);
    2432
    108
            }
    2433
            // mathvariant could be "sans-serif-bold-italic" -- get the parts
    2434
    108
            let math_variant = math_variant.unwrap();
    2435
    108
            let italic = math_variant.contains("italic");
    2436
    108
            if italic & !braille_chars.contains('I') {
    2437
    0
                braille_chars = "I".to_string() + &braille_chars;
    2438
    108
            }
    2439
    108
            let bold = math_variant.contains("bold");
    2440
    108
            if bold & !braille_chars.contains('B') {
    2441
    0
                braille_chars = "B".to_string() + &braille_chars;
    2442
    108
            }
    2443
    108
            let typeface = match HAS_TYPEFACE.find(math_variant) {
    2444
    107
                None => "",
    2445
    1
                Some(m) => match m.as_str() {
    2446
    1
                    "double-struck" => "𝔹"0,
    2447
    1
                    "script" => "T"0,
    2448
    1
                    "fraktur" => "D",
    2449
    0
                    "sans-serif" => "S",
    2450
                    //  don't consider monospace as a typeform
    2451
    0
                    _ => "",
    2452
                },
    2453
            };
    2454
    116
            let result108 = PICK_APART_CHAR108.replace_all108(&braille_chars108, |caps: &Captures| {
    2455
                // debug!("captures: {:?}", caps);
    2456
                // debug!("  bold: {:?}, italic: {:?}, face: {:?}, cap: {:?}, char: {:?}",
    2457
                //        &caps["bold"], &caps["italic"], &caps["face"], &caps["cap"], &caps["char"]);
    2458
    116
                if bold || !caps["bold"].is_empty()111 {"B"5} else {""111}.to_string()
    2459
    116
                    + if italic || !caps["italic"].is_empty() {"I"0} else {""}
    2460
    116
                    + if !&caps["face"].is_empty() {&caps["face"]1} else {typeface115}
    2461
    116
                    + &caps["cap"]
    2462
    116
                    + &caps["greek"]
    2463
    116
                    + &caps["char"]
    2464
    116
            });
    2465
            // debug!("get_braille_ueb_chars: '{}'", &result);
    2466
    108
            return Ok(result.to_string())
    2467
    2.82k
        }
    2468
    2469
    3.70k
        fn get_braille_cmu_chars(node: Element, text_range: Option<Range<usize>>) -> Result<String> {
    2470
            // In CMU, we need to replace spaces used for number blocks with "."
    2471
            // For other numbers, we need to add "." to create digit blocks
    2472
    2473
    1
            static HAS_TYPEFACE: LazyLock<Regex> = LazyLock::new(|| Regex::new(".*?(double-struck|script|fraktur|sans-serif).*").unwrap());
    2474
    1
            static PICK_APART_CHAR: LazyLock<Regex> = LazyLock::new(|| {
    2475
    1
                Regex::new(r"(?P<bold>B??)(?P<italic>I??)(?P<face>[S𝔹TD]??)s??(?P<cap>C??)(?P<greek>G??)(?P<char>[NL].)").unwrap()
    2476
    1
            });
    2477
    2478
    3.70k
            let math_variant = node.attribute_value("mathvariant");
    2479
    3.70k
            let text = BrailleChars::substring(as_text(node), &text_range);
    2480
    3.70k
            let text = add_separator(text);
    2481
    2482
    3.70k
            let braille_chars = braille_replace_chars(&text, node)?0;
    2483
    2484
            // debug!("get_braille_ueb_chars: before/after unicode.yaml: '{}'/'{}'", text, braille_chars);
    2485
    3.70k
            if math_variant.is_none() {         // nothing we need to do
    2486
    3.70k
                return Ok(braille_chars);
    2487
    4
            }
    2488
            // mathvariant could be "sans-serif-bold-italic" -- get the parts
    2489
    4
            let math_variant = math_variant.unwrap();
    2490
    4
            let bold = math_variant.contains("bold");
    2491
    4
            let italic = math_variant.contains("italic");
    2492
    4
            let typeface = match HAS_TYPEFACE.find(math_variant) {
    2493
    4
                None => "",
    2494
    0
                Some(m) => match m.as_str() {
    2495
    0
                    "double-struck" => "𝔹",
    2496
    0
                    "script" => "T",
    2497
    0
                    "fraktur" => "D",
    2498
    0
                    "sans-serif" => "S",
    2499
                    //  don't consider monospace as a typeform
    2500
    0
                    _ => "",
    2501
                },
    2502
            };
    2503
    4
            let result = PICK_APART_CHAR.replace_all(&braille_chars, |caps: &Captures| {
    2504
                // debug!("captures: {:?}", caps);
    2505
                // debug!("  bold: {:?}, italic: {:?}, face: {:?}, cap: {:?}, char: {:?}",
    2506
                //        &caps["bold"], &caps["italic"], &caps["face"], &caps["cap"], &caps["char"]);
    2507
    4
                if bold || !caps["bold"].is_empty() {"B"0} else {""}.to_string()
    2508
    4
                    + if italic || !caps["italic"].is_empty() {"I"0} else {""}
    2509
    4
                    + if !&caps["face"].is_empty() {&caps["face"]0} else {typeface}
    2510
    4
                    + &caps["cap"]
    2511
    4
                    + &caps["greek"]
    2512
    4
                    + &caps["char"]
    2513
    4
            });
    2514
    4
            return Ok(result.to_string());
    2515
    2516
    3.70k
            fn add_separator(text: String) -> String {
    2517
                use crate::definitions::BRAILLE_DEFINITIONS;
    2518
    3.70k
                if let Some(text_without_arc0) = text.strip_prefix("arc") {
    2519
                    // "." after arc (7.5.3)
    2520
    0
                    let is_function_name = BRAILLE_DEFINITIONS.with(|definitions| {
    2521
    0
                        let definitions = definitions.borrow();
    2522
    0
                        let set = definitions.get_hashset("CMUFunctionNames").unwrap();
    2523
    0
                        return set.contains(&text);
    2524
    0
                    });
    2525
    0
                    if is_function_name {
    2526
    0
                        return "arc.".to_string() + text_without_arc;
    2527
    0
                    }
    2528
    3.70k
                }
    2529
    3.70k
                return text;
    2530
    3.70k
            }
    2531
    3.70k
        }
    2532
    2533
    536
        fn get_braille_vietnam_chars(node: Element, text_range: Option<Range<usize>>) -> Result<String> {
    2534
            // this is basically the same as for ueb except:
    2535
            // 1. we deal with switching '.' and ',' if in English style for numbers
    2536
            // 2. if it is identified as a Roman Numeral, we make all but the first char lower case because they shouldn't get a cap indicator
    2537
            // 3. double letter chemical elements should NOT be part of a cap word sequence
    2538
    536
            if name(node) == "mn" {
    2539
    248
                // text of element is modified by these if needed
    2540
    248
                lower_case_roman_numerals(node);
    2541
    248
                switch_if_english_style_number(node);
    2542
    288
            }
    2543
    536
            let result = BrailleChars::get_braille_ueb_chars(node, text_range)?0;
    2544
    536
            return Ok(result);
    2545
    2546
    248
            fn lower_case_roman_numerals(mn_node: Element) {
    2547
    248
                if mn_node.attribute("data-roman-numeral").is_some() {
    2548
    2
                    // if a roman numeral, all ASCII so we can optimize
    2549
    2
                    let text = as_text(mn_node);
    2550
    2
                    let mut new_text = String::from(&text[..1]);
    2551
    2
                    new_text.push_str(text[1..].to_ascii_lowercase().as_str());    // works for single char too
    2552
    2
                    mn_node.set_text(&new_text);
    2553
    246
                }
    2554
    248
            }
    2555
    248
            fn switch_if_english_style_number(mn_node: Element) {
    2556
    248
                let text = as_text(mn_node);
    2557
    248
                let dot = text.find('.');
    2558
    248
                let comma = text.find(',');
    2559
    248
                match (dot, comma) {
    2560
    218
                    (None, None) => (),
    2561
    4
                    (Some(dot), Some(comma)) => {
    2562
    4
                        if comma < dot {
    2563
    2
                            // switch dot/comma -- using "\x01" as a temp when switching the two chars
    2564
    2
                            let switched = text.replace('.', "\x01").replace(',', ".").replace('\x01', ",");
    2565
    2
                            mn_node.set_text(&switched);
    2566
    2
                        }
    2567
                    },
    2568
    17
                    (Some(dot), None) => {
    2569
                        // If it starts with a '.', a leading 0, or if there is only one '.' and not three chars after it
    2570
    17
                        if dot==0 ||
    2571
    15
                           (dot==1 && text11.starts_with11('0')) ||
    2572
    13
                           (text[dot+1..].find('.').is_none() && text[dot+1..].len()!=310) {
    2573
    5
                            mn_node.set_text(&text.replace('.', ","));
    2574
    12
                        }
    2575
                    },
    2576
    9
                    (None, Some(comma)) => {
    2577
                        // if there is more than one ",", than it can't be a decimal separator
    2578
    9
                        if text[comma+1..].find(',').is_some() {
    2579
    1
                            mn_node.set_text(&text.replace(',', "."));
    2580
    8
                        }
    2581
                    },
    2582
                }
    2583
    248
            }
    2584
    2585
    536
        }
    2586
    2587
    2588
    3.45k
        fn is_in_enclosed_list(node: Element) -> bool {
    2589
            // Nemeth Rule 10 defines an enclosed list:
    2590
            // 1: begins and ends with fence
    2591
            // 2: FIX: not implemented -- must contain no word, abbreviation, ordinal or plural ending
    2592
            // 3: function names or signs of shape and the signs which follow them are a single item (not a word)
    2593
            // 4: an item of the list may be an ellipsis or any sign used for omission
    2594
            // 5: no relational operator may appear within the list
    2595
            // 6: the list must have at least 2 items.
    2596
            //       Items are separated by commas, can not have other punctuation (except ellipsis and dash)
    2597
    3.45k
            let mut parent = get_parent(node); // safe since 'math' is always at root
    2598
    7.37k
            while name(parent) == "mrow" {
    2599
    4.04k
                if IsBracketed::is_bracketed(parent, "", "", true, false) {
    2600
    388
                    for child in parent134.children134() {
    2601
    388
                        if !child_meets_conditions(as_element(child)) {
    2602
    14
                            return false;
    2603
    374
                        }
    2604
                    }
    2605
    120
                    return true;
    2606
    3.91k
                }
    2607
    3.91k
                parent = get_parent(parent);
    2608
            }
    2609
    3.32k
            return false;
    2610
    2611
    1.55k
            fn child_meets_conditions(node: Element) -> bool {
    2612
    1.55k
                let name = name(node);
    2613
    1.55k
                return match name {
    2614
    1.55k
                    "mi" | "mn"1.39k => true476,
    2615
    1.07k
                    "mo"  => !crate::canonicalize::is_relational_op(node)664,
    2616
    412
                    "mtext" => {
    2617
    9
                        let text = as_text(node).trim();
    2618
    9
                        return text=="?" || text=="-?-" || text.is_empty();   // various forms of "fill in missing content" (see also Nemeth_RULEs.yaml, "omissions")
    2619
                    },
    2620
    403
                    "mrow" => {
    2621
    385
                        if IsBracketed::is_bracketed(node, "", "", false, false) {
    2622
    125
                            return child_meets_conditions(as_element(node.children()[1]));
    2623
                        } else {
    2624
    1.00k
                            for child in node260.children260() {
    2625
    1.00k
                                if !child_meets_conditions(as_element(child)) {
    2626
    28
                                    return false;
    2627
    975
                                }
    2628
                            }
    2629
                        }
    2630
    232
                        true
    2631
                    },
    2632
    18
                    "menclose" => {
    2633
    0
                        if let Some(notation) = node.attribute_value("notation") {
    2634
    0
                            if notation != "bottom" || notation != "box" {
    2635
    0
                                return false;
    2636
    0
                            }
    2637
    0
                            let child = as_element(node.children()[0]);     // menclose has exactly one child
    2638
    0
                            return is_leaf(child) && as_text(child) == "?";
    2639
    0
                        }
    2640
    0
                        return false;
    2641
                    },
    2642
                    _ => {
    2643
    36
                        for child in node18.children18() {
    2644
    36
                            if !child_meets_conditions(as_element(child)) {
    2645
    0
                                return false;
    2646
    36
                            }
    2647
                        }
    2648
    18
                        true
    2649
                    },
    2650
                }
    2651
    1.55k
            }
    2652
    3.45k
        }
    2653
    2654
        /// Extract the `char`s from `str` within `range` (these are chars, not byte offsets)
    2655
    12.5k
        fn substring(str: &str, text_range: &Option<Range<usize>>) -> String {
    2656
    12.5k
            return match text_range {
    2657
    9.99k
                None => str.to_string(),
    2658
    2.52k
                Some(range) => str.chars().skip(range.start).take(range.end - range.start).collect(),
    2659
            }
    2660
    12.5k
        }
    2661
    }
    2662
    2663
    impl Function for BrailleChars {
    2664
        /**
    2665
         * Returns a string with the correct number of nesting chars (could be an empty string)
    2666
         * @param(node) -- current node or string
    2667
         * @param(char) -- char (string) that should be repeated
    2668
         * Note: as a side effect, an attribute with the value so repeated calls to this or a child will be fast
    2669
         */
    2670
    12.5k
        fn evaluate<'d>(&self,
    2671
    12.5k
                            context: &context::Evaluation<'_, 'd>,
    2672
    12.5k
                            args: Vec<Value<'d>>)
    2673
    12.5k
                            -> StdResult<Value<'d>, XPathError>
    2674
        {
    2675
            use crate::canonicalize::create_mathml_element;
    2676
    12.5k
            let mut args = Args(args);
    2677
    12.5k
            if let Err(e0) = args.exactly(2).or_else(|_| args2.52k.exactly2.52k(4)) {
    2678
    0
                return Err( XPathError::Other(format!("BrailleChars requires 2 or 4 args: {e}")));
    2679
    12.5k
            };
    2680
    2681
    12.5k
            let range = if args.len() == 4 {
    2682
    2.52k
                let end = args.pop_number()?0 as usize - 1;      // non-inclusive at end, 0-based
    2683
    2.52k
                let start = args.pop_number()?0 as usize - 1;    // inclusive at start, a 0-based
    2684
    2.52k
                Some(start..end)
    2685
            } else {
    2686
    9.99k
                None
    2687
            };
    2688
    12.5k
            let braille_code = args.pop_string()?0;
    2689
    12.5k
            let v: Value<'_> = args.0.pop().ok_or(XPathError::ArgumentMissing)?0;
    2690
    12.5k
            let node = match v {
    2691
    11.8k
                Value::Nodeset(nodes) => {
    2692
    11.8k
                    validate_one_node(nodes, "BrailleChars")?0.element().unwrap()
    2693
                },
    2694
    2
                Value::Number(n) => {
    2695
    2
                    let new_node = create_mathml_element(&context.node.document(), "mn");
    2696
    2
                    new_node.set_text(&n.to_string());
    2697
    2
                    new_node
    2698
                },
    2699
    681
                Value::String(s) => {
    2700
    681
                    let new_node = create_mathml_element(&context.node.document(), "mi");   // FIX: try to guess mi vs mo???
    2701
    681
                    new_node.set_text(&s);
    2702
    681
                    new_node
    2703
                },
    2704
                _ => {
    2705
    0
                    return Ok( Value::String("".to_string()) ) // not an element, so nothing to do
    2706
                },
    2707
            };
    2708
    2709
    12.5k
            if !is_leaf(node) {
    2710
    0
                return Err( XPathError::Other(format!("BrailleChars called on non-leaf element '{}'", mml_to_string(node))) );
    2711
    12.5k
            }
    2712
    12.5k
            return Ok( Value::String( BrailleChars::get_braille_chars(node, &braille_code, range)?0 ) );
    2713
    12.5k
        }
    2714
    }
    2715
    2716
    pub struct NeedsToBeGrouped;
    2717
    impl NeedsToBeGrouped {
    2718
        // ordinals often have an irregular start (e.g., "half") before becoming regular.
    2719
        // if the number is irregular, return the ordinal form, otherwise return 'None'.
    2720
    805
        fn needs_grouping_for_cmu(element: Element, _is_base: bool) -> bool {
    2721
    805
            let node_name = name(element);
    2722
    805
            let children = element.children();
    2723
    805
            if node_name == "mrow" {
    2724
                // check for bracketed exprs
    2725
    544
                if IsBracketed::is_bracketed(element, "", "", false, true) {
    2726
    0
                    return false;
    2727
    544
                }
    2728
    2729
                // check for prefix and postfix ops at start or end (=> len()==2, prefix is first op, postfix is last op)
    2730
    544
                if children.len() == 2 &&
    2731
    9
                    (name(as_element(children[0])) == "mo" || name5(as_element5(children[1])) == "mo") {
    2732
    7
                    return false;
    2733
    537
                }
    2734
    2735
    537
                if children.len() != 3 {  // ==3, need to check if it a linear fraction
    2736
    4
                    return true;
    2737
    533
                }
    2738
    533
                let operator = as_element(children[1]);
    2739
    533
                if name(operator) != "mo" || as_text(operator) != "/" {
    2740
    532
                    return true;
    2741
    1
                }
    2742
    261
            }
    2743
    2744
    262
            if !(node_name == "mrow" || node_name == "mfrac"261) {
    2745
    258
                return false;
    2746
    4
            }
    2747
            // check for numeric fractions (regular fractions need brackets, not numeric fractions), either as an mfrac or with "/"
    2748
            // if the fraction starts with a "-", it is still a numeric fraction that doesn't need parens
    2749
    4
            let mut numerator = as_element(children[0]);
    2750
    4
            let denominator = as_element(children[children.len()-1]);
    2751
    4
            let decimal_separator = crate::interface::get_preference("DecimalSeparators").unwrap()
    2752
    4
                                                            .chars().next().unwrap_or('.');
    2753
    4
            if is_integer(denominator, decimal_separator) {
    2754
                // check numerator being either an integer "- integer"
    2755
    2
                if name(numerator) == "mrow" {
    2756
    1
                    let numerator_children = numerator.children();
    2757
    1
                    if !(numerator_children.len() == 2 &&
    2758
    1
                            name(as_element(numerator_children[0])) == "mo" &&
    2759
    1
                            as_text(as_element(numerator_children[0])) == "-") {
    2760
    0
                        return true;
    2761
    1
                    }
    2762
    1
                    numerator = as_element(numerator_children[1]);
    2763
    1
                }
    2764
    2
                return !is_integer(numerator, decimal_separator);
    2765
    2
            }
    2766
    2
            return true;
    2767
    2768
    6
            fn is_integer(mathml: Element, decimal_separator: char) -> bool {
    2769
    6
                return name(mathml) == "mn" && !4as_text(mathml)4.contains(decimal_separator)
    2770
    6
            }
    2771
    805
        }
    2772
    2773
        /// FIX: what needs to be implemented?
    2774
    0
        fn needs_grouping_for_finnish(mathml: Element, is_base: bool) -> bool {
    2775
            use crate::xpath_functions::IsInDefinition;
    2776
    0
            let mut node_name = name(mathml);
    2777
    0
            if mathml.attribute_value("data-roman-numeral").is_some() {
    2778
    0
                node_name = "mi";           // roman numerals don't follow number rules
    2779
    0
            }
    2780
    2781
            // FIX: the leaf rules are from UEB -- check the Swedish rules
    2782
    0
            match node_name {
    2783
    0
                "mn" => {
    2784
    0
                    if !is_base {
    2785
    0
                        return false;
    2786
    0
                    }                                                                                        // clause 1
    2787
                    // two 'mn's can be adjacent, in which case we need to group the 'mn' to make it clear it is separate (see bug #204)
    2788
    0
                    let parent = get_parent(mathml);   // there is always a "math" node
    2789
    0
                    let grandparent = if name(parent) == "math" {parent} else {get_parent(parent)};
    2790
    0
                    if name(grandparent) != "mrow" {
    2791
    0
                        return false;
    2792
    0
                    }
    2793
    0
                    let preceding = parent.preceding_siblings();
    2794
    0
                    if preceding.len()  < 2 {
    2795
    0
                        return false;
    2796
    0
                    }
    2797
                    // any 'mn' would be separated from this node by invisible times
    2798
    0
                    let previous_child = as_element(preceding[preceding.len()-1]);
    2799
    0
                    if name(previous_child) == "mo" && as_text(previous_child) == "\u{2062}" {
    2800
    0
                        let previous_child = as_element(preceding[preceding.len()-2]);
    2801
    0
                        return name(previous_child) == "mn"
    2802
                    } else {
    2803
    0
                        return false;
    2804
                    }
    2805
                },
    2806
    0
                "mi" | "mo" | "mtext" => {
    2807
    0
                    let text = as_text(mathml);
    2808
    0
                    let parent = get_parent(mathml);   // there is always a "math" node
    2809
    0
                    let parent_name = name(parent);   // there is always a "math" node
    2810
    0
                    if is_base && (parent_name == "msub" || parent_name == "msup" || parent_name == "msubsup") && !text.contains([' ', '\u{00A0}']) {
    2811
    0
                        return false;
    2812
    0
                    }
    2813
    0
                    let mut chars = text.chars();
    2814
    0
                    let first_char = chars.next().unwrap();             // canonicalization assures it isn't empty;
    2815
    0
                    let is_one_char = chars.next().is_none();
    2816
                    // '¨', etc., brailles as two chars -- there probably is some exception list but I haven't found it -- these are the ones I know about
    2817
    0
                    return !((is_one_char && !['¨', '″', '‴', '⁗'].contains(&first_char)) ||                       // clause 8
    2818
                                // "lim", "cos", etc., appear not to get parens, but the rules don't mention it (tests show it)
    2819
    0
                                IsInDefinition::is_defined_in(text, &SPEECH_DEFINITIONS, "FunctionNames").unwrap() ||
    2820
    0
                                IsInDefinition::is_defined_in(text, &SPEECH_DEFINITIONS, "Arrows").unwrap() ||          // clause 4
    2821
    0
                                IsInDefinition::is_defined_in(text, &SPEECH_DEFINITIONS, "GeometryShapes").unwrap());   // clause 5
    2822
                },
    2823
    0
                "mrow" => {
    2824
                    // check for bracketed exprs
    2825
    0
                    if IsBracketed::is_bracketed(mathml, "", "", false, true) {
    2826
    0
                        return false;
    2827
    0
                    }
    2828
    2829
    0
                    let parent = get_parent(mathml); // safe since 'math' is always at root
    2830
    0
                    if name(parent) == "mfrac" {
    2831
    0
                        let children = mathml.children();
    2832
    0
                        if mathml.preceding_siblings().is_empty() {
    2833
                            // numerator: check for multiplication -- doesn't need grouping in numerator
    2834
    0
                            if children.len() >= 3 {
    2835
    0
                                let operator = as_element(children[1]);
    2836
    0
                                if name(operator) == "mo" {
    2837
    0
                                    let ch = as_text(operator);
    2838
    0
                                    if ch == "\u{2062}" || ch == "⋅" || ch == "×"  {
    2839
    0
                                        return false;
    2840
    0
                                    }
    2841
    0
                                }
    2842
    0
                            }
    2843
    0
                            return true;
    2844
                        } else {
    2845
                            // denominator
    2846
    0
                            return true;
    2847
                        }
    2848
    2849
    0
                    }
    2850
                    // check for prefix at start
    2851
                    // example 7.12 has "2-" in superscript and is grouped, so we don't consider postfix ops
    2852
    0
                    let children = mathml.children();
    2853
    0
                    if children.len() == 2 &&
    2854
    0
                        (name(as_element(children[0])) == "mo") {
    2855
    0
                        return false;
    2856
    0
                    }
    2857
    0
                    return true;
    2858
                },
    2859
    0
                _ => return false,
    2860
            }
    2861
    0
        }
    2862
    2863
        // ordinals often have an irregular start (e.g., "half") before becoming regular.
    2864
        // if the number is irregular, return the ordinal form, otherwise return 'None'.
    2865
    0
        fn needs_grouping_for_swedish(mathml: Element, is_base: bool) -> bool {
    2866
            use crate::xpath_functions::IsInDefinition;
    2867
    0
            let mut node_name = name(mathml);
    2868
    0
            if mathml.attribute_value("data-roman-numeral").is_some() {
    2869
    0
                node_name = "mi";           // roman numerals don't follow number rules
    2870
    0
            }
    2871
    2872
    0
            match node_name {
    2873
    0
                "mn" => return false,
    2874
    0
                "mi" | "mo" | "mtext" => {
    2875
    0
                    let text = as_text(mathml);
    2876
    0
                    let parent = get_parent(mathml);   // there is always a "math" node
    2877
    0
                    let parent_name = name(parent);   // there is always a "math" node
    2878
    0
                    if is_base && (parent_name == "msub" || parent_name == "msup" || parent_name == "msubsup") && !text.contains([' ', '\u{00A0}']) {
    2879
    0
                        return false;
    2880
    0
                    }
    2881
    0
                    let mut chars = text.chars();
    2882
    0
                    let first_char = chars.next().unwrap();             // canonicalization assures it isn't empty;
    2883
    0
                    let is_one_char = chars.next().is_none();
    2884
                    // '¨', etc., brailles as two chars -- there probably is some exception list but I haven't found it -- these are the ones I know about
    2885
    0
                    return !((is_one_char && !['¨', '″', '‴', '⁗'].contains(&first_char)) ||                       // clause 8
    2886
                                // "lim", "cos", etc., appear not to get parens, but the rules don't mention it (tests show it)
    2887
    0
                                IsInDefinition::is_defined_in(text, &SPEECH_DEFINITIONS, "FunctionNames").unwrap() ||
    2888
    0
                                IsInDefinition::is_defined_in(text, &SPEECH_DEFINITIONS, "Arrows").unwrap() ||          // clause 4
    2889
    0
                                IsInDefinition::is_defined_in(text, &SPEECH_DEFINITIONS, "GeometryShapes").unwrap());   // clause 5
    2890
                },
    2891
    0
                "mrow" => {
    2892
                    // check for bracketed exprs
    2893
    0
                    if IsBracketed::is_bracketed(mathml, "", "", false, true) {
    2894
    0
                        return false;
    2895
    0
                    }
    2896
    2897
                    // check for prefix at start
    2898
                    // example 7.12 has "2-" in superscript and is grouped, so we don't consider postfix ops
    2899
    0
                    let children = mathml.children();
    2900
    0
                    if children.len() == 2 &&
    2901
    0
                        (name(as_element(children[0])) == "mo") {
    2902
    0
                        return false;
    2903
    0
                    }
    2904
    0
                    return true;
    2905
                },
    2906
    0
                "mfrac" => {
    2907
                    // exclude simple fractions -- they are not bracketed with start/end marks
    2908
    0
                    let children = mathml.children();
    2909
    0
                    return !(NeedsToBeGrouped::needs_grouping_for_swedish(as_element(children[0]), true) ||
    2910
    0
                             NeedsToBeGrouped::needs_grouping_for_swedish(as_element(children[0]), true));
    2911
                },
    2912
                // At least for msup (Ex 7.7, and 7.32 and maybe more), spec seems to feel grouping is not needed.
    2913
                // "msub" | "msup" | "msubsup" | "munder" | "mover" | "munderover" => return true,
    2914
    0
                "mtable" => return true,    // Fix: should check for trivial cases that don't need grouping
    2915
    0
                _ => return false,
    2916
            }
    2917
    0
        }
    2918
    2919
        /// Returns true if the element needs grouping symbols
    2920
        /// Bases need extra attention because if they are a number and the item to the left is one, that needs distinguishing
    2921
    538
        fn needs_grouping_for_ueb(mathml: Element, is_base: bool) -> bool {
    2922
            // From GTM 7.1
    2923
            // 1. An entire number, i.e. the initiating numeric symbol and all succeeding symbols within the numeric mode thus
    2924
            //     established (which would include any interior decimal points, commas, separator spaces, or simple numeric fraction lines).
    2925
            // 2. An entire general fraction, enclosed in fraction indicators.
    2926
            // 3. An entire radical expression, enclosed in radical indicators.
    2927
            // 4. An arrow.
    2928
            // 5. An arbitrary shape.
    2929
            // 6. Any expression enclosed in matching pairs of round parentheses, square brackets or curly braces.
    2930
            // 7. Any expression enclosed in the braille grouping indicators.   [Note: not possible here]
    2931
            // 8. If none of the foregoing apply, the item is simply the [this element's] individual symbol.
    2932
    2933
            use crate::xpath_functions::IsInDefinition;
    2934
    538
            let mut node_name = name(mathml);
    2935
    538
            if mathml.attribute_value("data-roman-numeral").is_some() {
    2936
    1
                node_name = "mi";           // roman numerals don't follow number rules
    2937
    537
            }
    2938
    538
            match node_name {
    2939
    538
                "mn" => {
    2940
    250
                    if !is_base {
    2941
    233
                        return false;
    2942
    17
                    }                                                                                        // clause 1
    2943
                    // two 'mn's can be adjacent, in which case we need to group the 'mn' to make it clear it is separate (see bug #204)
    2944
    17
                    let parent = get_parent(mathml);   // there is always a "math" node
    2945
    17
                    let grandparent = if name(parent) == "math" {parent0} else {get_parent(parent)};
    2946
    17
                    if name(grandparent) != "mrow" {
    2947
    2
                        return false;
    2948
    15
                    }
    2949
    15
                    let preceding = parent.preceding_siblings();
    2950
    15
                    if preceding.len()  < 2 {
    2951
    6
                        return false;
    2952
    9
                    }
    2953
                    // any 'mn' would be separated from this node by invisible times
    2954
    9
                    let previous_child = as_element(preceding[preceding.len()-1]);
    2955
    9
                    if name(previous_child) == "mo" && as_text(previous_child) == "\u{2062}" {
    2956
    6
                        let previous_child = as_element(preceding[preceding.len()-2]);
    2957
    6
                        return name(previous_child) == "mn"
    2958
                    } else {
    2959
    3
                        return false;
    2960
                    }
    2961
                },
    2962
    288
                "mi" | "mo"44 | "mtext"32 => {
    2963
    258
                    let text = as_text(mathml);
    2964
    258
                    let parent = get_parent(mathml);   // there is always a "math" node
    2965
    258
                    let parent_name = name(parent);   // there is always a "math" node
    2966
    258
                    if is_base && (parent_name == "msub"230 || parent_name == "msup"224 || parent_name == "msubsup"10) && !224text224.contains([' ', '\u{00A0}']) {
    2967
    224
                        return false;
    2968
    34
                    }
    2969
    34
                    let mut chars = text.chars();
    2970
    34
                    let first_char = chars.next().unwrap();             // canonicalization assures it isn't empty;
    2971
    34
                    let is_one_char = chars.next().is_none();
    2972
                    // '¨', etc., brailles as two chars -- there probably is some exception list but I haven't found it -- these are the ones I know about
    2973
    34
                    return !((is_one_char && !31['¨', '″', '‴', '⁗']31.contains(&first_char)) ||                       // clause 8
    2974
                                // "lim", "cos", etc., appear not to get parens, but the rules don't mention it (tests show it)
    2975
    4
                                IsInDefinition::is_defined_in(text, &SPEECH_DEFINITIONS, "FunctionNames").unwrap() ||
    2976
    3
                                IsInDefinition::is_defined_in(text, &SPEECH_DEFINITIONS, "Arrows").unwrap() ||          // clause 4
    2977
    3
                                IsInDefinition::is_defined_in(text, &SPEECH_DEFINITIONS, "GeometryShapes").unwrap());   // clause 5
    2978
                },
    2979
    30
                "mfrac" => return false2,                                                     // clause 2 (test GTM 8.2(4) shows numeric fractions are not special)
    2980
    28
                "msqrt" | "mroot" => return false0,                                           // clause 3
    2981
                        // clause 6 only mentions three grouping chars, I'm a little suspicious of that, but that's what it says
    2982
    28
                "mrow" => return !(IsBracketed::is_bracketed22(mathml22, "("22, ")"22, false, false) ||
    2983
    16
                                    IsBracketed::is_bracketed(mathml, "[", "]", false, false) ||
    2984
    15
                                    IsBracketed::is_bracketed(mathml, "{", "}", false, false) ),
    2985
    6
                "msub" | "msup"4 | "msubsup"1 => {
    2986
                    // I'm a little dubious about the false value, but see GTM 7.7(2)
    2987
    5
                    if !is_base {
    2988
    3
                        return true;
    2989
    2
                    }
    2990
                    // need to group nested scripts in base -- see GTM 12.2(2)
    2991
    2
                    let parent = get_parent(mathml);   // there is always a "math" node
    2992
    2
                    let parent_name = name(parent);   // there is always a "math" node
    2993
    2
                    return parent_name == "munder" || parent_name == "mover" || parent_name == "munderover"1;
    2994
                },
    2995
    1
                _ => return true,
    2996
            }
    2997
    2998
    538
        }
    2999
    }
    3000
    3001
    impl Function for NeedsToBeGrouped {
    3002
        // convert a node to an ordinal number
    3003
    1.34k
        fn evaluate<'d>(&self,
    3004
    1.34k
                            _context: &context::Evaluation<'_, 'd>,
    3005
    1.34k
                            args: Vec<Value<'d>>)
    3006
    1.34k
                            -> StdResult<Value<'d>, XPathError>
    3007
        {
    3008
    1.34k
            let mut args = Args(args);
    3009
    1.34k
            args.exactly(3)?0;
    3010
    1.34k
            let is_base = args.pop_boolean()?0;
    3011
    1.34k
            let braille_code = args.pop_string()?0;
    3012
    1.34k
            let node = validate_one_node(args.pop_nodeset()?0, "NeedsToBeGrouped")?0;
    3013
    1.34k
            if let Node::Element(e) = node {
    3014
    1.34k
                let answer = match braille_code.as_str() {
    3015
    1.34k
                    "CMU" => NeedsToBeGrouped::needs_grouping_for_cmu805(e805, is_base805),
    3016
    538
                    "UEB" => NeedsToBeGrouped::needs_grouping_for_ueb(e, is_base),
    3017
    0
                    "Finnish" => NeedsToBeGrouped::needs_grouping_for_finnish(e, is_base),
    3018
    0
                    "Swedish" => NeedsToBeGrouped::needs_grouping_for_swedish(e, is_base),
    3019
    0
                    _ => return Err(XPathError::Other(format!("NeedsToBeGrouped: braille code arg '{braille_code:?}' is not a known code ('UEB', 'CMU', or 'Swedish')"))),
    3020
                };
    3021
    1.34k
                return Ok( Value::Boolean( answer ) );
    3022
    0
            }
    3023
    3024
    0
            return Err(XPathError::Other(format!("NeedsToBeGrouped: first arg '{node:?}' is not a node")));
    3025
    1.34k
        }
    3026
    }
    3027
    3028
    3029
    3030
    #[cfg(test)]
    3031
    mod tests {
    3032
        use super::*;
    3033
        #[allow(unused_imports)]
    3034
        use crate::init_logger;
    3035
        use crate::interface::*;
    3036
        use log::debug;
    3037
    3038
        #[test]
    3039
    1
        fn ueb_highlight_24() -> Result<()> {       // issue 24
    3040
    1
            let mathml_str = "<math display='block' id='id-0'>
    3041
    1
                <mrow id='id-1'>
    3042
    1
                    <mn id='id-2'>4</mn>
    3043
    1
                    <mo id='id-3'>&#x2062;</mo>
    3044
    1
                    <mi id='id-4'>a</mi>
    3045
    1
                    <mo id='id-5'>&#x2062;</mo>
    3046
    1
                    <mi id='id-6'>c</mi>
    3047
    1
                </mrow>
    3048
    1
            </math>";
    3049
    1
            crate::interface::set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
    3050
    1
            set_mathml(mathml_str).unwrap();
    3051
    1
            set_preference("BrailleCode", "UEB").unwrap();
    3052
    1
            set_preference("BrailleNavHighlight", "All").unwrap();
    3053
    1
            let braille = get_braille("id-2")?0;
    3054
    1
            assert_eq!("⣼⣙⠰⠁⠉", braille);
    3055
    1
            set_navigation_node("id-2", 0)?0;
    3056
    1
            assert_eq!( get_braille_position()?0, (0,2));
    3057
    3058
    1
            let braille = get_braille("id-4")?0;
    3059
    1
            assert_eq!("⠼⠙⣰⣁⠉", braille);
    3060
    1
            set_navigation_node("id-4", 0)?0;
    3061
    1
            assert_eq!( get_braille_position()?0, (2,4));
    3062
    1
            return Ok( () );
    3063
    1
        }
    3064
    3065
        #[test]
    3066
        // This test probably should be repeated for each braille code and be taken out of here
    3067
    1
        fn find_mathml_from_braille() -> Result<()> {
    3068
            use std::time::Instant;
    3069
    1
            let mathml_str = "<math id='id-0'>
    3070
    1
            <mrow data-changed='added' id='id-1'>
    3071
    1
              <mi id='id-2'>x</mi>
    3072
    1
              <mo id='id-3'>=</mo>
    3073
    1
              <mfrac id='id-4'>
    3074
    1
                <mrow id='id-5'>
    3075
    1
                  <mrow data-changed='added' id='id-6'>
    3076
    1
                    <mo id='id-7'>-</mo>
    3077
    1
                    <mi id='id-8'>b</mi>
    3078
    1
                  </mrow>
    3079
    1
                  <mo id='id-9'>±</mo>
    3080
    1
                  <msqrt id='id-10'>
    3081
    1
                    <mrow data-changed='added' id='id-11'>
    3082
    1
                      <msup id='id-12'>
    3083
    1
                        <mi id='id-13'>b</mi>
    3084
    1
                        <mn id='id-14'>2</mn>
    3085
    1
                      </msup>
    3086
    1
                      <mo id='id-15'>-</mo>
    3087
    1
                      <mrow data-changed='added' id='id-16'>
    3088
    1
                        <mn id='id-17'>4</mn>
    3089
    1
                        <mo data-changed='added' id='id-18'>&#x2062;</mo>
    3090
    1
                        <mi id='id-19'>a</mi>
    3091
    1
                        <mo data-changed='added' id='id-20'>&#x2062;</mo>
    3092
    1
                        <mi id='id-21'>c</mi>
    3093
    1
                      </mrow>
    3094
    1
                    </mrow>
    3095
    1
                  </msqrt>
    3096
    1
                </mrow>
    3097
    1
                <mrow id='id-22'>
    3098
    1
                  <mn id='id-23'>2</mn>
    3099
    1
                  <mo data-changed='added' id='id-24'>&#x2062;</mo>
    3100
    1
                  <mi id='id-25'>a</mi>
    3101
    1
                </mrow>
    3102
    1
              </mfrac>
    3103
    1
            </mrow>
    3104
    1
           </math>";
    3105
    1
            crate::interface::set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
    3106
    1
            set_mathml(mathml_str).unwrap();
    3107
    1
            set_preference("BrailleNavHighlight", "Off").unwrap();
    3108
    3109
    1
            set_preference("BrailleCode", "Nemeth").unwrap();
    3110
    1
            let _braille = get_braille("")?0;
    3111
    1
            let answers= &[2, 3, 3, 3, 3, 4, 7, 8, 9, 9,   10, 13, 12, 14, 12, 15, 17, 19, 21, 10,   4, 23, 25, 4];
    3112
    24
            let answers1 = answers1.map1(|num| format!("id-{}", num));
    3113
    1
            debug!("\n*** Testing Nemeth ***");
    3114
    24
            for (i, answer) in answers1.iter1().enumerate1() {
    3115
    24
                debug!("\n===  i={}  ===", i);
    3116
    24
                let instant = Instant::now();
    3117
    24
                let (id, _offset) = crate::interface::get_navigation_node_from_braille_position(i)?0;
    3118
    24
                N_PROBES.with(|n| {debug!("test {:2} #probes = {}", i, n0.borrow0())});
    3119
    24
                debug!("Time taken: {}ms", instant.elapsed()0.as_millis0());
    3120
    24
                assert_eq!(*answer, id, "\nNemeth test ith position={}", i);
    3121
            }
    3122
    3123
    1
            set_preference("BrailleCode", "UEB").unwrap();
    3124
    1
            let _braille = get_braille("")?0;
    3125
    1
            let answers= &[0, 0, 0, 2, 3, 3, 3, 3, 4, 7,   7, 8, 9, 9, 10, 13, 12, 14, 14, 15,   15, 17, 17, 19, 19, 21, 10, 4, 4, 23,   23, 25, 25, 4, 0, 0];
    3126
    36
            let answers1 = answers1.map1(|num| format!("id-{}", num));
    3127
    1
            debug!("\n\n*** Testing UEB ***");
    3128
    36
            for (i, answer) in answers1.iter1().enumerate1() {
    3129
    36
                debug!("\n===  i={}  ===", i);
    3130
    36
                let instant = Instant::now();
    3131
    36
                let (id, _offset) = crate::interface::get_navigation_node_from_braille_position(i)?0;
    3132
    36
                N_PROBES.with(|n| {debug!("test {:2} #probes = {}", i, n0.borrow0())});
    3133
    36
                debug!("Time taken: {}ms", instant.elapsed()0.as_millis0());
    3134
    36
                assert_eq!(*answer, id, "\nUEB test ith position={}", i);
    3135
            }
    3136
    1
            set_preference("BrailleCode", "CMU").unwrap();
    3137
    1
            let braille = get_braille("")?0;
    3138
    1
            let answers= &[2, 3, 5, 7, 8, 9, 9, 9, 10, 10,   11, 13, 12, 14, 14, 15, 17, 17, 19, 19,   21, 11, 5, 4, 22, 23, 23, 25, 25, 22,];
    3139
    30
            let answers1 = answers1.map1(|num| format!("id-{}", num));
    3140
    1
            debug!("\n\n*** Testing CMU ***");
    3141
    1
            debug!("Braille: {}", braille);
    3142
    30
            for (i, answer) in answers1.iter1().enumerate1() {
    3143
    30
                debug!("\n===  i={}  ===", i);
    3144
    30
                let instant = Instant::now();
    3145
    30
                let (id, _offset) = crate::interface::get_navigation_node_from_braille_position(i)?0;
    3146
    30
                N_PROBES.with(|n| {debug!("test {:2} #probes = {}", i, n0.borrow0())});
    3147
    30
                debug!("Time taken: {}ms", instant.elapsed()0.as_millis0());
    3148
    30
                assert_eq!(*answer, id, "\nCMU test ith position={}", i);
    3149
            }
    3150
    1
            return Ok( () );
    3151
    1
        }
    3152
    3153
        #[test]
    3154
        #[allow(non_snake_case)]
    3155
    1
        fn test_UEB_start_mode() -> Result<()> {
    3156
    1
            let mathml_str = "<math><msup><mi>x</mi><mi>n</mi></msup></math>";
    3157
    1
            crate::interface::set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
    3158
    1
            set_mathml(mathml_str).unwrap();
    3159
    1
            set_preference("BrailleCode", "UEB").unwrap();
    3160
    1
            set_preference("UEB_START_MODE", "Grade2").unwrap();
    3161
    1
            let braille = get_braille("")?0;
    3162
    1
            assert_eq!("⠭⠰⠔⠝", braille, "Grade2");
    3163
    1
            set_preference("UEB_START_MODE", "Grade1").unwrap();
    3164
    1
            let braille = get_braille("")?0;
    3165
    1
            assert_eq!("⠭⠔⠝", braille, "Grade1");
    3166
    1
            return Ok( () );
    3167
    1
        }
    3168
    }