/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'>⁢</mo> |
3044 | 1 | <mi id='id-4'>a</mi> |
3045 | 1 | <mo id='id-5'>⁢</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'>⁢</mo> |
3090 | 1 | <mi id='id-19'>a</mi> |
3091 | 1 | <mo data-changed='added' id='id-20'>⁢</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'>⁢</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 | | } |