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' && 
!275
is_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' && 
!239
is_forced_grade1239
(&chars, i) {
1023
235
                    if is_standing_alone_already_encountered ||
1024
226
                       ((found_g1 || 
!is_after_whitespace33
) &&
!203
is_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' &&
!416
is_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" && 
!4
as_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
) &&
!224
text224
.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
}