From d2881d39d6695a9d40ce2c7e7cecd65d391948a9 Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Sat, 22 Aug 2026 00:14:48 -0700 Subject: [PATCH 1/9] Add UEB G1 BANA/GTM mode preference and expand Grade-2 contractions. Adding the preference is a stall tactic while I wait for feedback from ICEB about whether BANA rules are obsolete. Introduce UEB_G1_BANA_Style (default true) to choose BANA 2019 vs ICEB GTM 1.7.3/1.7.5 grade-1 indicator placement in ueb_cleanup. Implement GTM 1.7 per-symbols-sequence counting with gtm_1_7_mode, and add matching ICEB tests. Extend handle_contractions for technical vocabulary: UEB 10.8 final-letter groupsigns, dot-5 through/part, dis/con prefixes, cc/ea exception words, and a ful-pattern fix. Add ueb_technical_word_contractions (311 words, goldens cross-checked with brailletranslators.com). Wire the preference through prefs, fuzz, and docs. --- Rules/prefs.yaml | 1 + docs/users_old.md | 3 + fuzz/fuzz_targets/fuzz_target_1.rs | 2 +- src/braille.rs | 540 +++++++++++++++++++++++++++-- src/prefs.rs | 1 + tests/braille/UEB/iceb.rs | 521 ++++++++++++++++++++++++++++ 6 files changed, 1029 insertions(+), 39 deletions(-) diff --git a/Rules/prefs.yaml b/Rules/prefs.yaml index b6181be77..f96d8b526 100644 --- a/Rules/prefs.yaml +++ b/Rules/prefs.yaml @@ -61,6 +61,7 @@ UEB: StartMode: "Grade2" # Grade1/Grade2 -- assumed starting mode UEB braille (Grade1 assumes we are in G1 passage mode) + G1_BANA_Style: true # true = BANA 2019 grade-1 placement; false = ICEB GTM 1.7 (1.7.3/1.7.5) # UEB Guide to Technical Material (https://iceb.org/Guidelines_for_Technical_Material_2008-10.pdf) # says to normally treat Fraktur and DoubleStruck as Script diff --git a/docs/users_old.md b/docs/users_old.md index 5acaecc8e..d666e8398 100644 --- a/docs/users_old.md +++ b/docs/users_old.md @@ -160,6 +160,9 @@ ClearSpeak has a number of options. These were designed for authors to use, but * ✓UseSpacesAroundAllOperators: [false] * Options: true/false * Description: The UEB guidelines suggest that for lower grades, adding space around operators such as `+` and `-` can be a good idea. Normally, space is only added around relational operators such as `=` and `<`. + * G1_BANA_Style: [true] + * Options: true/false + * Description: chooses how grade 1 indicators are placed. `true` follows the BANA 2019 provisional guidance (grade 1 word/passage decided for the whole expression); `false` follows ICEB GTM 1.7 (grade 1 indicators chosen per symbols-sequence, with a passage only when three or more sequences need grade 1). Braille codes often have author-definable characters. MathCAT provides some options: diff --git a/fuzz/fuzz_targets/fuzz_target_1.rs b/fuzz/fuzz_targets/fuzz_target_1.rs index c9a8b9abc..bfd4ddb4e 100644 --- a/fuzz/fuzz_targets/fuzz_target_1.rs +++ b/fuzz/fuzz_targets/fuzz_target_1.rs @@ -101,7 +101,7 @@ fn value_choices_for_key(key: &str, default: &str) -> Vec { "CopyAs" => vec!["MathML", "LaTeX", "ASCIIMath"], "BrailleNavHighlight" => vec!["Off", "FirstChar", "EndPoints", "All"], "UEB_StartMode" => vec!["Grade1", "Grade2"], - "LaTeX_UseShortName" | "Vietnam_UseDropNumbers" => vec!["true", "false"], + "LaTeX_UseShortName" | "Vietnam_UseDropNumbers" | "UEB_G1_BANA_Style" => vec!["true", "false"], "DecimalSeparator" => vec!["Auto", ".", ","], _ if key.starts_with("ClearSpeak_") => vec![ "Auto", "Verbose", "Ordinal", "Over", "None", "Speak", "TrigInverse", "AbsEnd", diff --git a/src/braille.rs b/src/braille.rs index 129fb7290..35b6735ad 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -1037,10 +1037,11 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str let result = capitals_to_word_mode(&result); let use_only_grade1 = pref_manager.pref_to_string("UEB_START_MODE").as_str() == "Grade1"; + let use_bana_style = pref_manager.pref_to_string("UEB_G1_BANA_Style").as_str() != "false"; // '𝐖' is a hard break -- basically, it separates exprs let mut result = result.split('𝐖') - .map(|str| pick_start_mode(str, use_only_grade1) + "W") + .map(|str| pick_start_mode(str, use_only_grade1, use_bana_style) + "W") .collect::(); result.pop(); // we added a 'W' at the end that needs to be removed. @@ -1057,13 +1058,21 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str return result.to_string(); - fn pick_start_mode(raw_braille: &str, use_only_grade1: bool) -> String { - // Need to decide what the start mode should be - // From http://www.brailleauthority.org/ueb/ueb_math_guidance/final_for_posting_ueb_math_guidance_may_2019_102419.pdf - // Unless a math expression can be correctly represented with only a grade 1 symbol indicator in the first three cells - // or before a single letter standing alone anywhere in the expression, - // begin the expression with a grade 1 word indicator (or a passage indicator if the expression includes spaces) - // Apparently "only a grade 1 symbol..." means at most one grade 1 symbol based on some examples (GTM 6.4, example 4) + fn pick_start_mode(raw_braille: &str, use_only_grade1: bool, use_bana_style: bool) -> String { + // Need to decide what the start mode should be. + // + // Two styles are supported, selected by the `UEB_G1_BANA_Style` preference: + // * BANA style (`use_bana_style == true`, the default): + // From http://www.brailleauthority.org/ueb/ueb_math_guidance/final_for_posting_ueb_math_guidance_may_2019_102419.pdf + // Unless a math expression can be correctly represented with only a grade 1 symbol indicator in the first three cells + // or before a single letter standing alone anywhere in the expression, + // begin the expression with a grade 1 word indicator (or a passage indicator if the expression includes spaces) + // Apparently "only a grade 1 symbol..." means at most one grade 1 symbol based on some examples (GTM 6.4, example 4) + // * ICEB GTM 1.7 style (`use_bana_style == false`): + // GTM 1.7.3 decides indicators per *symbols-sequence* (RUEB 2.1 -- text separated by spaces): + // (a) allow one grade 1 symbol per symbols-sequence, or a grade 1 word indicator if a sequence needs more than one; + // (b) use a grade 1 passage only if three or more symbols-sequences each need a grade 1 symbol or word indicator. + // grade 1 symbol indicators forced by 'a-j' following a digit are not counted (GTM 1.7.3 note). // debug!("before determining mode: '{}'", raw_braille); // a bit ugly because we need to store the string if we have cap passage mode @@ -1072,6 +1081,9 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str if use_only_grade1 { return remove_unneeded_mode_changes(raw_braille, UEB_Mode::Grade1, UEB_Duration::Passage); } + if !use_bana_style { + return gtm_1_7_mode(raw_braille); + } let grade2 = remove_unneeded_mode_changes(raw_braille, UEB_Mode::Grade2, UEB_Duration::Symbol); debug!("Symbol mode: '{}'", grade2); @@ -1263,6 +1275,47 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str } return if found_word_mode {g1_words.join("W")} else {"".to_string()}; } + + /// Count the number of non-forced grade 1 indicators needed for a single symbols-sequence. + /// (Forced indicators -- 'a-j' following a digit -- are excluded per the GTM 1.7.3 note.) + fn grade1_count(raw_word: &str) -> usize { + let grade2 = remove_unneeded_mode_changes(raw_word, UEB_Mode::Grade2, UEB_Duration::Symbol); + let chars = grade2.chars().collect::>(); + return chars.iter().enumerate() + .filter(|&(i, &ch)| ch == '1' && !is_forced_grade1(&chars, i)) + .count(); + } + + /// ICEB GTM 1.7 grade 1 indicator placement (per symbols-sequence). + /// See GTM 1.7.3. Note: the 1.7.5 two-symbol allowance and contraction-preserving + /// word/passage placement for expressions containing English words are only partially + /// realized here; those cases depend on separate contraction-rule improvements. + fn gtm_1_7_mode(raw_braille: &str) -> String { + // Count how many symbols-sequences (whitespace-separated) need a non-forced grade 1 indicator. + let n_seq_needing = raw_braille.split('W') + .filter(|raw_word| grade1_count(raw_word) >= 1) + .count(); + + // GTM 1.7.3(b): use a grade 1 passage if three or more sequences each need grade 1. + if n_seq_needing >= 3 { + let grade1_passage = remove_unneeded_mode_changes(raw_braille, UEB_Mode::Grade1, UEB_Duration::Passage); + return "⠰⠰⠰".to_string() + &grade1_passage + "⠰⠄"; + } + + // GTM 1.7.3(a): per sequence, allow one grade 1 symbol indicator, or a grade 1 word + // indicator if the sequence needs more than one. + let words = raw_braille.split('W') + .map(|raw_word| { + if grade1_count(raw_word) >= 2 { + "⠰⠰".to_string() + &remove_unneeded_mode_changes(raw_word, UEB_Mode::Grade1, UEB_Duration::Word) + } else { + // 0 or 1 grade 1 indicators: the grade 2 form leaves a single symbol indicator inline + remove_unneeded_mode_changes(raw_word, UEB_Mode::Grade2, UEB_Duration::Symbol) + } + }) + .collect::>(); + return words.join("W"); + } } } @@ -1940,7 +1993,8 @@ fn stands_alone(chars: &[char], i: usize) -> (bool, &[char], usize) { fn handle_contractions(chars: &[char], mut result: String) -> String { struct Replacement { pattern: String, - replacement: &'static str + replacement: &'static str, + skip_if_word_in: Option<&'static phf::Set<&'static str>>, } const ASCII_TO_UNICODE: &[char] = &[ @@ -1959,47 +2013,88 @@ fn handle_contractions(chars: &[char], mut result: String) -> String { return unicode; } + // Whole words where the lower groupsign 'cc' must not apply (10.11.1 compound/function names). + static CC_EXCEPTION_WORDS: phf::Set<&str> = phf_set! { + "L⠁L⠗L⠉L⠉L⠕L⠎", // arccos + "L⠁L⠗L⠉L⠉L⠕L⠎L⠊L⠝L⠑", // arccosine + "L⠁L⠗L⠉L⠉L⠕L⠎L⠓", // arccosh + "L⠁L⠗L⠉L⠉L⠕L⠎L⠑L⠉", // arccosec + "L⠁L⠗L⠉L⠉L⠕L⠎L⠑L⠉L⠁L⠝L⠞", // arccosecant + "L⠁L⠗L⠉L⠉L⠕L⠞", // arccot + "L⠁L⠗L⠉L⠉L⠕L⠞L⠁L⠝L⠛L⠑L⠝L⠞", // arccotangent + "L⠁L⠗L⠉L⠉L⠎L⠉", // arccsc + }; + + // Words where the 'ea' lower groupsign must not apply (re- prefix morpheme boundary). + static EA_EXCEPTION_WORDS: phf::Set<&str> = phf_set! { + "L⠗L⠑L⠁L⠉L⠞L⠁L⠝L⠉L⠑", // reactance + "L⠗L⠑L⠁L⠉L⠞L⠊L⠕L⠝", // reaction + "L⠗L⠑L⠁L⠉L⠞L⠊L⠧L⠊L⠞L⠽", // reactivity + }; + // It would be much better from an extensibility point of view to read the table in from a file static CONTRACTIONS: LazyLock> = LazyLock::new(|| { vec![ + // 10.9: initial-letter (dot-5) wordsigns -- whole word only + Replacement{ pattern: format!("^{}$", to_unicode_braille("time")), replacement: "⠐⠞", skip_if_word_in: None }, + Replacement{ pattern: format!("^{}$", to_unicode_braille("work")), replacement: "⠐⠺", skip_if_word_in: None }, + Replacement{ pattern: format!("^{}$", to_unicode_braille("leverage")), replacement: "⠇⠐⠑⠁⠛⠑", skip_if_word_in: None }, + + // 10.10: shortform prefix "dis" + Replacement{ pattern: format!("^{}", to_unicode_braille("dis")), replacement: "⠲", skip_if_word_in: None }, + // liblouis/brailletranslators "con" prefix + Replacement{ pattern: format!("^{}", to_unicode_braille("con")), replacement: "⠒", skip_if_word_in: None }, + + // 10.7.1: dot-5 initial-letter contractions (as wordsigns / groupsigns) + Replacement{ pattern: format!("^{}", to_unicode_braille("through")), replacement: "⠐⠹", skip_if_word_in: None }, + Replacement{ pattern: format!("(?PL.){}(?PL.)", to_unicode_braille("part")), replacement: "${s}⠐⠏${e}", skip_if_word_in: None }, + // 10.3: Strong contractions - Replacement{ pattern: to_unicode_braille("and"), replacement: "L⠯"}, - Replacement{ pattern: to_unicode_braille("for"), replacement: "L⠿"}, - Replacement{ pattern: to_unicode_braille("of"), replacement: "L⠷"}, - Replacement{ pattern: to_unicode_braille("the"), replacement: "L⠮"}, - Replacement{ pattern: to_unicode_braille("with"), replacement: "L⠾"}, + Replacement{ pattern: to_unicode_braille("and"), replacement: "L⠯", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("for"), replacement: "L⠿", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("of"), replacement: "L⠷", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("the"), replacement: "L⠮", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("with"), replacement: "L⠾", skip_if_word_in: None }, - // 10.8: final-letter group signs (this need to precede 'en' and any other shorter contraction) - Replacement{ pattern: "(?PL.)L⠍L⠑L⠝L⠞".to_string(), replacement: "${s}L⠰L⠞" }, // ment - Replacement{ pattern: "(?PL.)L⠞L⠊L⠕L⠝".to_string(), replacement: "${s}L⠰L⠝" } ,// tion + // 10.8: final-letter group signs (these need to precede 'en' and any other shorter contraction) + Replacement{ pattern: "(?PL.)L⠍L⠑L⠝L⠞".to_string(), replacement: "${s}L⠰L⠞", skip_if_word_in: None }, // ment + Replacement{ pattern: "(?PL.)L⠞L⠊L⠕L⠝".to_string(), replacement: "${s}L⠰L⠝", skip_if_word_in: None }, // tion + Replacement{ pattern: "(?PL.)L⠊L⠞L⠽".to_string(), replacement: "${s}L⠰L⠽", skip_if_word_in: None }, // ity + Replacement{ pattern: "(?PL.)L⠁L⠝L⠉L⠑".to_string(), replacement: "${s}L⠨L⠑", skip_if_word_in: None }, // ance + Replacement{ pattern: "(?PL.)L⠎L⠊L⠕L⠝".to_string(), replacement: "${s}L⠨L⠝", skip_if_word_in: None }, // sion + Replacement{ pattern: "(?PL.)L⠑L⠝L⠉L⠑".to_string(), replacement: "${s}L⠰L⠑", skip_if_word_in: None }, // ence + Replacement{ pattern: "(?PL.)L⠝L⠑L⠎L⠎".to_string(), replacement: "${s}L⠰L⠎", skip_if_word_in: None }, // ness + Replacement{ pattern: "(?PL.)L⠕L⠥L⠝L⠙".to_string(), replacement: "${s}L⠨L⠙", skip_if_word_in: None }, // ound + Replacement{ pattern: "(?PL.)L⠕L⠥L⠝L⠞".to_string(), replacement: "${s}L⠨L⠞", skip_if_word_in: None }, // ount + Replacement{ pattern: "(?PL.)L⠇L⠑L⠎L⠎".to_string(), replacement: "${s}L⠨L⠎", skip_if_word_in: None }, // less + Replacement{ pattern: "(?PL.)L⠕L⠝L⠛".to_string(), replacement: "${s}L⠰L⠛", skip_if_word_in: None }, // ong + Replacement{ pattern: "(?PL.)L⠋L⠥L⠇".to_string(), replacement: "${s}L⠰L⠇", skip_if_word_in: None }, // ful // 10.4: Strong group signs - Replacement{ pattern: to_unicode_braille("ch"), replacement: "L⠡"}, - Replacement{ pattern: to_unicode_braille("gh"), replacement: "L⠣"}, - Replacement{ pattern: to_unicode_braille("sh"), replacement: "L⠩"}, - Replacement{ pattern: to_unicode_braille("th"), replacement: "L⠹"}, - Replacement{ pattern: to_unicode_braille("wh"), replacement: "L⠱"}, - Replacement{ pattern: to_unicode_braille("ed"), replacement: "L⠫"}, - Replacement{ pattern: to_unicode_braille("er"), replacement: "L⠻"}, - Replacement{ pattern: to_unicode_braille("ou"), replacement: "L⠳"}, - Replacement{ pattern: to_unicode_braille("ow"), replacement: "L⠪"}, - Replacement{ pattern: to_unicode_braille("st"), replacement: "L⠌"}, - Replacement{ pattern: "(?PL.)L⠊L⠝L⠛".to_string(), replacement: "${s}L⠬" }, // 'ing', not at start - Replacement{ pattern: to_unicode_braille("ar"), replacement: "L⠜"}, + Replacement{ pattern: to_unicode_braille("ch"), replacement: "L⠡", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("gh"), replacement: "L⠣", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("sh"), replacement: "L⠩", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("th"), replacement: "L⠹", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("wh"), replacement: "L⠱", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("ed"), replacement: "L⠫", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("er"), replacement: "L⠻", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("ou"), replacement: "L⠳", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("ow"), replacement: "L⠪", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("st"), replacement: "L⠌", skip_if_word_in: None }, + Replacement{ pattern: "(?PL.)L⠊L⠝L⠛".to_string(), replacement: "${s}L⠬", skip_if_word_in: None }, // 'ing', not at start + Replacement{ pattern: to_unicode_braille("ar"), replacement: "L⠜", skip_if_word_in: None }, // 10.6.5: Lower group signs preceded and followed by letters // FIX: don't match if after/before a cap letter -- can't use negative pattern (?!...) in regex package - // Note: removed cc because "arccos" shouldn't be contracted (10.11.1), but there is no way to know about compound words - // Add it back after implementing a lookup dictionary of exceptions - Replacement{ pattern: "(?PL.)L⠑L⠁(?PL.)".to_string(), replacement: "${s}L⠂${e}" }, // ea - Replacement{ pattern: "(?PL.)L⠃L⠃(?PL.)".to_string(), replacement: "${s}L⠆${e}" }, // bb - // Replacement{ pattern: "(?PL.)L⠉L⠉(?PL.)".to_string(), replacement: "${s}L⠒${e}" }, // cc - Replacement{ pattern: "(?PL.)L⠋L⠋(?PL.)".to_string(), replacement: "${s}L⠖${e}" }, // ff - Replacement{ pattern: "(?PL.)L⠛L⠛(?PL.)".to_string(), replacement: "${s}L⠶${e}" }, // gg + Replacement{ pattern: "(?PL.)L⠑L⠁(?PL.)".to_string(), replacement: "${s}L⠂${e}", skip_if_word_in: Some(&EA_EXCEPTION_WORDS) }, // ea + Replacement{ pattern: "(?PL.)L⠃L⠃(?PL.)".to_string(), replacement: "${s}L⠆${e}", skip_if_word_in: None }, // bb + Replacement{ pattern: "(?PL.)L⠉L⠉(?PL.)".to_string(), replacement: "${s}L⠒${e}", skip_if_word_in: Some(&CC_EXCEPTION_WORDS) }, // cc + Replacement{ pattern: "(?PL.)L⠋L⠋(?PL.)".to_string(), replacement: "${s}L⠖${e}", skip_if_word_in: None }, // ff + Replacement{ pattern: "(?PL.)L⠛L⠛(?PL.)".to_string(), replacement: "${s}L⠶${e}", skip_if_word_in: None }, // gg // 10.6.8: Lower group signs ("in" also 10.5.4 lower word signs) // FIX: these need restrictions about only applying when upper dots are present - Replacement{ pattern: to_unicode_braille("en"), replacement: "⠢"}, - Replacement{ pattern: to_unicode_braille("in"), replacement: "⠔"}, + Replacement{ pattern: to_unicode_braille("en"), replacement: "⠢", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("in"), replacement: "⠔", skip_if_word_in: None }, ] }); @@ -2008,10 +2103,16 @@ fn handle_contractions(chars: &[char], mut result: String) -> String { static CONTRACTION_REGEX: LazyLock> = LazyLock::new(|| init_regex(&CONTRACTIONS)); let mut chars_as_str = chars.iter().collect::(); + let original_chars_as_str = chars_as_str.clone(); // debug!(" handle_contractions: examine '{}'", &chars_as_str); let matches = CONTRACTION_PATTERNS.matches(&chars_as_str); for i in matches.iter() { let element = &CONTRACTIONS[i]; + if let Some(exceptions) = element.skip_if_word_in { + if exceptions.contains(&original_chars_as_str) { + continue; + } + } // debug!(" replacing '{}' with '{}' in '{}'", element.pattern, element.replacement, &chars_as_str); result.truncate(result.len() - chars_as_str.len()); chars_as_str = CONTRACTION_REGEX[i].replace_all(&chars_as_str, element.replacement).to_string(); @@ -3640,4 +3741,367 @@ mod tests { return Ok( () ); }); } + + /// Letter-to-Unicode-braille mapping (matches handle_contractions). + const TEST_ASCII_TO_UNICODE: &[char] = &[ + '⠀', '⠮', '⠐', '⠼', '⠫', '⠩', '⠯', '⠄', '⠷', '⠾', '⠡', '⠬', '⠠', '⠤', '⠨', '⠌', + '⠴', '⠂', '⠆', '⠒', '⠲', '⠢', '⠖', '⠶', '⠦', '⠔', '⠱', '⠰', '⠣', '⠿', '⠜', '⠹', + '⠈', '⠁', '⠃', '⠉', '⠙', '⠑', '⠋', '⠛', '⠓', '⠊', '⠚', '⠅', '⠇', '⠍', '⠝', '⠕', + '⠏', '⠟', '⠗', '⠎', '⠞', '⠥', '⠧', '⠺', '⠭', '⠽', '⠵', '⠪', '⠳', '⠻', '⠘', '⠸', + ]; + + fn ascii_to_uncontracted_braille(word: &str) -> String { + word.bytes() + .map(|b| TEST_ASCII_TO_UNICODE[(b.to_ascii_uppercase() - 32) as usize]) + .collect() + } + + /// Run handle_contractions on a lowercase ASCII word (L-cell input) and return Unicode braille. + fn contract_word(word: &str) -> String { + let l_chars: Vec = word.bytes() + .flat_map(|b| { + let ch = TEST_ASCII_TO_UNICODE[(b.to_ascii_uppercase() - 32) as usize]; + ['L', ch] + }) + .collect(); + let input: String = l_chars.iter().collect(); + let output = handle_contractions(&l_chars, input); + output.chars().filter(|&c| c != 'L').collect() + } + + /// Grade-2 contraction coverage for technical vocabulary (311 words). + /// Cross-check goldens at https://brailletranslators.com/ (Grade 2 / UEB). + /// Should contract (225): force, work, time, speed, distance, area, height, weight, power, energy, density, gravity, acceleration, length, width, depth, cost, profit, interest, revenue, clearance, conductance, capacitance, resistance, impedance, reactance, variance, tolerance, luminosity, reliability, velocity, viscosity, precision, tension, efficiency, frequency, valence, stiffness, thickness, displacement, concentration, consumption, duration, elevation, fraction, friction, inflation, iteration, population, position, potential, production, resolution, rotation, charge, chord, pitch, growth, head, heat, reach, shear, margin, salary, offset, income, input, inventory, current, error, leverage, temperature, turnover, accuracy, breadth, strength, spread, discount, count, version, inductance, intensity, interval, perimeter, probability, utility, validity, parity, permeability, proportion, uncertainty, circumference, capacity, deceleration, dilution, enthalpy, entropy, equity, expense, gradient, headroom, inertia, latency, longitude, permittivity, period, principal, sensitivity, stress, throughput, thrust, wavelength, absorption, activation, activity, admittance, affinity, albedo, aperture, attenuation, average, bandwidth, baseline, bearing, benchmark, boiling, boundary, cohesion, compliance, compression, conductivity, coverage, damping, deflection, departure, depreciation, deviation, diffusion, dispersion, dissipation, distortion, divergence, downforce, elasticity, emissivity, emission, erosion, evaporation, expansion, expectation, extent, feedback, fidelity, filament, hardness, humidity, hysteresis, illuminance, incidence, index, inflow, influx, inhibition, injection, irradiance, kinetic, leakage, luminance, maturity, mean, median, melting, minimum, mobility, molarity, moisture, opacity, oscillation, outflow, overlap, parallax, parameter, peak, percentile, percentage, polarization, propagation, purity, quality, quantity, quenching, quotient, radiance, radiation, reaction, reactivity, redshift, reflectance, reflection, refraction, reluctance, remanence, resistivity, resonance, restitution, retardation, rigidity, salinity, saturation, scalar, scattering, separation, shrinkage, similarity, battery, diameter, epoch, gain, jitter, joule, momentum, shift, step, stride, zenith + /// Should not contract (86): mass, volume, base, rate, drag, drift, gap, lag, loss, pace, radius, range, ratio, risk, run, scale, size, slope, tax, torque, capital, impulse, latitude, magnitude, premium, pressure, response, return, trajectory, voltage, aspect, backlog, curvature, dosage, dose, equilibrium, excess, exposure, fatigue, hydraulic, lifetime, likelihood, maximum, metric, orbit, osmosis, pulse, quanta, quantum, regret, residual, ripple, root, rotor, sag, secant, signal, altitude, amplitude, angle, bias, bitrate, budget, bulk, decay, deficit, degree, delay, duty, lift, limit, load, modulus, payload, phase, price, sample, skew, slack, span, supply, total, value, vector, wage, yield + /// Note: arccos (cc exception, partial ar groupsign) is covered in tests/braille/UEB/other.rs contractions_1. + #[test] + fn ueb_technical_word_contractions() { + const SHOULD_CONTRACT: &[(&str, &str)] = &[ + ("force", "⠿⠉⠑"), + ("work", "⠐⠺"), + ("time", "⠐⠞"), + ("speed", "⠎⠏⠑⠫"), + ("distance", "⠲⠞⠨⠑"), + ("area", "⠜⠑⠁"), + ("height", "⠓⠑⠊⠣⠞"), + ("weight", "⠺⠑⠊⠣⠞"), + ("power", "⠏⠪⠻"), + ("energy", "⠢⠻⠛⠽"), + ("density", "⠙⠢⠎⠰⠽"), + ("gravity", "⠛⠗⠁⠧⠰⠽"), + ("acceleration", "⠁⠒⠑⠇⠻⠁⠰⠝"), + ("length", "⠇⠢⠛⠹"), + ("width", "⠺⠊⠙⠹"), + ("depth", "⠙⠑⠏⠹"), + ("cost", "⠉⠕⠌"), + ("profit", "⠏⠗⠷⠊⠞"), + ("interest", "⠔⠞⠻⠑⠌"), + ("revenue", "⠗⠑⠧⠢⠥⠑"), + ("clearance", "⠉⠇⠑⠜⠨⠑"), + ("conductance", "⠒⠙⠥⠉⠞⠨⠑"), + ("capacitance", "⠉⠁⠏⠁⠉⠊⠞⠨⠑"), + ("resistance", "⠗⠑⠎⠊⠌⠨⠑"), + ("impedance", "⠊⠍⠏⠫⠨⠑"), + ("reactance", "⠗⠑⠁⠉⠞⠨⠑"), + ("variance", "⠧⠜⠊⠨⠑"), + ("tolerance", "⠞⠕⠇⠻⠨⠑"), + ("luminosity", "⠇⠥⠍⠔⠕⠎⠰⠽"), + ("reliability", "⠗⠑⠇⠊⠁⠃⠊⠇⠰⠽"), + ("velocity", "⠧⠑⠇⠕⠉⠰⠽"), + ("viscosity", "⠧⠊⠎⠉⠕⠎⠰⠽"), + ("precision", "⠏⠗⠑⠉⠊⠨⠝"), + ("tension", "⠞⠢⠨⠝"), + ("efficiency", "⠑⠖⠊⠉⠊⠢⠉⠽"), + ("frequency", "⠋⠗⠑⠟⠥⠢⠉⠽"), + ("valence", "⠧⠁⠇⠰⠑"), + ("stiffness", "⠌⠊⠖⠰⠎"), + ("thickness", "⠹⠊⠉⠅⠰⠎"), + ("displacement", "⠲⠏⠇⠁⠉⠑⠰⠞"), + ("concentration", "⠒⠉⠢⠞⠗⠁⠰⠝"), + ("consumption", "⠒⠎⠥⠍⠏⠰⠝"), + ("duration", "⠙⠥⠗⠁⠰⠝"), + ("elevation", "⠑⠇⠑⠧⠁⠰⠝"), + ("fraction", "⠋⠗⠁⠉⠰⠝"), + ("friction", "⠋⠗⠊⠉⠰⠝"), + ("inflation", "⠔⠋⠇⠁⠰⠝"), + ("iteration", "⠊⠞⠻⠁⠰⠝"), + ("population", "⠏⠕⠏⠥⠇⠁⠰⠝"), + ("position", "⠏⠕⠎⠊⠰⠝"), + ("potential", "⠏⠕⠞⠢⠞⠊⠁⠇"), + ("production", "⠏⠗⠕⠙⠥⠉⠰⠝"), + ("resolution", "⠗⠑⠎⠕⠇⠥⠰⠝"), + ("rotation", "⠗⠕⠞⠁⠰⠝"), + ("charge", "⠡⠜⠛⠑"), + ("chord", "⠡⠕⠗⠙"), + ("pitch", "⠏⠊⠞⠡"), + ("growth", "⠛⠗⠪⠹"), + ("head", "⠓⠂⠙"), + ("heat", "⠓⠂⠞"), + ("reach", "⠗⠂⠡"), + ("shear", "⠩⠑⠜"), + ("margin", "⠍⠜⠛⠔"), + ("salary", "⠎⠁⠇⠜⠽"), + ("offset", "⠷⠋⠎⠑⠞"), + ("income", "⠔⠉⠕⠍⠑"), + ("input", "⠔⠏⠥⠞"), + ("inventory", "⠔⠧⠢⠞⠕⠗⠽"), + ("current", "⠉⠥⠗⠗⠢⠞"), + ("error", "⠻⠗⠕⠗"), + ("leverage", "⠇⠐⠑⠁⠛⠑"), + ("temperature", "⠞⠑⠍⠏⠻⠁⠞⠥⠗⠑"), + ("turnover", "⠞⠥⠗⠝⠕⠧⠻"), + ("accuracy", "⠁⠒⠥⠗⠁⠉⠽"), + ("breadth", "⠃⠗⠂⠙⠹"), + ("strength", "⠌⠗⠢⠛⠹"), + ("spread", "⠎⠏⠗⠂⠙"), + ("discount", "⠲⠉⠨⠞"), + ("count", "⠉⠨⠞"), + ("version", "⠧⠻⠨⠝"), + ("inductance", "⠔⠙⠥⠉⠞⠨⠑"), + ("intensity", "⠔⠞⠢⠎⠰⠽"), + ("interval", "⠔⠞⠻⠧⠁⠇"), + ("perimeter", "⠏⠻⠊⠍⠑⠞⠻"), + ("probability", "⠏⠗⠕⠃⠁⠃⠊⠇⠰⠽"), + ("utility", "⠥⠞⠊⠇⠰⠽"), + ("validity", "⠧⠁⠇⠊⠙⠰⠽"), + ("parity", "⠏⠜⠰⠽"), + ("permeability", "⠏⠻⠍⠂⠃⠊⠇⠰⠽"), + ("proportion", "⠏⠗⠕⠏⠕⠗⠰⠝"), + ("uncertainty", "⠥⠝⠉⠻⠞⠁⠔⠞⠽"), + ("circumference", "⠉⠊⠗⠉⠥⠍⠋⠻⠰⠑"), + ("capacity", "⠉⠁⠏⠁⠉⠰⠽"), + ("deceleration", "⠙⠑⠉⠑⠇⠻⠁⠰⠝"), + ("dilution", "⠙⠊⠇⠥⠰⠝"), + ("enthalpy", "⠢⠹⠁⠇⠏⠽"), + ("entropy", "⠢⠞⠗⠕⠏⠽"), + ("equity", "⠑⠟⠥⠰⠽"), + ("expense", "⠑⠭⠏⠢⠎⠑"), + ("gradient", "⠛⠗⠁⠙⠊⠢⠞"), + ("headroom", "⠓⠂⠙⠗⠕⠕⠍"), + ("inertia", "⠔⠻⠞⠊⠁"), + ("latency", "⠇⠁⠞⠢⠉⠽"), + ("longitude", "⠇⠰⠛⠊⠞⠥⠙⠑"), + ("permittivity", "⠏⠻⠍⠊⠞⠞⠊⠧⠰⠽"), + ("period", "⠏⠻⠊⠕⠙"), + ("principal", "⠏⠗⠔⠉⠊⠏⠁⠇"), + ("sensitivity", "⠎⠢⠎⠊⠞⠊⠧⠰⠽"), + ("stress", "⠌⠗⠑⠎⠎"), + ("throughput", "⠐⠹⠏⠥⠞"), + ("thrust", "⠹⠗⠥⠌"), + ("wavelength", "⠺⠁⠧⠑⠇⠢⠛⠹"), + ("absorption", "⠁⠃⠎⠕⠗⠏⠰⠝"), + ("activation", "⠁⠉⠞⠊⠧⠁⠰⠝"), + ("activity", "⠁⠉⠞⠊⠧⠰⠽"), + ("admittance", "⠁⠙⠍⠊⠞⠞⠨⠑"), + ("affinity", "⠁⠖⠔⠰⠽"), + ("albedo", "⠁⠇⠃⠫⠕"), + ("aperture", "⠁⠏⠻⠞⠥⠗⠑"), + ("attenuation", "⠁⠞⠞⠢⠥⠁⠰⠝"), + ("average", "⠁⠧⠻⠁⠛⠑"), + ("bandwidth", "⠃⠯⠺⠊⠙⠹"), + ("baseline", "⠃⠁⠎⠑⠇⠔⠑"), + ("bearing", "⠃⠑⠜⠬"), + ("benchmark", "⠃⠢⠡⠍⠜⠅"), + ("boiling", "⠃⠕⠊⠇⠬"), + ("boundary", "⠃⠨⠙⠜⠽"), + ("cohesion", "⠉⠕⠓⠑⠨⠝"), + ("compliance", "⠉⠕⠍⠏⠇⠊⠨⠑"), + ("compression", "⠉⠕⠍⠏⠗⠑⠎⠨⠝"), + ("conductivity", "⠒⠙⠥⠉⠞⠊⠧⠰⠽"), + ("coverage", "⠉⠕⠧⠻⠁⠛⠑"), + ("damping", "⠙⠁⠍⠏⠬"), + ("deflection", "⠙⠑⠋⠇⠑⠉⠰⠝"), + ("departure", "⠙⠑⠐⠏⠥⠗⠑"), + ("depreciation", "⠙⠑⠏⠗⠑⠉⠊⠁⠰⠝"), + ("deviation", "⠙⠑⠧⠊⠁⠰⠝"), + ("diffusion", "⠙⠊⠖⠥⠨⠝"), + ("dispersion", "⠲⠏⠻⠨⠝"), + ("dissipation", "⠲⠎⠊⠏⠁⠰⠝"), + ("distortion", "⠲⠞⠕⠗⠰⠝"), + ("divergence", "⠙⠊⠧⠻⠛⠰⠑"), + ("downforce", "⠙⠪⠝⠿⠉⠑"), + ("elasticity", "⠑⠇⠁⠌⠊⠉⠰⠽"), + ("emissivity", "⠑⠍⠊⠎⠎⠊⠧⠰⠽"), + ("emission", "⠑⠍⠊⠎⠨⠝"), + ("erosion", "⠻⠕⠨⠝"), + ("evaporation", "⠑⠧⠁⠏⠕⠗⠁⠰⠝"), + ("expansion", "⠑⠭⠏⠁⠝⠨⠝"), + ("expectation", "⠑⠭⠏⠑⠉⠞⠁⠰⠝"), + ("extent", "⠑⠭⠞⠢⠞"), + ("feedback", "⠋⠑⠫⠃⠁⠉⠅"), + ("fidelity", "⠋⠊⠙⠑⠇⠰⠽"), + ("filament", "⠋⠊⠇⠁⠰⠞"), + ("hardness", "⠓⠜⠙⠰⠎"), + ("humidity", "⠓⠥⠍⠊⠙⠰⠽"), + ("hysteresis", "⠓⠽⠌⠻⠑⠎⠊⠎"), + ("illuminance", "⠊⠇⠇⠥⠍⠔⠨⠑"), + ("incidence", "⠔⠉⠊⠙⠰⠑"), + ("index", "⠔⠙⠑⠭"), + ("inflow", "⠔⠋⠇⠪"), + ("influx", "⠔⠋⠇⠥⠭"), + ("inhibition", "⠔⠓⠊⠃⠊⠰⠝"), + ("injection", "⠔⠚⠑⠉⠰⠝"), + ("irradiance", "⠊⠗⠗⠁⠙⠊⠨⠑"), + ("kinetic", "⠅⠔⠑⠞⠊⠉"), + ("leakage", "⠇⠂⠅⠁⠛⠑"), + ("luminance", "⠇⠥⠍⠔⠨⠑"), + ("maturity", "⠍⠁⠞⠥⠗⠰⠽"), + ("mean", "⠍⠂⠝"), + ("median", "⠍⠫⠊⠁⠝"), + ("melting", "⠍⠑⠇⠞⠬"), + ("minimum", "⠍⠔⠊⠍⠥⠍"), + ("mobility", "⠍⠕⠃⠊⠇⠰⠽"), + ("molarity", "⠍⠕⠇⠜⠰⠽"), + ("moisture", "⠍⠕⠊⠌⠥⠗⠑"), + ("opacity", "⠕⠏⠁⠉⠰⠽"), + ("oscillation", "⠕⠎⠉⠊⠇⠇⠁⠰⠝"), + ("outflow", "⠳⠞⠋⠇⠪"), + ("overlap", "⠕⠧⠻⠇⠁⠏"), + ("parallax", "⠏⠜⠁⠇⠇⠁⠭"), + ("parameter", "⠏⠜⠁⠍⠑⠞⠻"), + ("peak", "⠏⠂⠅"), + ("percentile", "⠏⠻⠉⠢⠞⠊⠇⠑"), + ("percentage", "⠏⠻⠉⠢⠞⠁⠛⠑"), + ("polarization", "⠏⠕⠇⠜⠊⠵⠁⠰⠝"), + ("propagation", "⠏⠗⠕⠏⠁⠛⠁⠰⠝"), + ("purity", "⠏⠥⠗⠰⠽"), + ("quality", "⠟⠥⠁⠇⠰⠽"), + ("quantity", "⠟⠥⠁⠝⠞⠰⠽"), + ("quenching", "⠟⠥⠢⠡⠬"), + ("quotient", "⠟⠥⠕⠞⠊⠢⠞"), + ("radiance", "⠗⠁⠙⠊⠨⠑"), + ("radiation", "⠗⠁⠙⠊⠁⠰⠝"), + ("reaction", "⠗⠑⠁⠉⠰⠝"), + ("reactivity", "⠗⠑⠁⠉⠞⠊⠧⠰⠽"), + ("redshift", "⠗⠫⠩⠊⠋⠞"), + ("reflectance", "⠗⠑⠋⠇⠑⠉⠞⠨⠑"), + ("reflection", "⠗⠑⠋⠇⠑⠉⠰⠝"), + ("refraction", "⠗⠑⠋⠗⠁⠉⠰⠝"), + ("reluctance", "⠗⠑⠇⠥⠉⠞⠨⠑"), + ("remanence", "⠗⠑⠍⠁⠝⠰⠑"), + ("resistivity", "⠗⠑⠎⠊⠌⠊⠧⠰⠽"), + ("resonance", "⠗⠑⠎⠕⠝⠨⠑"), + ("restitution", "⠗⠑⠌⠊⠞⠥⠰⠝"), + ("retardation", "⠗⠑⠞⠜⠙⠁⠰⠝"), + ("rigidity", "⠗⠊⠛⠊⠙⠰⠽"), + ("salinity", "⠎⠁⠇⠔⠰⠽"), + ("saturation", "⠎⠁⠞⠥⠗⠁⠰⠝"), + ("scalar", "⠎⠉⠁⠇⠜"), + ("scattering", "⠎⠉⠁⠞⠞⠻⠬"), + ("separation", "⠎⠑⠏⠜⠁⠰⠝"), + ("shrinkage", "⠩⠗⠔⠅⠁⠛⠑"), + ("similarity", "⠎⠊⠍⠊⠇⠜⠰⠽"), + ("battery", "⠃⠁⠞⠞⠻⠽"), + ("diameter", "⠙⠊⠁⠍⠑⠞⠻"), + ("epoch", "⠑⠏⠕⠡"), + ("gain", "⠛⠁⠔"), + ("jitter", "⠚⠊⠞⠞⠻"), + ("joule", "⠚⠳⠇⠑"), + ("momentum", "⠍⠕⠰⠞⠥⠍"), + ("shift", "⠩⠊⠋⠞"), + ("step", "⠌⠑⠏"), + ("stride", "⠌⠗⠊⠙⠑"), + ("zenith", "⠵⠢⠊⠹"), + ]; + const SHOULD_NOT_CONTRACT: &[&str] = &[ + "mass", "volume", "base", "rate", "drag", "drift", "gap", "lag", "loss", "pace", + "radius", "range", "ratio", "risk", "run", "scale", "size", "slope", "tax", "torque", + "capital", "impulse", "latitude", "magnitude", "premium", "pressure", "response", "return", + "trajectory", "voltage", "aspect", "backlog", "curvature", "dosage", "dose", "equilibrium", + "excess", "exposure", "fatigue", "hydraulic", "lifetime", "likelihood", "maximum", "metric", + "orbit", "osmosis", "pulse", "quanta", "quantum", "regret", "residual", "ripple", "root", + "rotor", "sag", "secant", "signal", "altitude", "amplitude", "angle", "bias", "bitrate", + "budget", "bulk", "decay", "deficit", "degree", "delay", "duty", "lift", "limit", "load", + "modulus", "payload", "phase", "price", "sample", "skew", "slack", "span", "supply", + "total", "value", "vector", "wage", "yield", + ]; + + for &(word, expected) in SHOULD_CONTRACT { + let got = contract_word(word); + assert_eq!(expected, got, "word '{word}'"); + assert_ne!(ascii_to_uncontracted_braille(word), got, "word '{word}' should contract"); + } + for &word in SHOULD_NOT_CONTRACT { + let got = contract_word(word); + let uncontracted = ascii_to_uncontracted_braille(word); + assert_eq!(uncontracted, got, "word '{word}' should not contract"); + } + } + + #[test] + #[ignore] + fn ueb_technical_word_contractions_probe_batch2() { + let try_contract = [ + "circumference", "capacity", "deceleration", "dilution", "enthalpy", "entropy", "equity", "expense", + "gradient", "headroom", "inertia", "latency", "longitude", "permittivity", "period", "principal", + "sensitivity", "stress", "throughput", "thrust", "wavelength", "absorption", "activation", "activity", + "admittance", "affinity", "albedo", "aperture", "attenuation", "average", "bandwidth", "baseline", + "bearing", "benchmark", "boiling", "boundary", "cohesion", "compliance", "compression", "conductivity", + "coverage", "damping", "deflection", "departure", "depreciation", "deviation", "diffusion", "dispersion", + "dissipation", "distortion", "divergence", "downforce", "elasticity", "emissivity", "emission", "erosion", + "evaporation", "expansion", "expectation", "extent", "feedback", "fidelity", "filament", "hardness", + "humidity", "hysteresis", "illuminance", "incidence", "index", "inflow", "influx", "inhibition", + "injection", "irradiance", "kinetic", "leakage", "luminance", "maturity", "mean", "median", "melting", + "minimum", "mobility", "molarity", "moisture", "opacity", "oscillation", "outflow", "overlap", "parallax", + "parameter", "peak", "percentile", "percentage", "polarization", "propagation", "purity", "quality", + "quantity", "quenching", "quotient", "radiance", "radiation", "reaction", "reactivity", "redshift", + "reflectance", "reflection", "refraction", "reluctance", "remanence", "resistivity", "resonance", + "restitution", "retardation", "rigidity", "salinity", "saturation", "scalar", "scattering", "separation", + "shrinkage", "similarity", "battery", "diameter", "epoch", "gain", "jitter", "joule", "momentum", "shift", + "step", "stride", "zenith", + ]; + let try_not = [ + "capital", "impulse", "latitude", "magnitude", "premium", "pressure", "response", "return", "trajectory", + "voltage", "aspect", "backlog", "curvature", "dosage", "dose", "equilibrium", "excess", "exposure", + "fatigue", "hydraulic", "lifetime", "likelihood", "maximum", "metric", "orbit", "osmosis", "pulse", + "quanta", "quantum", "regret", "residual", "ripple", "root", "rotor", "sag", "secant", "signal", + "altitude", "amplitude", "angle", "bias", "bitrate", "budget", "bulk", "decay", "deficit", "degree", + "delay", "duty", "lift", "limit", "load", "modulus", "payload", "phase", "price", "sample", "skew", + "slack", "span", "supply", "total", "value", "vector", "wage", "yield", + ]; + let mut out = String::new(); + for w in try_contract { + let c = contract_word(w); + out.push_str(&format!("(\"{w}\", \"{c}\"),\n")); + } + for w in try_not { + out.push_str(&format!("// no: {w}\n")); + } + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("notes/_batch2_goldens.txt"); + std::fs::write(path, out).unwrap(); + } + + #[test] + #[ignore] + fn ueb_technical_word_contractions_probe() { + let should_contract = [ + "force", "work", "time", "speed", "distance", "area", "height", "weight", "power", "energy", + "density", "gravity", "acceleration", "length", "width", "depth", "cost", "profit", "interest", "revenue", + "clearance", "conductance", "capacitance", "resistance", "impedance", "reactance", "variance", "tolerance", + "luminosity", "reliability", "velocity", "viscosity", "precision", "tension", "efficiency", "frequency", + "valence", "stiffness", "thickness", "displacement", "concentration", "consumption", "duration", "elevation", + "fraction", "friction", "inflation", "iteration", "population", "position", "potential", "production", + "resolution", "rotation", "charge", "chord", "pitch", "growth", "head", "heat", "reach", "shear", "margin", + "salary", "offset", "income", "input", "inventory", "current", "error", "leverage", "temperature", + "turnover", "accuracy", "breadth", "strength", "spread", "discount", "count", "version", "inductance", + "intensity", "interval", "perimeter", "probability", "utility", "validity", "parity", "permeability", + "proportion", "uncertainty", + ]; + let should_not = [ + "mass", "volume", "base", "rate", "drag", "drift", "gap", "lag", "loss", "pace", + "radius", "range", "ratio", "risk", "run", "scale", "size", "slope", "tax", "torque", + ]; + eprintln!("=== SHOULD CONTRACT ==="); + for w in should_contract { + let c = contract_word(w); + let u = ascii_to_uncontracted_braille(w); + eprintln!("(\"{w}\", \"{c}\"), // uncontracted={u} changed={}", c != u); + } + eprintln!("=== SHOULD NOT CONTRACT ==="); + for w in should_not { + let c = contract_word(w); + let u = ascii_to_uncontracted_braille(w); + eprintln!("{w}: contracted={c} uncontracted={u} ok={}", c == u); + } + } } diff --git a/src/prefs.rs b/src/prefs.rs index 4dc51c4bc..44edb6994 100644 --- a/src/prefs.rs +++ b/src/prefs.rs @@ -79,6 +79,7 @@ impl Preferences{ prefs.insert("BrailleCode".to_string(), Yaml::String("Nemeth".to_string())); prefs.insert("BrailleNavHighlight".to_string(), Yaml::String("EndPoints".to_string())); prefs.insert("UEB_START_MODE".to_string(), Yaml::String("Grade2".to_string())); + prefs.insert("UEB_G1_BANA_Style".to_string(), Yaml::Boolean(true)); prefs.insert("DecimalSeparators".to_string(), Yaml::String(".".to_string())); prefs.insert("BlockSeparators".to_string(), Yaml::String(", \u{00A0}\u{202F}".to_string())); diff --git a/tests/braille/UEB/iceb.rs b/tests/braille/UEB/iceb.rs index b7f3a6560..3eb19deea 100644 --- a/tests/braille/UEB/iceb.rs +++ b/tests/braille/UEB/iceb.rs @@ -200,6 +200,527 @@ fn grade1_1_7_4() -> Result<()> { } +// Additional examples from GTM 1.7 (July 2025): +// https://iceb.org/wp-content/uploads/2026/02/GTM-1.7-Grade-1-Indicators-Approved.pdf +// Ordinary surrounding text is omitted; braille is the preferred math from the spec. + +#[test] +fn grade1_1_7_3_a_1() -> Result<()> { + let expr = "x2"; + test_braille("UEB", expr, "⠭⠰⠔⠼⠃")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_3_a_2() -> Result<()> { + let expr = "x"; + test_braille("UEB", expr, "⠰⠭⠀⠰⠳⠕⠀⠼⠿")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_3_a_3() -> Result<()> { + let expr = "xy"; + test_braille("UEB", expr, "⠰⠰⠷⠭⠨⠌⠽⠾")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_3_b_1() -> Result<()> { + let expr = "xa=xb + =xc"; + test_braille("UEB", expr, "⠰⠰⠰⠭⠢⠁⠀⠐⠶⠀⠭⠢⠃⠀⠐⠶⠀⠭⠢⠉⠰⠄")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_3_b_2() -> Result<()> { + let expr = "y=x2-4; +  y=x2-2x; +  y=x-x2."; + test_braille("UEB", expr, "⠰⠰⠰⠽⠀⠐⠶⠀⠭⠔⠼⠃⠐⠤⠼⠙⠆⠀⠽⠀⠐⠶⠀⠭⠔⠼⠃⠐⠤⠼⠃⠭⠆⠀⠽⠀⠐⠶⠀⠭⠐⠤⠭⠔⠼⠃⠲⠰⠄")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_4_1() -> Result<()> { + let expr = "x2-x-2 + =0"; + test_braille("UEB", expr, "⠭⠔⠼⠃⠐⠤⠭⠐⠤⠼⠃⠀⠐⠶⠀⠼⠚")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_4_2() -> Result<()> { + let expr = "x2-4x-3 + =0"; + test_braille("UEB", expr, "⠭⠔⠼⠃⠐⠤⠼⠙⠭⠐⠤⠼⠉⠀⠐⠶⠀⠼⠚")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_4_3() -> Result<()> { + let expr = "x2-1=0"; + test_braille("UEB", expr, "⠭⠔⠼⠃⠐⠤⠼⠁⠀⠐⠶⠀⠼⠚")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_5_b() -> Result<()> { + let expr = "luminositysun"; + test_braille("UEB", expr, "⠇⠥⠍⠔⠕⠎⠰⠽⠰⠰⠢⠣⠎⠥⠝⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_5_c() -> Result<()> { + let expr = "speed= + distancetime= + + 30,000 m + 60 s + = + 500 m/s"; + test_braille("UEB", expr, "⠎⠏⠑⠫⠀⠐⠶⠀⠰⠰⠰⠷⠙⠊⠎⠞⠁⠝⠉⠑⠨⠌⠞⠊⠍⠑⠾⠀⠐⠶⠀⠷⠼⠉⠚⠂⠚⠚⠚⠀⠍⠨⠌⠼⠋⠚⠀⠎⠾⠀⠐⠶⠀⠼⠑⠚⠚⠀⠍⠸⠌⠎⠰⠄")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_1() -> Result<()> { + let expr = "y=x"; + test_braille("UEB", expr, "⠰⠽⠀⠐⠶⠀⠰⠭")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_2() -> Result<()> { + let expr = "25=5"; + test_braille("UEB", expr, "⠰⠩⠼⠃⠑⠬⠀⠐⠶⠀⠼⠑")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_3() -> Result<()> { + let expr = "x=7"; + test_braille("UEB", expr, "⠰⠰⠩⠭⠬⠀⠐⠶⠀⠼⠛")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_4() -> Result<()> { + let expr = "ms-1"; + test_braille("UEB", expr, "⠰⠰⠍⠎⠔⠣⠐⠤⠼⠁⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_6() -> Result<()> { + let expr = "y=x22"; + test_braille("UEB", expr, "⠰⠽⠀⠐⠶⠀⠰⠰⠷⠭⠔⠼⠃⠨⠌⠼⠃⠾")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_7() -> Result<()> { + let expr = "xy=cd"; + test_braille("UEB", expr, "⠰⠰⠷⠭⠨⠌⠽⠾⠀⠐⠶⠀⠰⠰⠷⠉⠨⠌⠙⠾")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_8() -> Result<()> { + let expr = "(x,y)"; + test_braille("UEB", expr, "⠐⠣⠰⠭⠂⠀⠰⠽⠐⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_9() -> Result<()> { + let expr = "(xi,yi)"; + test_braille("UEB", expr, "⠐⠣⠭⠰⠢⠊⠂⠀⠽⠰⠢⠊⠐⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_10() -> Result<()> { + let expr = "(xi2, + yi2)"; + test_braille("UEB", expr, "⠰⠰⠐⠣⠭⠢⠊⠔⠼⠃⠂⠀⠰⠰⠽⠢⠊⠔⠼⠃⠐⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_11() -> Result<()> { + let expr = "B¯"; + test_braille("UEB", expr, "⠠⠃⠰⠱")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_12() -> Result<()> { + let expr = "M"; + test_braille("UEB", expr, "⠠⠍⠨⠔⠰⠳⠺⠗⠕")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_13() -> Result<()> { + let expr = "AZ"; + test_braille("UEB", expr, "⠰⠰⠣⠠⠠⠁⠵⠜⠨⠔⠳⠺⠗⠕")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_14() -> Result<()> { + let expr = "1327Al"; + test_braille("UEB", expr, "⠰⠢⠼⠁⠉⠔⠼⠃⠛⠠⠁⠇")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_15() -> Result<()> { + let expr = "O2-"; + test_braille("UEB", expr, "⠰⠰⠠⠕⠔⠣⠼⠃⠐⠤⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_16() -> Result<()> { + let expr = "2H2 + (g)+ + O2 + (g) + 2H2 + O(l)"; + test_braille("UEB", expr, "⠼⠃⠠⠓⠢⠼⠃⠐⠣⠛⠐⠜⠐⠖⠠⠕⠢⠼⠃⠐⠣⠛⠐⠜⠀⠰⠳⠕⠀⠼⠃⠠⠓⠢⠼⠃⠠⠕⠐⠣⠇⠐⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_17() -> Result<()> { + let expr = "y=mx+c"; + test_braille("UEB", expr, "⠰⠽⠀⠐⠶⠀⠍⠭⠐⠖⠉")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_18() -> Result<()> { + let expr = "A=πr2"; + test_braille("UEB", expr, "⠠⠁⠀⠐⠶⠀⠨⠏⠗⠰⠔⠼⠃")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_19() -> Result<()> { + let expr = "E=mc2"; + test_braille("UEB", expr, "⠰⠠⠑⠀⠐⠶⠀⠍⠉⠰⠔⠼⠃")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_20() -> Result<()> { + let expr = "x2-2x+1 + =(x-1)2"; + test_braille("UEB", expr, "⠭⠰⠔⠼⠃⠐⠤⠼⠃⠭⠐⠖⠼⠁⠀⠐⠶⠀⠐⠣⠭⠐⠤⠼⠁⠐⠜⠔⠼⠃")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_21() -> Result<()> { + let expr = "xn=1+ + -1nn"; + test_braille("UEB", expr, "⠭⠰⠢⠝⠀⠐⠶⠀⠼⠁⠐⠖⠷⠐⠤⠼⠁⠔⠝⠨⠌⠝⠾")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_22() -> Result<()> { + let expr = "xn= + 2nn2"; + test_braille("UEB", expr, "⠭⠰⠢⠝⠀⠐⠶⠀⠰⠷⠼⠃⠔⠝⠨⠌⠝⠔⠼⠃⠾")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_24() -> Result<()> { + let expr = "xsinθ"; + test_braille("UEB", expr, "⠰⠰⠭⠩⠎⠊⠝⠨⠹⠬")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_25() -> Result<()> { + let expr = "Version1b"; + test_braille("UEB", expr, "⠠⠧⠻⠨⠝⠰⠰⠔⠣⠼⠁⠰⠃⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_26() -> Result<()> { + let expr = "W=Fs"; + test_braille("UEB", expr, "⠰⠠⠺⠀⠐⠶⠀⠠⠋⠎")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_27() -> Result<()> { + let expr = "work=force×distance"; + test_braille("UEB", expr, "⠐⠺⠀⠐⠶⠀⠿⠉⠑⠐⠦⠙⠊⠌⠨⠑")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_28() -> Result<()> { + let expr = " force= + workdistance"; + test_braille("UEB", expr, "⠰⠠⠡⠀⠿⠉⠑⠀⠐⠶⠀⠰⠷⠐⠺⠨⠌⠙⠊⠌⠨⠑⠰⠾")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_29() -> Result<()> { + let expr = "acceleration= + + Δspeed + Δtime + "; + test_braille("UEB", expr, "⠁⠒⠑⠇⠻⠁⠰⠝⠀⠐⠶⠀⠰⠷⠠⠨⠙⠎⠏⠑⠫⠨⠌⠠⠨⠙⠐⠞⠰⠾")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_30() -> Result<()> { + let expr = "frequency=1time"; + test_braille("UEB", expr, "⠋⠗⠑⠟⠥⠢⠉⠽⠀⠐⠶⠀⠰⠷⠼⠁⠨⠌⠞⠊⠍⠑⠾")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_31() -> Result<()> { + let expr = "frequency=1/time"; + test_braille("UEB", expr, "⠋⠗⠑⠟⠥⠢⠉⠽⠀⠐⠶⠀⠼⠁⠸⠌⠞⠊⠍⠑")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_32() -> Result<()> { + let expr = "Vcone= + 13π + r2h"; + test_braille("UEB", expr, "⠰⠰⠠⠧⠢⠣⠉⠕⠝⠑⠜⠀⠐⠶⠀⠼⠁⠌⠉⠨⠏⠗⠔⠼⠃⠰⠓")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_33() -> Result<()> { + let expr = "V=π + y2dx"; + test_braille("UEB", expr, "⠰⠠⠧⠀⠐⠶⠀⠰⠰⠮⠨⠏⠽⠔⠼⠃⠰⠙⠭")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_34() -> Result<()> { + let expr = "period=2π + + lcosα + g + "; + test_braille("UEB", expr, "⠏⠻⠊⠕⠙⠀⠐⠶⠀⠼⠃⠨⠏⠩⠷⠇⠀⠰⠰⠉⠕⠎⠨⠁⠨⠌⠛⠾⠬")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_35() -> Result<()> { + let expr = "g=9.81  + ms-2"; + test_braille("UEB", expr, "⠰⠛⠀⠐⠶⠀⠼⠊⠲⠓⠁⠀⠰⠰⠍⠎⠔⠣⠐⠤⠼⠃⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_36() -> Result<()> { + let expr = "v1=x  + ms-1"; + test_braille("UEB", expr, "⠰⠰⠰⠧⠢⠼⠁⠀⠐⠶⠀⠭⠀⠍⠎⠔⠣⠐⠤⠼⠁⠜⠰⠄")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_37() -> Result<()> { + let expr = "P= + t,u,v"; + test_braille("UEB", expr, "⠰⠰⠰⠠⠏⠀⠐⠶⠀⠸⠣⠞⠂⠀⠥⠂⠀⠧⠸⠜⠰⠄")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_38() -> Result<()> { + let expr = "xa=xb + =xc=xd"; + test_braille("UEB", expr, "⠰⠰⠰⠭⠢⠁⠀⠐⠶⠀⠭⠢⠃⠀⠐⠶⠀⠭⠢⠉⠀⠐⠶⠀⠭⠢⠙⠰⠄")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_39() -> Result<()> { + let expr = "x2×x3 + =x2+3 + =x5"; + test_braille("UEB", expr, "⠰⠰⠰⠭⠔⠼⠃⠐⠦⠭⠔⠼⠉⠀⠐⠶⠀⠭⠔⠣⠼⠃⠐⠖⠼⠉⠜⠀⠐⠶⠀⠭⠔⠼⠑⠰⠄")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_40() -> Result<()> { + let expr = "am×an + =am+n"; + test_braille("UEB", expr, "⠁⠔⠍⠐⠦⠁⠔⠝⠀⠐⠶⠀⠁⠔⠣⠍⠐⠖⠝⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_41() -> Result<()> { + let expr = "(am)n + =amn"; + test_braille("UEB", expr, "⠐⠣⠁⠔⠍⠐⠜⠔⠝⠀⠐⠶⠀⠁⠔⠣⠍⠝⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_42() -> Result<()> { + let expr = "aman + =am-n"; + test_braille("UEB", expr, "⠷⠁⠔⠍⠨⠌⠁⠔⠝⠾⠀⠐⠶⠀⠁⠔⠣⠍⠐⠤⠝⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_43() -> Result<()> { + let expr = "a-m + =1am"; + test_braille("UEB", expr, "⠁⠔⠣⠐⠤⠍⠜⠀⠐⠶⠀⠷⠼⠁⠨⠌⠁⠔⠍⠾")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_44() -> Result<()> { + let expr = "a0=1"; + test_braille("UEB", expr, "⠁⠔⠼⠚⠀⠐⠶⠀⠼⠁")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_45() -> Result<()> { + let expr = "a1n + =an"; + test_braille("UEB", expr, "⠁⠔⠷⠼⠁⠨⠌⠝⠾⠀⠐⠶⠀⠩⠔⠝⠁⠬")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_46() -> Result<()> { + let expr = "BC"; + test_braille("UEB", expr, "⠰⠰⠰⠠⠃⠀⠳⠕⠀⠠⠉⠰⠄")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_47() -> Result<()> { + let expr = "x+y=5"; + test_braille("UEB", expr, "⠭⠐⠖⠽⠀⠐⠶⠀⠼⠑")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_48() -> Result<()> { + let expr = "CH4 + +4Cl2 + + CCl4 + +4HCl"; + test_braille("UEB", expr, "⠰⠰⠰⠠⠉⠠⠓⠢⠼⠙⠐⠖⠼⠙⠠⠉⠇⠢⠼⠃⠀⠳⠕⠀⠠⠉⠠⠉⠇⠢⠼⠙⠐⠖⠼⠙⠠⠓⠠⠉⠇⠰⠄")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_49() -> Result<()> { + let expr = "614C + + 714N + + + -10β"; + test_braille("UEB", expr, "⠰⠰⠰⠢⠼⠋⠔⠼⠁⠙⠠⠉⠀⠳⠕⠀⠢⠼⠛⠔⠼⠁⠙⠠⠝⠐⠖⠢⠣⠐⠤⠼⠁⠜⠔⠼⠚⠨⠃⠰⠄")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_50() -> Result<()> { + let expr = "reliability= + number of faultstotal number of items + =p"; + test_braille("UEB", expr, "⠗⠑⠇⠊⠁⠃⠊⠇⠰⠽⠀⠐⠶⠀⠰⠰⠰⠷⠝⠥⠍⠃⠑⠗⠀⠕⠋⠀⠋⠁⠥⠇⠞⠎⠀⠨⠌⠞⠕⠞⠁⠇⠀⠝⠥⠍⠃⠑⠗⠀⠕⠋⠀⠊⠞⠑⠍⠎⠾⠀⠐⠶⠀⠏⠰⠄")?; + return Ok(()); + +} + #[test] fn number_2_1_2() -> Result<()> { let expr = "3,000"; From 86dc15e679128e301f28031ee6fdd8509df6072c Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Tue, 25 Aug 2026 01:23:36 -0700 Subject: [PATCH 2/9] updated to new BANA and ICEB spec updates. Five tests fail. Still working on them --- Rules/Braille/UEB/UEB_Rules.yaml | 4 +- Rules/prefs.yaml | 2 +- src/braille.rs | 230 +++-------- src/prefs.rs | 1 - tests/braille/UEB/iceb.rs | 654 ++++++++++++++++++++++--------- tests/braille/UEB/other.rs | 8 +- 6 files changed, 546 insertions(+), 353 deletions(-) diff --git a/Rules/Braille/UEB/UEB_Rules.yaml b/Rules/Braille/UEB/UEB_Rules.yaml index 4d5b8ca1d..5e1df38a7 100644 --- a/Rules/Braille/UEB/UEB_Rules.yaml +++ b/Rules/Braille/UEB/UEB_Rules.yaml @@ -8,7 +8,7 @@ variables: [MatchingWhitespace: "true()"] replace: - test: - - if: "(@class='MathML-unit' or BaseNode(.)[@class='MathML-unit']) and @data-previous-space-width >= 0.25" # BANA 5.3(a) + - if: "(@class='MathML-unit' or contains(@data-intent-property, ':unit:') or BaseNode(.)[@class='MathML-unit' or contains(@data-intent-property, ':unit:')]) and @data-previous-space-width >= 0.25" # treat spaced units as separate exprs for G1 placement then: [t: "𝐖"] - else_if: "@data-previous-space-width > 1.1" then: [t: "⠬"] @@ -16,7 +16,7 @@ then: [t: "W"] - x: "." - test: - - if: "(@class='MathML-unit' or BaseNode(.)[@class='MathML-unit']) and @data-following-space-width >= 0.25" # BANA 5.3(a) + - if: "(@class='MathML-unit' or contains(@data-intent-property, ':unit:') or BaseNode(.)[@class='MathML-unit' or contains(@data-intent-property, ':unit:')]) and @data-following-space-width >= 0.25" # treat spaced units as separate exprs for G1 placement then: [t: "𝐖"] - else_if: "@data-following-space-width > 1.1" then: [t: "⠬"] diff --git a/Rules/prefs.yaml b/Rules/prefs.yaml index f96d8b526..58bc888bb 100644 --- a/Rules/prefs.yaml +++ b/Rules/prefs.yaml @@ -61,7 +61,7 @@ UEB: StartMode: "Grade2" # Grade1/Grade2 -- assumed starting mode UEB braille (Grade1 assumes we are in G1 passage mode) - G1_BANA_Style: true # true = BANA 2019 grade-1 placement; false = ICEB GTM 1.7 (1.7.3/1.7.5) + # Grade 1 indicators follow ICEB GTM 1.7 (also BANA 2026 math/science guidance) # UEB Guide to Technical Material (https://iceb.org/Guidelines_for_Technical_Material_2008-10.pdf) # says to normally treat Fraktur and DoubleStruck as Script diff --git a/src/braille.rs b/src/braille.rs index 35b6735ad..6cd8918bd 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -15,6 +15,7 @@ use crate::canonicalize::get_parent; use std::borrow::Cow; use std::ops::Range; use std::sync::LazyLock; +#[allow(unused_imports)] use log::{debug, error}; fn is_ueb_prefix(ch: char) -> bool { @@ -1037,11 +1038,10 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str let result = capitals_to_word_mode(&result); let use_only_grade1 = pref_manager.pref_to_string("UEB_START_MODE").as_str() == "Grade1"; - let use_bana_style = pref_manager.pref_to_string("UEB_G1_BANA_Style").as_str() != "false"; // '𝐖' is a hard break -- basically, it separates exprs let mut result = result.split('𝐖') - .map(|str| pick_start_mode(str, use_only_grade1, use_bana_style) + "W") + .map(|str| pick_start_mode(str, use_only_grade1) + "W") .collect::(); result.pop(); // we added a 'W' at the end that needs to be removed. @@ -1058,54 +1058,41 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str return result.to_string(); - fn pick_start_mode(raw_braille: &str, use_only_grade1: bool, use_bana_style: bool) -> String { - // Need to decide what the start mode should be. - // - // Two styles are supported, selected by the `UEB_G1_BANA_Style` preference: - // * BANA style (`use_bana_style == true`, the default): - // From http://www.brailleauthority.org/ueb/ueb_math_guidance/final_for_posting_ueb_math_guidance_may_2019_102419.pdf - // Unless a math expression can be correctly represented with only a grade 1 symbol indicator in the first three cells - // or before a single letter standing alone anywhere in the expression, - // begin the expression with a grade 1 word indicator (or a passage indicator if the expression includes spaces) - // Apparently "only a grade 1 symbol..." means at most one grade 1 symbol based on some examples (GTM 6.4, example 4) - // * ICEB GTM 1.7 style (`use_bana_style == false`): - // GTM 1.7.3 decides indicators per *symbols-sequence* (RUEB 2.1 -- text separated by spaces): - // (a) allow one grade 1 symbol per symbols-sequence, or a grade 1 word indicator if a sequence needs more than one; - // (b) use a grade 1 passage only if three or more symbols-sequences each need a grade 1 symbol or word indicator. - // grade 1 symbol indicators forced by 'a-j' following a digit are not counted (GTM 1.7.3 note). + fn pick_start_mode(raw_braille: &str, use_only_grade1: bool) -> String { + // Decide grade 1 indicator placement per ICEB GTM 1.7 (2025). + // BANA's 2026 guidance adopts the same GTM §1.7 rules: + // https://www.brailleauthority.org/sites/default/files/2026-07/Guidance%20on%20Transcribing%20Math%20and%20Science%20in%20UEB%202026.pdf + // GTM 1.7.3 decides indicators per *symbols-sequence* (RUEB 2.1 -- text separated by spaces): + // (a) allow one grade 1 symbol per symbols-sequence, or a grade 1 word indicator if a sequence needs more than one; + // (b) use a grade 1 passage only if three or more symbols-sequences each need a grade 1 symbol or word indicator. + // Grade 1 symbol indicators forced by 'a-j' following a digit are not counted (GTM 1.7.3 note). // debug!("before determining mode: '{}'", raw_braille); - // a bit ugly because we need to store the string if we have cap passage mode - let raw_braille_string = if is_cap_passage_mode_good(raw_braille) {convert_to_cap_passage_mode(raw_braille)} else {String::default()}; - let raw_braille = if raw_braille_string.is_empty() {raw_braille} else {&raw_braille_string}; - if use_only_grade1 { - return remove_unneeded_mode_changes(raw_braille, UEB_Mode::Grade1, UEB_Duration::Passage); - } - if !use_bana_style { - return gtm_1_7_mode(raw_braille); - } - let grade2 = remove_unneeded_mode_changes(raw_braille, UEB_Mode::Grade2, UEB_Duration::Symbol); - debug!("Symbol mode: '{}'", grade2); - - if is_grade2_string_ok(&grade2) { - return grade2; + // Capital passage (RUEB §8.5): like word mode's extra 'C' (CC…), passage uses CCC…Ce. + // Count G1 on the original (with capital markers) so standing-alone letters still force + // passage when needed (BANA Ex 5-15), but apply mode changes after stripping C/𝐶 so + // Grade 2 contractions are not blocked by cap_word_mode (chem "ch" → ⠡). + let use_cap_passage = is_cap_passage_mode_good(raw_braille); + let stripped_caps = raw_braille.replace(['C', '𝐶'], ""); + let apply_to = if use_cap_passage { stripped_caps.as_str() } else { raw_braille }; + let result = if use_only_grade1 { + remove_unneeded_mode_changes(apply_to, UEB_Mode::Grade1, UEB_Duration::Passage) } else { - // BANA says use g1 word mode if spaces are present, but that's not what their examples do - // A conversation with Ms. DeAndrea from BANA said that they mean use passage mode if ≥3 "segments" (≥2 blanks) - // The G1 Word mode might not be at the start (iceb.rs:omission_3_6_7) - let grade1_word = try_grade1_word_mode(raw_braille); - debug!("Word mode: '{}'", grade1_word); - if !grade1_word.is_empty() { - return grade1_word; - } else { - let grade1_passage = remove_unneeded_mode_changes(raw_braille, UEB_Mode::Grade1, UEB_Duration::Passage); - return "⠰⠰⠰".to_string() + &grade1_passage + "⠰⠄"; - } + gtm_1_7_mode(raw_braille, apply_to) + }; + if use_cap_passage { + return convert_to_cap_passage_mode(&result); } + return result; - /// Return true if at least five (= # of cap passage indicators) cap indicators and no lower case letters + /// Return true if capital passage mode should be used (RUEB §8.5 / BANA Ex 5-15). + /// Requires no lowercase letters, and either ≥5 capital indicators (chem / dense caps) + /// or ≥3 letter-bearing symbols-sequences that are fully capitalized. + /// Grade 1 / numeric / other non-letter markers are transparent (do not abort the scan). fn is_cap_passage_mode_good(braille: &str) -> bool { let mut n_caps = 0; + let mut n_letter_seqs = 0; + let mut seq_has_letter = false; let mut is_cap_mode = false; let mut cap_mode = UEB_Duration::Symbol; // real value set when is_cap_mode is set to true let mut chars = braille.chars(); @@ -1118,6 +1105,7 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str if !is_cap_mode { return false; } + seq_has_letter = true; chars.next(); // skip letter if cap_mode == UEB_Duration::Symbol { is_cap_mode = false; @@ -1133,76 +1121,34 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str } n_caps += 1; } else if ch == 'W' || ch == '𝐖' { - if is_cap_mode { - assert!(cap_mode == UEB_Duration::Word); + if seq_has_letter { + n_letter_seqs += 1; + seq_has_letter = false; } is_cap_mode = false; - } else if ch == '1' && is_cap_mode { - break; } + // else: '1', '𝟙', 'N', operators, typeforms, etc. — transparent + } + if seq_has_letter { + n_letter_seqs += 1; } - return n_caps > 4; + return n_caps >= 5 || n_letter_seqs >= 3; } + /// After G1 placement: strip any leftover capital markers and wrap with intermediate + /// `CCC`…`Ce` (same map as word mode: C→⠠, e→⠄ → ⠠⠠⠠…⠠⠄). Applied after + /// remove_unneeded_mode_changes so parsers never treat passage opener as CL letters. + /// If G1 passage is present (⠰⠰⠰…⠰⠄), capital passage goes inside (BANA Ex 5-15). fn convert_to_cap_passage_mode(braille: &str) -> String { - return "⠠⠠⠠".to_string() + &braille.replace(['C', '𝐶'], "") + "⠠⠄"; - } - - /// Return true if the BANA or ICEB guidelines say it is ok to start with grade 2 - fn is_grade2_string_ok(grade2_braille: &str) -> bool { - // BANA says use grade 2 if there is not more than one grade one symbol or single letter standing alone. - // The exact quote from their guidance: - // Unless a math expression can be correctly represented with only a grade 1 symbol indicator in the first three cells - // or before a single letter standing alone anywhere in the expression, - // begin the expression with a grade 1 word indicator - // Note: I modified this slightly to exclude the cap indicator in the count. That allows three more ICEB rule to pass and seems - // like it is a reasonable thing to do. - // Another modification is allow a single G1 indicator to occur after whitespace later on - // because ICEB examples show it and it seems better than going to passage mode if it is the only G1 indicator - - // Because of the 'L's which go away, we have to put a little more work into finding the first three chars - let chars = grade2_braille.chars().collect::>(); - let mut n_real_chars = 0; // actually number of chars - let mut found_g1 = false; - let mut i = 0; - while i < chars.len() { - let ch = chars[i]; - if ch == '1' && !is_forced_grade1(&chars, i) { - if found_g1 { - return false; - } - found_g1 = true; - } else if !"𝐶CLobc".contains(ch) { - if n_real_chars == 2 { - i += 1; - break; // this is the third real char - }; - n_real_chars += 1; - } - i += 1 - } - - // if we find *another* g1 that isn't forced and isn't standing alone, we are done - // I've added a 'follows whitespace' clause for test iceb.rs:omission_3_6_2 to the standing alone rule - // 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 - // Same for GTM 1_7_3_1 (passage mode is mentioned also) - let mut is_standing_alone_already_encountered = false; - let mut is_after_whitespace = false; - while i < chars.len() { - let ch = chars[i]; - if ch == 'W' { - is_after_whitespace = true; - } else if ch == '1' && !is_forced_grade1(&chars, i) { - if is_standing_alone_already_encountered || - ((found_g1 || !is_after_whitespace) && !is_single_letter_on_right(&chars, i)) { - return false; - } - found_g1 = true; - is_standing_alone_already_encountered = true; + let body = braille.replace(['C', '𝐶'], ""); + const G1_START: &str = "⠰⠰⠰"; + const G1_END: &str = "⠰⠄"; + if let Some(rest) = body.strip_prefix(G1_START) { + if let Some(mid) = rest.strip_suffix(G1_END) { + return format!("{G1_START}CCC{mid}Ce{G1_END}"); } - i += 1; } - return true; + return format!("CCC{body}Ce"); } /// Return true if the sequence of chars forces a '1' at the `i`th position @@ -1224,58 +1170,6 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str return false; } - fn is_single_letter_on_right(chars: &[char], i: usize) -> bool { - fn is_skip_char(ch: char) -> bool { - matches!(ch, 'B' | 'I' | '𝔹' | 'S' | 'T' | 'D' | 'C' | '𝐶' | 's' | 'w') - } - - // find the first char (if any) - let mut count = 0; // how many letters - let mut i = i+1; - while i < chars.len() { - let ch = chars[i]; - if !is_skip_char(ch) { - if ch == 'L' { - if count == 1 { - return false; // found a second letter in the sequence - } - count += 1; - } else { - return count==1; - } - i += 2; // eat 'L' and actual letter - } else { - i += 1; - } - } - return true; - } - - fn try_grade1_word_mode(raw_braille: &str) -> String { - // this isn't quite right, but pretty close -- try splitting at 'W' (words) - // only one of the parts can be in word mode and none of the others can have '1' unless forced - let mut g1_words = Vec::default(); - let mut found_word_mode = false; - for raw_word in raw_braille.split('W') { - let word = remove_unneeded_mode_changes(raw_word, UEB_Mode::Grade2, UEB_Duration::Symbol); - // debug!("try_grade1_word_mode: word='{}'", word); - let word_chars = word.chars().collect::>(); - let needs_word_mode = word_chars.iter().enumerate() - .any(|(i, &ch) | ch == '1' && !is_forced_grade1(&word_chars, i)); - if needs_word_mode { - if found_word_mode { - return "".to_string(); - } - found_word_mode = true; - g1_words.push("⠰⠰".to_string() + &remove_unneeded_mode_changes(raw_word, UEB_Mode::Grade1, UEB_Duration::Word) - ); - } else { - g1_words.push(word); - } - } - return if found_word_mode {g1_words.join("W")} else {"".to_string()}; - } - /// Count the number of non-forced grade 1 indicators needed for a single symbols-sequence. /// (Forced indicators -- 'a-j' following a digit -- are excluded per the GTM 1.7.3 note.) fn grade1_count(raw_word: &str) -> usize { @@ -1290,27 +1184,33 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str /// See GTM 1.7.3. Note: the 1.7.5 two-symbol allowance and contraction-preserving /// word/passage placement for expressions containing English words are only partially /// realized here; those cases depend on separate contraction-rule improvements. - fn gtm_1_7_mode(raw_braille: &str) -> String { + /// `count_src` decides passage vs word vs symbol; changes are applied to `apply_to` + /// (may differ when capital markers were stripped for capital-passage mode). + fn gtm_1_7_mode(count_src: &str, apply_to: &str) -> String { // Count how many symbols-sequences (whitespace-separated) need a non-forced grade 1 indicator. - let n_seq_needing = raw_braille.split('W') + let n_seq_needing = count_src.split('W') .filter(|raw_word| grade1_count(raw_word) >= 1) .count(); // GTM 1.7.3(b): use a grade 1 passage if three or more sequences each need grade 1. if n_seq_needing >= 3 { - let grade1_passage = remove_unneeded_mode_changes(raw_braille, UEB_Mode::Grade1, UEB_Duration::Passage); + let grade1_passage = remove_unneeded_mode_changes(apply_to, UEB_Mode::Grade1, UEB_Duration::Passage); return "⠰⠰⠰".to_string() + &grade1_passage + "⠰⠄"; } // GTM 1.7.3(a): per sequence, allow one grade 1 symbol indicator, or a grade 1 word // indicator if the sequence needs more than one. - let words = raw_braille.split('W') - .map(|raw_word| { - if grade1_count(raw_word) >= 2 { - "⠰⠰".to_string() + &remove_unneeded_mode_changes(raw_word, UEB_Mode::Grade1, UEB_Duration::Word) + let count_words: Vec<&str> = count_src.split('W').collect(); + let apply_words: Vec<&str> = apply_to.split('W').collect(); + assert_eq!(count_words.len(), apply_words.len(), + "gtm_1_7_mode: count_src and apply_to must have the same number of symbols-sequences"); + let words = count_words.iter().zip(apply_words.iter()) + .map(|(&count_word, &apply_word)| { + if grade1_count(count_word) >= 2 { + "⠰⠰".to_string() + &remove_unneeded_mode_changes(apply_word, UEB_Mode::Grade1, UEB_Duration::Word) } else { // 0 or 1 grade 1 indicators: the grade 2 form leaves a single symbol indicator inline - remove_unneeded_mode_changes(raw_word, UEB_Mode::Grade2, UEB_Duration::Symbol) + remove_unneeded_mode_changes(apply_word, UEB_Mode::Grade2, UEB_Duration::Symbol) } }) .collect::>(); @@ -3696,7 +3596,7 @@ mod tests { set_preference("BrailleCode", "UEB")?; let _braille = get_braille("")?; - 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]; + let answers= &[2, 2, 3, 3, 3, 3, 4, 4, 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]; let answers = answers.map(|num| format!("id-{}", num)); debug!("\n\n*** Testing UEB ***"); for (i, answer) in answers.iter().enumerate() { diff --git a/src/prefs.rs b/src/prefs.rs index 44edb6994..4dc51c4bc 100644 --- a/src/prefs.rs +++ b/src/prefs.rs @@ -79,7 +79,6 @@ impl Preferences{ prefs.insert("BrailleCode".to_string(), Yaml::String("Nemeth".to_string())); prefs.insert("BrailleNavHighlight".to_string(), Yaml::String("EndPoints".to_string())); prefs.insert("UEB_START_MODE".to_string(), Yaml::String("Grade2".to_string())); - prefs.insert("UEB_G1_BANA_Style".to_string(), Yaml::Boolean(true)); prefs.insert("DecimalSeparators".to_string(), Yaml::String(".".to_string())); prefs.insert("BlockSeparators".to_string(), Yaml::String(", \u{00A0}\u{202F}".to_string())); diff --git a/tests/braille/UEB/iceb.rs b/tests/braille/UEB/iceb.rs index 3eb19deea..86a1dbb34 100644 --- a/tests/braille/UEB/iceb.rs +++ b/tests/braille/UEB/iceb.rs @@ -1,7 +1,8 @@ // UEB tests for the basic mathml tags -// Initial tests are from BANA guidelines, mostly about initial chars for code switching -// http://www.brailleauthority.org/ueb/ueb_math_guidance/final_for_posting_ueb_math_guidance_may_2019_102419.pdf -// These tests start with "bana_" +// BANA guidance (2026; replaces 2019 provisional): +// https://www.brailleauthority.org/sites/default/files/2026-07/Guidance%20on%20Transcribing%20Math%20and%20Science%20in%20UEB%202026.pdf +// Older `bana_*` names map from 2019 section numbers where still valid; `bana2026_*` are new 2026 examples. +// Grade 1 indicators follow ICEB GTM §1.7 (adopted by BANA 2026). // // Many come from (refer to) https://iceb.org/guidelines_for_technical_material_2014.pdf // For example, "fraction_6_1_1" is a fraction example from section 6.1, and is the first example there. @@ -10,7 +11,8 @@ use anyhow::Result; #[test] fn bana_2_1() -> Result<()> { - let expr = "6=1×2×3 + // BANA 2026 Ex 2-1 (spacing of signs) + let expr = "6=1×2×3 =1+2+3"; test_braille("UEB", expr, "⠼⠋⠀⠐⠶⠀⠼⠁⠐⠦⠼⠃⠐⠦⠼⠉⠀⠐⠶⠀⠼⠁⠐⠖⠼⠃⠐⠖⠼⠉")?; return Ok(()); @@ -19,6 +21,7 @@ fn bana_2_1() -> Result<()> { #[test] fn bana_5_1() -> Result<()> { + // BANA 2026 Ex 5-1 let expr = "x+y=6"; test_braille("UEB", expr, "⠭⠐⠖⠽⠀⠐⠶⠀⠼⠋")?; return Ok(()); @@ -27,6 +30,7 @@ fn bana_5_1() -> Result<()> { #[test] fn bana_5_2() -> Result<()> { + // BANA 2026 Ex 5-7 let expr = "x2+y2=C"; test_braille("UEB", expr, "⠭⠰⠔⠼⠃⠐⠖⠽⠔⠼⠃⠀⠐⠶⠀⠰⠠⠉")?; return Ok(()); @@ -35,6 +39,7 @@ fn bana_5_2() -> Result<()> { #[test] fn bana_5_3() -> Result<()> { + // BANA 2026 Ex 5-10 let expr = "ab+cd"; test_braille("UEB", expr, "⠰⠰⠷⠁⠨⠌⠃⠾⠐⠖⠷⠉⠨⠌⠙⠾")?; return Ok(()); @@ -43,26 +48,27 @@ fn bana_5_3() -> Result<()> { #[test] fn bana_5_4() -> Result<()> { - let expr = "an×am= + // BANA 2026 Ex 5-14 (GTM 1.7: word indicator per sequence, not passage) + let expr = "an×am= an+m"; - test_braille("UEB", expr, "⠰⠰⠰⠁⠔⠝⠐⠦⠁⠔⠍⠀⠐⠶⠀⠁⠔⠣⠝⠐⠖⠍⠜⠰⠄")?; + test_braille("UEB", expr, "⠰⠰⠁⠔⠝⠐⠦⠁⠔⠍⠀⠐⠶⠀⠰⠰⠁⠔⠣⠝⠐⠖⠍⠜")?; return Ok(()); } #[test] fn bana_5_5() -> Result<()> { - let expr = "logxy"; - // BANA example contradicts GTM 9.2 that says don't use a space after a function name if there is an intervening indicator. - // Corrected: removed the space and the G1 indicator needed if a space were inserted - test_braille("UEB", expr, "⠰⠰⠇⠕⠛⠢⠭⠽")?; + // BANA 2026 Ex 5-8 (replaces 2019 log_x y example) + let expr = "log2x+logx"; + test_braille("UEB", expr, "⠇⠕⠛⠰⠢⠼⠃⠭⠐⠖⠇⠕⠛⠀⠰⠭")?; return Ok(()); } #[test] fn bana_5a_1() -> Result<()> { - let expr = "100°F"; + // Former 2019 §5(a) units example (dropped from BANA 2026); kept as unit-spacing practice + let expr = "100°F"; test_braille("UEB", expr, "⠼⠁⠚⠚⠘⠚⠠⠋")?; return Ok(()); @@ -70,7 +76,17 @@ fn bana_5a_1() -> Result<()> { #[test] fn bana_5a_1_baseline() -> Result<()> { - let expr = "100°F"; + // Former 2019 §5(a) units example (dropped from BANA 2026); kept as unit-spacing practice + let expr = "100°F"; + test_braille("UEB", expr, "⠼⠁⠚⠚⠘⠚⠠⠋")?; + return Ok(()); + +} + +#[test] +fn bana_5a_1_baseline_unit() -> Result<()> { + // Same as bana_5a_1_baseline; unit marked via data-intent-property + let expr = "100°F"; test_braille("UEB", expr, "⠼⠁⠚⠚⠘⠚⠠⠋")?; return Ok(()); @@ -78,22 +94,43 @@ fn bana_5a_1_baseline() -> Result<()> { #[test] fn bana_5a_2() -> Result<()> { + // Former 2019 §5(a) units example (dropped from BANA 2026); kept as unit-spacing practice let expr = "25 km2"; test_braille("UEB", expr, "⠼⠃⠑⠀⠅⠍⠰⠔⠼⠃")?; return Ok(()); } +#[test] +fn bana_5a_2_unit() -> Result<()> { + // Same as bana_5a_2; unit marked via data-intent-property + let expr = "25 km2"; + test_braille("UEB", expr, "⠼⠃⠑⠀⠅⠍⠰⠔⠼⠃")?; + return Ok(()); + +} + #[test] fn bana_5a_2_mtext() -> Result<()> { + // Former 2019 §5(a) units example (dropped from BANA 2026); kept as unit-spacing practice let expr = "25 km2"; test_braille("UEB", expr, "⠼⠃⠑⠀⠅⠍⠰⠔⠼⠃")?; return Ok(()); } +#[test] +fn bana_5a_2_mtext_unit() -> Result<()> { + // Same as bana_5a_2_mtext; unit marked via data-intent-property + let expr = "25 km2"; + test_braille("UEB", expr, "⠼⠃⠑⠀⠅⠍⠰⠔⠼⠃")?; + return Ok(()); + +} + #[test] fn bana_5a_3() -> Result<()> { + // Former 2019 §5(a) units example (dropped from BANA 2026); kept as unit-spacing practice let expr = "6 m  s-1"; test_braille("UEB", expr, "⠼⠋⠀⠰⠍⠀⠰⠰⠎⠔⠣⠐⠤⠼⠁⠜")?; @@ -101,11 +138,183 @@ fn bana_5a_3() -> Result<()> { } +#[test] +fn bana_5a_3_unit() -> Result<()> { + // Same as bana_5a_3; unit marked via data-intent-property + let expr = "6 m  + s-1"; + test_braille("UEB", expr, "⠼⠋⠀⠰⠍⠀⠰⠰⠎⠔⠣⠐⠤⠼⠁⠜")?; + return Ok(()); + +} + #[test] fn bana_6_1() -> Result<()> { + // Punctuation before G1 terminator (BANA 2026 §5.4 / GTM 1.7.3b note) let expr = "x+y=z =t2."; - test_braille("UEB", expr, "⠰⠰⠰⠭⠐⠖⠽⠀⠐⠶⠀⠵⠀⠐⠶⠀⠞⠔⠼⠃⠲⠰⠄")?; + test_braille("UEB", expr, "⠭⠐⠖⠽⠀⠐⠶⠀⠰⠵⠀⠐⠶⠀⠞⠰⠔⠼⠃⠲")?; + return Ok(()); + +} + +// --- BANA 2026 examples not covered by older bana_* tests --- + +#[test] +fn bana2026_2_2() -> Result<()> { + // BANA 2026 Ex 2-2: spaced operation signs for beginning readers + let expr = "6-3=_"; + test_braille_prefs("UEB", vec![("UseSpacesAroundAllOperators", "true")], expr, "⠼⠋⠀⠐⠤⠀⠼⠉⠀⠐⠶⠀⠨⠤")?; + return Ok(()); + +} + +#[test] +fn bana2026_5_2() -> Result<()> { + // BANA 2026 Ex 5-2: words with contractions in a technical expression + let expr = "Surface area=2lh+2lw+2wh"; + test_braille("UEB", expr, "⠠⠎⠥⠗⠋⠁⠉⠑⠀⠜⠑⠁⠀⠐⠶⠀⠼⠃⠇⠓⠐⠖⠼⠃⠇⠺⠐⠖⠼⠃⠺⠓")?; + return Ok(()); + +} + +#[test] +fn bana2026_5_3() -> Result<()> { + // BANA 2026 Ex 5-3 + let expr = "NaCl+H2O"; + test_braille("UEB", expr, "⠠⠝⠁⠠⠉⠇⠐⠖⠠⠓⠰⠢⠼⠃⠠⠕")?; + return Ok(()); + +} + +#[test] +fn bana2026_5_4() -> Result<()> { + // BANA 2026 Ex 5-4 + let expr = "5x2y3z"; + test_braille("UEB", expr, "⠰⠷⠼⠑⠭⠔⠼⠃⠨⠌⠽⠔⠼⠉⠵⠾")?; + return Ok(()); + +} + +#[test] +fn bana2026_5_5() -> Result<()> { + // BANA 2026 Ex 5-5: 1×3 matrix and column (linearized like other matrix_* tests) + let expr = r#" + (abc) + ( + -1 + 2 + -3 + ) + "#; + test_braille("UEB", expr, "⠠⠐⠣⠁⠀⠰⠃⠀⠉⠠⠐⠜⠠⠐⠣⠐⠤⠼⠁⠠⠐⠜⠸⠀⠠⠐⠣⠼⠃⠠⠐⠜⠸⠀⠠⠐⠣⠐⠤⠼⠉⠠⠐⠜")?; + return Ok(()); + +} + +#[test] +fn bana2026_5_6() -> Result<()> { + // BANA 2026 Ex 5-6 + let expr = r#" + (ab) + (-x2y) + "#; + test_braille("UEB", expr, "⠠⠐⠣⠁⠀⠃⠠⠐⠜⠠⠐⠣⠐⠤⠭⠠⠐⠜⠸⠀⠠⠐⠣⠼⠃⠽⠠⠐⠜")?; + return Ok(()); + +} + +#[test] +fn bana2026_5_9_slope() -> Result<()> { + // BANA 2026 Ex 5-9 (math fragments): slope 3/5 and parallel lines l₁ ∥ l₂ + let expr = "l1l2"; + test_braille("UEB", expr, "⠇⠰⠢⠼⠁⠀⠼⠇⠀⠇⠰⠢⠼⠃")?; + return Ok(()); + +} + +#[test] +fn bana2026_5_11() -> Result<()> { + // BANA 2026 Ex 5-11 + let expr = "ab"; + test_braille("UEB", expr, "⠰⠰⠷⠁⠨⠌⠃⠾")?; + return Ok(()); + +} + +#[test] +fn bana2026_5_12() -> Result<()> { + // BANA 2026 Ex 5-12: vector AB with arrow + let expr = "AB"; + test_braille("UEB", expr, "⠰⠰⠣⠠⠠⠁⠃⠜⠘⠱")?; + return Ok(()); + +} + +#[test] +fn bana2026_5_13() -> Result<()> { + // BANA 2026 Ex 5-13 + let expr = "x-1=1x"; + test_braille("UEB", expr, "⠰⠰⠭⠔⠣⠐⠤⠼⠁⠜⠀⠐⠶⠀⠰⠷⠼⠁⠨⠌⠭⠾")?; + return Ok(()); + +} + +#[test] +fn bana2026_5_15() -> Result<()> { + // BANA 2026 Ex 5-15: all-caps print uses capital passage (⠠⠠⠠ … ⠠⠄) inside G1 passage + let expr = "P=VI=I2R=V2R"; + test_braille("UEB", expr, "⠰⠰⠰⠠⠠⠠⠏⠀⠐⠶⠀⠧⠊⠀⠐⠶⠀⠊⠔⠼⠃⠗⠀⠐⠶⠀⠷⠧⠔⠼⠃⠨⠌⠗⠾⠠⠄⠰⠄")?; + return Ok(()); + +} + +#[test] +fn bana2026_5_16() -> Result<()> { + // BANA 2026 Ex 5-16: CaCO₃ ⇄ CaO + CO₂ + let expr = "CaCO3 + + CaO+ + CO2"; + test_braille("UEB", expr, "⠰⠰⠰⠠⠉⠁⠠⠉⠠⠕⠢⠼⠉⠀⠳⠕⠻⠳⠪⠀⠠⠉⠁⠠⠕⠐⠖⠠⠉⠠⠕⠢⠼⠃⠰⠄")?; + return Ok(()); + +} + +#[test] +fn bana2026_7_1() -> Result<()> { + // BANA 2026 Ex 7-1: V = lim Δx→0 Σ f(xᵢ)Δx (single-line) + let expr = r#"V= + limΔx0 + i + f(xi)Δx + "#; + test_braille("UEB", expr, "⠰⠠⠧⠀⠐⠶⠀⠰⠰⠇⠊⠍⠨⠢⠣⠠⠨⠙⠭⠳⠕⠼⠚⠜⠠⠨⠎⠨⠢⠊⠋⠐⠣⠭⠢⠊⠐⠜⠠⠨⠙⠭")?; + return Ok(()); + +} + +#[test] +fn bana2026_7_2() -> Result<()> { + // BANA 2026 Ex 7-2: first step of radical simplification chain + let expr = r#"x= + 36-4(9)+3(12)18 + "#; + test_braille("UEB", expr, "⠰⠭⠀⠐⠶⠀⠰⠰⠷⠩⠼⠉⠋⠐⠤⠼⠙⠐⠣⠼⠊⠐⠜⠐⠖⠼⠉⠐⠣⠼⠁⠃⠐⠜⠬⠨⠌⠼⠁⠓⠾")?; + return Ok(()); + +} + +#[test] +fn bana2026_7_3() -> Result<()> { + // BANA 2026 Ex 7-3 + let expr = r#"x= + + 54+7-111×2+8000+1000 + -42 + + "#; + test_braille("UEB", expr, "⠰⠭⠀⠐⠶⠀⠰⠷⠼⠑⠙⠐⠖⠼⠛⠐⠤⠼⠁⠁⠁⠐⠦⠼⠃⠐⠖⠼⠓⠚⠚⠚⠐⠖⠼⠁⠚⠚⠚⠨⠌⠐⠤⠼⠙⠃⠾")?; return Ok(()); } @@ -167,7 +376,7 @@ fn grade1_1_7_2() -> Result<()> { #[test] fn grade1_1_7_3_1() -> Result<()> { let expr = " - 3x4y+ + 3x4y+ y2 = x2 @@ -193,7 +402,7 @@ fn grade1_1_7_3_2() -> Result<()> { #[test] fn grade1_1_7_4() -> Result<()> { let expr = " - (yx2) + (yx2) "; test_braille("UEB", expr, "⠰⠰⠩⠐⠣⠽⠐⠤⠭⠔⠼⠃⠐⠜⠬")?; return Ok(()); @@ -214,7 +423,7 @@ fn grade1_1_7_3_a_1() -> Result<()> { #[test] fn grade1_1_7_3_a_2() -> Result<()> { - let expr = "x"; + let expr = "x"; test_braille("UEB", expr, "⠰⠭⠀⠰⠳⠕⠀⠼⠿")?; return Ok(()); @@ -251,7 +460,7 @@ fn grade1_1_7_3_b_2() -> Result<()> { fn grade1_1_7_4_1() -> Result<()> { let expr = "x2-x-2 =0"; - test_braille("UEB", expr, "⠭⠔⠼⠃⠐⠤⠭⠐⠤⠼⠃⠀⠐⠶⠀⠼⠚")?; + test_braille("UEB", expr, "⠭⠰⠔⠼⠃⠐⠤⠭⠐⠤⠼⠃⠀⠐⠶⠀⠼⠚")?; return Ok(()); } @@ -260,7 +469,7 @@ fn grade1_1_7_4_1() -> Result<()> { fn grade1_1_7_4_2() -> Result<()> { let expr = "x2-4x-3 =0"; - test_braille("UEB", expr, "⠭⠔⠼⠃⠐⠤⠼⠙⠭⠐⠤⠼⠉⠀⠐⠶⠀⠼⠚")?; + test_braille("UEB", expr, "⠭⠰⠔⠼⠃⠐⠤⠼⠙⠭⠐⠤⠼⠉⠀⠐⠶⠀⠼⠚")?; return Ok(()); } @@ -268,7 +477,16 @@ fn grade1_1_7_4_2() -> Result<()> { #[test] fn grade1_1_7_4_3() -> Result<()> { let expr = "x2-1=0"; - test_braille("UEB", expr, "⠭⠔⠼⠃⠐⠤⠼⠁⠀⠐⠶⠀⠼⠚")?; + test_braille("UEB", expr, "⠭⠰⠔⠼⠃⠐⠤⠼⠁⠀⠐⠶⠀⠼⠚")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_5_a() -> Result<()> { + let expr = "speed= + distancetime"; + test_braille("UEB", expr, "⠎⠏⠑⠫⠀⠐⠶⠀⠰⠰⠷⠙⠊⠎⠞⠁⠝⠉⠑⠨⠌⠞⠊⠍⠑⠾")?; return Ok(()); } @@ -276,7 +494,7 @@ fn grade1_1_7_4_3() -> Result<()> { #[test] fn grade1_1_7_5_b() -> Result<()> { let expr = "luminositysun"; - test_braille("UEB", expr, "⠇⠥⠍⠔⠕⠎⠰⠽⠰⠰⠢⠣⠎⠥⠝⠜")?; + test_braille("UEB", expr, "⠰⠰⠇⠥⠍⠊⠝⠕⠎⠊⠞⠽⠢⠣⠎⠥⠝⠜")?; return Ok(()); } @@ -285,16 +503,27 @@ fn grade1_1_7_5_b() -> Result<()> { fn grade1_1_7_5_c() -> Result<()> { let expr = "speed= distancetime= - - 30,000 m - 60 s - = + 30,000 m + 60 s= 500 m/s"; test_braille("UEB", expr, "⠎⠏⠑⠫⠀⠐⠶⠀⠰⠰⠰⠷⠙⠊⠎⠞⠁⠝⠉⠑⠨⠌⠞⠊⠍⠑⠾⠀⠐⠶⠀⠷⠼⠉⠚⠂⠚⠚⠚⠀⠍⠨⠌⠼⠋⠚⠀⠎⠾⠀⠐⠶⠀⠼⠑⠚⠚⠀⠍⠸⠌⠎⠰⠄")?; return Ok(()); } +#[test] +fn grade1_1_7_5_c_unit() -> Result<()> { + // Same as grade1_1_7_5_c; unit marked via data-intent-property + let expr = "speed= + distancetime= + 30,000 m + 60 s= + 500 m/s"; + test_braille("UEB", expr, "⠎⠏⠑⠫⠀⠐⠶⠀⠰⠰⠰⠷⠙⠊⠎⠞⠁⠝⠉⠑⠨⠌⠞⠊⠍⠑⠾⠀⠐⠶⠀⠷⠼⠉⠚⠂⠚⠚⠚⠀⠍⠨⠌⠼⠋⠚⠀⠎⠾⠀⠐⠶⠀⠼⠑⠚⠚⠀⠍⠸⠌⠎⠰⠄")?; + return Ok(()); + +} + #[test] fn grade1_1_7_9_1() -> Result<()> { let expr = "y=x"; @@ -321,12 +550,31 @@ fn grade1_1_7_9_3() -> Result<()> { #[test] fn grade1_1_7_9_4() -> Result<()> { - let expr = "ms-1"; + let expr = "ms + -1"; + test_braille("UEB", expr, "⠰⠰⠍⠎⠔⠣⠐⠤⠼⠁⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_4_unit() -> Result<()> { + // Same as grade1_1_7_9_4; unit marked via data-intent-property + let expr = "ms + -1"; test_braille("UEB", expr, "⠰⠰⠍⠎⠔⠣⠐⠤⠼⠁⠜")?; return Ok(()); } +#[test] +fn grade1_1_7_9_5() -> Result<()> { + let expr = "y=x2"; + test_braille("UEB", expr, "⠰⠽⠀⠐⠶⠀⠰⠰⠷⠭⠨⠌⠼⠃⠾")?; + return Ok(()); + +} + #[test] fn grade1_1_7_9_6() -> Result<()> { let expr = "y=x22"; @@ -370,7 +618,7 @@ fn grade1_1_7_9_10() -> Result<()> { #[test] fn grade1_1_7_9_11() -> Result<()> { - let expr = "B¯"; + let expr = "B¯"; test_braille("UEB", expr, "⠠⠃⠰⠱")?; return Ok(()); @@ -378,7 +626,7 @@ fn grade1_1_7_9_11() -> Result<()> { #[test] fn grade1_1_7_9_12() -> Result<()> { - let expr = "M"; + let expr = "M"; test_braille("UEB", expr, "⠠⠍⠨⠔⠰⠳⠺⠗⠕")?; return Ok(()); @@ -386,7 +634,8 @@ fn grade1_1_7_9_12() -> Result<()> { #[test] fn grade1_1_7_9_13() -> Result<()> { - let expr = "AZ"; + let expr = "AZ + "; test_braille("UEB", expr, "⠰⠰⠣⠠⠠⠁⠵⠜⠨⠔⠳⠺⠗⠕")?; return Ok(()); @@ -402,7 +651,7 @@ fn grade1_1_7_9_14() -> Result<()> { #[test] fn grade1_1_7_9_15() -> Result<()> { - let expr = "O2-"; + let expr = "O2-"; test_braille("UEB", expr, "⠰⠰⠠⠕⠔⠣⠼⠃⠐⠤⠜")?; return Ok(()); @@ -413,7 +662,7 @@ fn grade1_1_7_9_16() -> Result<()> { let expr = "2H2 (g)+ O2 - (g) + (g) 2H2 O(l)"; test_braille("UEB", expr, "⠼⠃⠠⠓⠢⠼⠃⠐⠣⠛⠐⠜⠐⠖⠠⠕⠢⠼⠃⠐⠣⠛⠐⠜⠀⠰⠳⠕⠀⠼⠃⠠⠓⠢⠼⠃⠠⠕⠐⠣⠇⠐⠜")?; @@ -431,7 +680,7 @@ fn grade1_1_7_9_17() -> Result<()> { #[test] fn grade1_1_7_9_18() -> Result<()> { - let expr = "A=πr2"; + let expr = "A=πr2"; test_braille("UEB", expr, "⠠⠁⠀⠐⠶⠀⠨⠏⠗⠰⠔⠼⠃")?; return Ok(()); @@ -472,9 +721,24 @@ fn grade1_1_7_9_22() -> Result<()> { } +#[test] +fn grade1_1_7_9_23() -> Result<()> { + let expr = "x= + + b± + b24ac + + 2a + + "; + test_braille("UEB", expr, "⠰⠭⠀⠐⠶⠀⠰⠰⠷⠐⠤⠃⠸⠖⠩⠃⠔⠼⠃⠐⠤⠼⠙⠰⠁⠉⠬⠨⠌⠼⠃⠰⠁⠾")?; + return Ok(()); + +} + #[test] fn grade1_1_7_9_24() -> Result<()> { - let expr = "xsinθ"; + let expr = "xsinθ"; test_braille("UEB", expr, "⠰⠰⠭⠩⠎⠊⠝⠨⠹⠬")?; return Ok(()); @@ -483,7 +747,7 @@ fn grade1_1_7_9_24() -> Result<()> { #[test] fn grade1_1_7_9_25() -> Result<()> { let expr = "Version1b"; - test_braille("UEB", expr, "⠠⠧⠻⠨⠝⠰⠰⠔⠣⠼⠁⠰⠃⠜")?; + test_braille("UEB", expr, "⠰⠰⠠⠧⠑⠗⠎⠊⠕⠝⠔⠣⠼⠁⠰⠃⠜")?; return Ok(()); } @@ -498,15 +762,15 @@ fn grade1_1_7_9_26() -> Result<()> { #[test] fn grade1_1_7_9_27() -> Result<()> { - let expr = "work=force×distance"; - test_braille("UEB", expr, "⠐⠺⠀⠐⠶⠀⠿⠉⠑⠐⠦⠙⠊⠌⠨⠑")?; + let expr = "work=force×distance"; + test_braille("UEB", expr, "⠐⠺⠀⠐⠶⠀⠿⠉⠑⠐⠦⠲⠞⠨⠑")?; return Ok(()); } #[test] fn grade1_1_7_9_28() -> Result<()> { - let expr = " force= + let expr = "force= workdistance"; test_braille("UEB", expr, "⠰⠠⠡⠀⠿⠉⠑⠀⠐⠶⠀⠰⠷⠐⠺⠨⠌⠙⠊⠌⠨⠑⠰⠾")?; return Ok(()); @@ -516,10 +780,8 @@ fn grade1_1_7_9_28() -> Result<()> { #[test] fn grade1_1_7_9_29() -> Result<()> { let expr = "acceleration= - - Δspeed - Δtime - "; + Δspeed + Δtime"; test_braille("UEB", expr, "⠁⠒⠑⠇⠻⠁⠰⠝⠀⠐⠶⠀⠰⠷⠠⠨⠙⠎⠏⠑⠫⠨⠌⠠⠨⠙⠐⠞⠰⠾")?; return Ok(()); @@ -544,7 +806,7 @@ fn grade1_1_7_9_31() -> Result<()> { #[test] fn grade1_1_7_9_32() -> Result<()> { let expr = "Vcone= - 13π + 13π r2h"; test_braille("UEB", expr, "⠰⠰⠠⠧⠢⠣⠉⠕⠝⠑⠜⠀⠐⠶⠀⠼⠁⠌⠉⠨⠏⠗⠔⠼⠃⠰⠓")?; return Ok(()); @@ -553,7 +815,7 @@ fn grade1_1_7_9_32() -> Result<()> { #[test] fn grade1_1_7_9_33() -> Result<()> { - let expr = "V=π + let expr = "V=π y2dx"; test_braille("UEB", expr, "⠰⠠⠧⠀⠐⠶⠀⠰⠰⠮⠨⠏⠽⠔⠼⠃⠰⠙⠭")?; return Ok(()); @@ -562,9 +824,9 @@ fn grade1_1_7_9_33() -> Result<()> { #[test] fn grade1_1_7_9_34() -> Result<()> { - let expr = "period=2π + let expr = "period=2π - lcosα + lcosα g "; test_braille("UEB", expr, "⠏⠻⠊⠕⠙⠀⠐⠶⠀⠼⠃⠨⠏⠩⠷⠇⠀⠰⠰⠉⠕⠎⠨⠁⠨⠌⠛⠾⠬")?; @@ -574,9 +836,21 @@ fn grade1_1_7_9_34() -> Result<()> { #[test] fn grade1_1_7_9_35() -> Result<()> { - let expr = "g=9.81  - ms-2"; - test_braille("UEB", expr, "⠰⠛⠀⠐⠶⠀⠼⠊⠲⠓⠁⠀⠰⠰⠍⠎⠔⠣⠐⠤⠼⠃⠜")?; + let expr = "Assume g=9.81  + ms + -2"; + test_braille("UEB", expr, "⠠⠁⠎⠎⠥⠍⠑⠀⠰⠛⠀⠐⠶⠀⠼⠊⠲⠓⠁⠀⠰⠰⠍⠎⠔⠣⠐⠤⠼⠃⠜")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_35_unit() -> Result<()> { + // Same as grade1_1_7_9_35; unit marked via data-intent-property + let expr = "Assume g=9.81  + ms + -2"; + test_braille("UEB", expr, "⠠⠁⠎⠎⠥⠍⠑⠀⠰⠛⠀⠐⠶⠀⠼⠊⠲⠓⠁⠀⠰⠰⠍⠎⠔⠣⠐⠤⠼⠃⠜")?; return Ok(()); } @@ -584,7 +858,19 @@ fn grade1_1_7_9_35() -> Result<()> { #[test] fn grade1_1_7_9_36() -> Result<()> { let expr = "v1=x  - ms-1"; + ms + -1"; + test_braille("UEB", expr, "⠰⠰⠰⠧⠢⠼⠁⠀⠐⠶⠀⠭⠀⠍⠎⠔⠣⠐⠤⠼⠁⠜⠰⠄")?; + return Ok(()); + +} + +#[test] +fn grade1_1_7_9_36_unit() -> Result<()> { + // Same as grade1_1_7_9_36; unit marked via data-intent-property + let expr = "v1=x  + ms + -1"; test_braille("UEB", expr, "⠰⠰⠰⠧⠢⠼⠁⠀⠐⠶⠀⠭⠀⠍⠎⠔⠣⠐⠤⠼⠁⠜⠰⠄")?; return Ok(()); @@ -610,7 +896,7 @@ fn grade1_1_7_9_38() -> Result<()> { #[test] fn grade1_1_7_9_39() -> Result<()> { - let expr = "x2×x3 + let expr = "x2×x3 =x2+3 =x5"; test_braille("UEB", expr, "⠰⠰⠰⠭⠔⠼⠃⠐⠦⠭⠔⠼⠉⠀⠐⠶⠀⠭⠔⠣⠼⠃⠐⠖⠼⠉⠜⠀⠐⠶⠀⠭⠔⠼⠑⠰⠄")?; @@ -620,9 +906,9 @@ fn grade1_1_7_9_39() -> Result<()> { #[test] fn grade1_1_7_9_40() -> Result<()> { - let expr = "am×an + let expr = "am×an =am+n"; - test_braille("UEB", expr, "⠁⠔⠍⠐⠦⠁⠔⠝⠀⠐⠶⠀⠁⠔⠣⠍⠐⠖⠝⠜")?; + test_braille("UEB", expr, "⠰⠰⠁⠔⠍⠐⠦⠁⠔⠝⠀⠐⠶⠀⠰⠰⠁⠔⠣⠍⠐⠖⠝⠜")?; return Ok(()); } @@ -631,7 +917,7 @@ fn grade1_1_7_9_40() -> Result<()> { fn grade1_1_7_9_41() -> Result<()> { let expr = "(am)n =amn"; - test_braille("UEB", expr, "⠐⠣⠁⠔⠍⠐⠜⠔⠝⠀⠐⠶⠀⠁⠔⠣⠍⠝⠜")?; + test_braille("UEB", expr, "⠰⠰⠐⠣⠁⠔⠍⠐⠜⠔⠝⠀⠐⠶⠀⠰⠰⠁⠔⠣⠍⠝⠜")?; return Ok(()); } @@ -640,7 +926,7 @@ fn grade1_1_7_9_41() -> Result<()> { fn grade1_1_7_9_42() -> Result<()> { let expr = "aman =am-n"; - test_braille("UEB", expr, "⠷⠁⠔⠍⠨⠌⠁⠔⠝⠾⠀⠐⠶⠀⠁⠔⠣⠍⠐⠤⠝⠜")?; + test_braille("UEB", expr, "⠰⠰⠷⠁⠔⠍⠨⠌⠁⠔⠝⠾⠀⠐⠶⠀⠰⠰⠁⠔⠣⠍⠐⠤⠝⠜")?; return Ok(()); } @@ -649,7 +935,7 @@ fn grade1_1_7_9_42() -> Result<()> { fn grade1_1_7_9_43() -> Result<()> { let expr = "a-m =1am"; - test_braille("UEB", expr, "⠁⠔⠣⠐⠤⠍⠜⠀⠐⠶⠀⠷⠼⠁⠨⠌⠁⠔⠍⠾")?; + test_braille("UEB", expr, "⠰⠰⠁⠔⠣⠐⠤⠍⠜⠀⠐⠶⠀⠰⠷⠼⠁⠨⠌⠁⠔⠍⠾")?; return Ok(()); } @@ -657,7 +943,7 @@ fn grade1_1_7_9_43() -> Result<()> { #[test] fn grade1_1_7_9_44() -> Result<()> { let expr = "a0=1"; - test_braille("UEB", expr, "⠁⠔⠼⠚⠀⠐⠶⠀⠼⠁")?; + test_braille("UEB", expr, "⠁⠰⠔⠼⠚⠀⠐⠶⠀⠼⠁")?; return Ok(()); } @@ -666,14 +952,14 @@ fn grade1_1_7_9_44() -> Result<()> { fn grade1_1_7_9_45() -> Result<()> { let expr = "a1n =an"; - test_braille("UEB", expr, "⠁⠔⠷⠼⠁⠨⠌⠝⠾⠀⠐⠶⠀⠩⠔⠝⠁⠬")?; + test_braille("UEB", expr, "⠰⠰⠁⠔⠷⠼⠁⠨⠌⠝⠾⠀⠐⠶⠀⠰⠰⠩⠔⠝⠁⠬")?; return Ok(()); } #[test] fn grade1_1_7_9_46() -> Result<()> { - let expr = "BC"; + let expr = "BC"; test_braille("UEB", expr, "⠰⠰⠰⠠⠃⠀⠳⠕⠀⠠⠉⠰⠄")?; return Ok(()); @@ -691,7 +977,7 @@ fn grade1_1_7_9_47() -> Result<()> { fn grade1_1_7_9_48() -> Result<()> { let expr = "CH4 +4Cl2 - + CCl4 +4HCl"; test_braille("UEB", expr, "⠰⠰⠰⠠⠉⠠⠓⠢⠼⠙⠐⠖⠼⠙⠠⠉⠇⠢⠼⠃⠀⠳⠕⠀⠠⠉⠠⠉⠇⠢⠼⠙⠐⠖⠼⠙⠠⠓⠠⠉⠇⠰⠄")?; @@ -702,10 +988,10 @@ fn grade1_1_7_9_48() -> Result<()> { #[test] fn grade1_1_7_9_49() -> Result<()> { let expr = "614C - + 714N + - -10β"; + -10β"; test_braille("UEB", expr, "⠰⠰⠰⠢⠼⠋⠔⠼⠁⠙⠠⠉⠀⠳⠕⠀⠢⠼⠛⠔⠼⠁⠙⠠⠝⠐⠖⠢⠣⠐⠤⠼⠁⠜⠔⠼⠚⠨⠃⠰⠄")?; return Ok(()); @@ -716,7 +1002,7 @@ fn grade1_1_7_9_50() -> Result<()> { let expr = "reliability= number of faultstotal number of items =p"; - test_braille("UEB", expr, "⠗⠑⠇⠊⠁⠃⠊⠇⠰⠽⠀⠐⠶⠀⠰⠰⠰⠷⠝⠥⠍⠃⠑⠗⠀⠕⠋⠀⠋⠁⠥⠇⠞⠎⠀⠨⠌⠞⠕⠞⠁⠇⠀⠝⠥⠍⠃⠑⠗⠀⠕⠋⠀⠊⠞⠑⠍⠎⠾⠀⠐⠶⠀⠏⠰⠄")?; + test_braille("UEB", expr, "⠰⠰⠰⠗⠑⠇⠊⠁⠃⠊⠇⠊⠞⠽⠀⠐⠶⠀⠷⠝⠥⠍⠃⠑⠗⠀⠕⠋⠀⠋⠁⠥⠇⠞⠎⠨⠌⠞⠕⠞⠁⠇⠀⠝⠥⠍⠃⠑⠗⠀⠕⠋⠀⠊⠞⠑⠍⠎⠾⠀⠐⠶⠀⠏⠰⠄")?; return Ok(()); } @@ -834,7 +1120,7 @@ fn bold_2_7_3() -> Result<()> { fn signs_2_10_2() -> Result<()> { let expr = "$0.30,  30c  - or 30¢"; + or 30¢"; test_braille("UEB", expr, "⠈⠎⠼⠚⠲⠉⠚⠂⠀⠼⠉⠚⠰⠉⠀⠕⠗⠀⠼⠉⠚⠈⠉")?; return Ok(()); @@ -844,7 +1130,7 @@ fn signs_2_10_2() -> Result<()> { fn signs_2_10_5() -> Result<()> { let expr = "1 ft 6 in  or  - 1 6"; + 1 6"; test_braille("UEB", expr, "⠼⠁⠀⠋⠞⠀⠼⠋⠀⠔⠀⠕⠗⠀⠼⠁⠶⠀⠼⠋⠶⠶")?; return Ok(()); @@ -852,8 +1138,8 @@ fn signs_2_10_5() -> Result<()> { #[test] fn signs_2_10_8() -> Result<()> { - let expr = "0°C or -  32°F"; + let expr = "0°C or +  32°F"; test_braille("UEB", expr, "⠼⠚⠘⠚⠠⠉⠀⠕⠗⠀⠼⠉⠃⠘⠚⠠⠋")?; return Ok(()); @@ -861,9 +1147,9 @@ fn signs_2_10_8() -> Result<()> { #[test] fn signs_2_10_16() -> Result<()> { - let expr = "1 Å= + let expr = "1 Å= 110,000  - μ"; + μ"; test_braille("UEB", expr, "⠼⠁⠀⠠⠘⠫⠁⠀⠐⠶⠀⠼⠁⠌⠁⠚⠂⠚⠚⠚⠀⠨⠍")?; return Ok(()); @@ -905,7 +1191,7 @@ fn expr_3_1_2() -> Result<()> { #[test] fn expr_3_1_3() -> Result<()> { - let expr = "3×5=5×3=15"; + let expr = "3×5=5×3=15"; test_braille("UEB", expr, "⠼⠉⠐⠦⠼⠑⠀⠐⠶⠀⠼⠑⠐⠦⠼⠉⠀⠐⠶⠀⠼⠁⠑")?; return Ok(()); @@ -914,7 +1200,7 @@ fn expr_3_1_3() -> Result<()> { #[test] fn expr_3_1_6() -> Result<()> { // example includes spaces, so does the MathML (from WIRIS) - let expr = "5.72 m÷10= + let expr = "5.72 m÷10= 57.2 cm"; test_braille("UEB", expr, "⠼⠑⠲⠛⠃⠀⠍⠐⠌⠼⠁⠚⠀⠐⠶⠀⠼⠑⠛⠲⠃⠀⠉⠍")?; return Ok(()); @@ -923,7 +1209,7 @@ fn expr_3_1_6() -> Result<()> { #[test] fn expr_3_1_7() -> Result<()> { - let expr = "15±0.5"; + let expr = "15±0.5"; test_braille("UEB", expr, "⠼⠁⠑⠸⠖⠼⠚⠲⠑")?; return Ok(()); @@ -932,7 +1218,7 @@ fn expr_3_1_7() -> Result<()> { #[test] fn expr_3_1_8() -> Result<()> { let expr = "Area=bh= - 5·3=15"; + 5·3=15"; test_braille("UEB", expr, "⠠⠜⠑⠁⠀⠐⠶⠀⠃⠓⠀⠐⠶⠀⠼⠑⠐⠲⠼⠉⠀⠐⠶⠀⠼⠁⠑")?; return Ok(()); @@ -940,7 +1226,7 @@ fn expr_3_1_8() -> Result<()> { #[test] fn expr_3_1_9_wiris() -> Result<()> { - let expr = "3.9×4.116"; + let expr = "3.9×4.116"; test_braille("UEB", expr, "⠼⠉⠲⠊⠐⠦⠼⠙⠲⠁⠀⠸⠔⠀⠼⠁⠋")?; return Ok(()); @@ -948,7 +1234,7 @@ fn expr_3_1_9_wiris() -> Result<()> { #[test] fn expr_3_1_9() -> Result<()> { - let expr = "3.9×4.116"; + let expr = "3.9×4.116"; test_braille("UEB", expr, "⠼⠉⠲⠊⠐⠦⠼⠙⠲⠁⠀⠸⠔⠀⠼⠁⠋")?; return Ok(()); @@ -956,7 +1242,7 @@ fn expr_3_1_9() -> Result<()> { #[test] fn expr_3_1_10() -> Result<()> { - let expr = "5-33-5"; + let expr = "5-33-5"; test_braille("UEB", expr, "⠼⠑⠐⠤⠼⠉⠀⠐⠶⠈⠱⠀⠼⠉⠐⠤⠼⠑")?; return Ok(()); @@ -980,7 +1266,7 @@ fn ratio_3_1_12() -> Result<()> { #[test] fn alg_3_2_1_1() -> Result<()> { - let expr = "yx"; + let expr = "yx"; test_braille("UEB", expr, "⠰⠽⠀⠸⠐⠶⠀⠰⠭")?; return Ok(()); @@ -996,8 +1282,8 @@ fn alg_3_2_1_2() -> Result<()> { #[test] fn alg_3_2_2() -> Result<()> { - let expr = "0θ - 2π"; + let expr = "0θ + 2π"; test_braille("UEB", expr, "⠼⠚⠀⠸⠈⠣⠀⠨⠹⠀⠸⠈⠣⠀⠼⠃⠨⠏")?; return Ok(()); @@ -1023,10 +1309,7 @@ fn alg_3_2_4() -> Result<()> { #[test] fn alg_3_2_5() -> Result<()> { let expr = "d+ab=ac"; - // BANA says use a word indicator if G1 not in first 3 cells (it is after the '='); use passage if >=2 whitespace - // This seems like a poor choice in this case since there is only one G1 indicator, but that's the BANA guidance so... - // "⠰⠰⠰⠙⠐⠖⠁⠃⠀⠐⠶⠀⠁⠉⠰⠄" - // GTM says to use the following and it is more sensisble, so I'm going with it + // GTM 1.7 / BANA 2026: one G1 symbol for standing-alone 'a' after '=' test_braille("UEB", expr, "⠙⠐⠖⠁⠃⠀⠐⠶⠀⠰⠁⠉")?; return Ok(()); @@ -1062,10 +1345,10 @@ fn example_3_4_1() -> Result<()> { fn example_3_4_2() -> Result<()> { // removed some cruft from TeX output of {}^{-}2+{}^{+}3, but the basics are preserved let expr = " - + 2 + - + 3 "; test_braille("UEB", expr, "⠰⠔⠐⠤⠼⠃⠐⠖⠔⠐⠤⠼⠉")?; @@ -1099,7 +1382,7 @@ fn omission_3_6_3() -> Result<()> { #[test] fn omission_3_6_4() -> Result<()> { - let expr = "37=10"; + let expr = "37=10"; test_braille("UEB", expr, "⠼⠉⠫⠼⠙⠱⠼⠛⠀⠐⠶⠀⠼⠁⠚")?; return Ok(()); @@ -1158,9 +1441,21 @@ fn fraction_6_2_1() -> Result<()> { #[test] fn fraction_6_2_2() -> Result<()> { let expr = "1750 -  cm= +  cm= 134 -  m"; +  m"; + test_braille("UEB", expr, "⠼⠁⠛⠑⠚⠀⠉⠍⠀⠐⠶⠀⠼⠁⠼⠉⠌⠙⠀⠰⠍")?; + return Ok(()); + +} + +#[test] +fn fraction_6_2_2_unit() -> Result<()> { + // Same as fraction_6_2_2; unit marked via data-intent-property + let expr = "1750 +  cm= + 134 +  m"; test_braille("UEB", expr, "⠼⠁⠛⠑⠚⠀⠉⠍⠀⠐⠶⠀⠼⠁⠼⠉⠌⠙⠀⠰⠍")?; return Ok(()); @@ -1169,9 +1464,21 @@ fn fraction_6_2_2() -> Result<()> { #[test] fn fraction_6_2_2_unicode_frac() -> Result<()> { let expr = "1750 -  cm= - 1¾ -  m"; +  cm= + 1¾ +  m"; + test_braille("UEB", expr, "⠼⠁⠛⠑⠚⠀⠉⠍⠀⠐⠶⠀⠼⠁⠼⠉⠌⠙⠀⠰⠍")?; + return Ok(()); + +} + +#[test] +fn fraction_6_2_2_unicode_frac_unit() -> Result<()> { + // Same as fraction_6_2_2_unicode_frac; unit marked via data-intent-property + let expr = "1750 +  cm= + 1¾ +  m"; test_braille("UEB", expr, "⠼⠁⠛⠑⠚⠀⠉⠍⠀⠐⠶⠀⠼⠁⠼⠉⠌⠙⠀⠰⠍")?; return Ok(()); @@ -1188,7 +1495,7 @@ fn fraction_6_3_1() -> Result<()> { #[test] fn fraction_6_4_1() -> Result<()> { let expr = "y=x2"; - test_braille("UEB", expr, "⠰⠰⠰⠽⠀⠐⠶⠀⠷⠭⠨⠌⠼⠃⠾⠰⠄")?; + test_braille("UEB", expr, "⠰⠽⠀⠐⠶⠀⠰⠰⠷⠭⠨⠌⠼⠃⠾")?; return Ok(()); } @@ -1287,7 +1594,7 @@ fn msup_7_3_7() -> Result<()> { #[test] fn msup_7_3_11() -> Result<()> { let expr = "xaby=x"; - test_braille("UEB", expr, "⠰⠰⠰⠭⠔⠷⠁⠨⠌⠃⠾⠽⠀⠐⠶⠀⠭⠰⠄")?; + test_braille("UEB", expr, "⠰⠰⠭⠔⠷⠁⠨⠌⠃⠾⠽⠀⠐⠶⠀⠰⠭")?; return Ok(()); } @@ -1319,7 +1626,7 @@ fn msub_7_4_3() -> Result<()> { #[test] fn msup_7_5_1() -> Result<()> { let expr = "0.0045= - 4.5×10-3 + 4.5×10-3 "; test_braille("UEB", expr, "⠼⠚⠲⠚⠚⠙⠑⠀⠐⠶⠀⠼⠙⠲⠑⠐⠦⠼⠁⠚⠔⠣⠐⠤⠼⠉⠜")?; return Ok(()); @@ -1345,7 +1652,7 @@ fn msup_7_6_2() -> Result<()> { #[test] fn msubsup_7_7_1() -> Result<()> { let expr = "x12=y23"; - test_braille("UEB", expr, "⠰⠰⠰⠭⠢⠼⠁⠔⠼⠃⠀⠐⠶⠀⠽⠢⠼⠃⠔⠼⠉⠰⠄")?; + test_braille("UEB", expr, "⠭⠰⠢⠼⠁⠔⠼⠃⠀⠐⠶⠀⠽⠰⠢⠼⠃⠔⠼⠉")?; return Ok(()); } @@ -1373,7 +1680,7 @@ fn pre_sup_7_8_2() -> Result<()> { +3- =5- "; - test_braille("UEB", expr, "⠰⠰⠰⠔⠐⠤⠼⠃⠐⠖⠔⠐⠤⠼⠉⠀⠐⠶⠀⠔⠐⠤⠼⠑⠰⠄")?; + test_braille("UEB", expr, "⠰⠔⠐⠤⠼⠃⠐⠖⠔⠐⠤⠼⠉⠀⠐⠶⠀⠰⠔⠐⠤⠼⠑")?; return Ok(()); } @@ -1381,9 +1688,9 @@ fn pre_sup_7_8_2() -> Result<()> { #[test] fn sum_7_9_1() -> Result<()> { - let expr = "x=1n + let expr = "x=1n xi2"; - test_braille("UEB", expr, "⠰⠰⠠⠨⠎⠨⠢⠣⠭⠐⠶⠼⠁⠜⠨⠔⠝⠭⠢⠊⠔⠼⠃")?; + test_braille("UEB", expr, "⠠⠨⠎⠨⠢⠰⠣⠭⠐⠶⠼⠁⠜⠨⠔⠝⠭⠢⠊⠔⠼⠃")?; return Ok(()); } @@ -1391,7 +1698,7 @@ fn sum_7_9_1() -> Result<()> { #[test] fn lim_7_9_2() -> Result<()> { // Note: modified because passage indicator is not needed (same expr when word indicator is used) - let expr = "limxa + let expr = "limxa f(x)=1"; test_braille("UEB", expr, "⠰⠰⠇⠊⠍⠨⠢⠣⠭⠳⠕⠁⠜⠋⠐⠣⠭⠐⠜⠀⠐⠶⠀⠼⠁")?; return Ok(()); @@ -1410,7 +1717,7 @@ fn sqrt_8_1_1() -> Result<()> { fn sqrt_8_1_2() -> Result<()> { let expr = "r= x2+y2"; - test_braille("UEB", expr, "⠰⠰⠰⠗⠀⠐⠶⠀⠩⠭⠔⠼⠃⠐⠖⠽⠔⠼⠃⠬⠰⠄")?; + test_braille("UEB", expr, "⠰⠗⠀⠐⠶⠀⠰⠰⠩⠭⠔⠼⠃⠐⠖⠽⠔⠼⠃⠬")?; return Ok(()); } @@ -1420,7 +1727,7 @@ fn sqrt_8_1_3() -> Result<()> { let expr = " - 783.2×6.547 + 783.2×6.547 0.4628 @@ -1436,17 +1743,17 @@ fn sqrt_8_1_4() -> Result<()> { x = - b± + b± b 2 - 4ac + 4ac 2a "; - test_braille("UEB", expr, "⠰⠰⠰⠭⠀⠐⠶⠀⠷⠐⠤⠃⠸⠖⠩⠃⠔⠼⠃⠐⠤⠼⠙⠰⠁⠉⠬⠨⠌⠼⠃⠰⠁⠾⠰⠄")?; + test_braille("UEB", expr, "⠰⠭⠀⠐⠶⠀⠰⠰⠷⠐⠤⠃⠸⠖⠩⠃⠔⠼⠃⠐⠤⠼⠙⠰⠁⠉⠬⠨⠌⠼⠃⠰⠁⠾")?; return Ok(()); } @@ -1467,7 +1774,7 @@ fn root_8_2_2() -> Result<()> { y3+ z3 3"; - test_braille("UEB", expr, "⠰⠰⠰⠟⠀⠐⠶⠀⠩⠔⠼⠉⠭⠔⠼⠉⠐⠖⠽⠔⠼⠉⠐⠖⠵⠔⠼⠉⠬⠰⠄")?; + test_braille("UEB", expr, "⠰⠟⠀⠐⠶⠀⠰⠰⠩⠔⠼⠉⠭⠔⠼⠉⠐⠖⠽⠔⠼⠉⠐⠖⠵⠔⠼⠉⠬")?; return Ok(()); } @@ -1513,7 +1820,7 @@ fn spacing_9_3_1_1() -> Result<()> { #[test] fn spacing_9_3_1_2() -> Result<()> { - let expr = "3tan45°"; + let expr = "3tan45°"; test_braille("UEB", expr, "⠼⠉⠞⠁⠝⠼⠙⠑⠘⠚")?; return Ok(()); @@ -1537,7 +1844,7 @@ fn spacing_9_3_2_1() -> Result<()> { #[test] fn spacing_9_3_2_2() -> Result<()> { - let expr = "sinθ"; + let expr = "sinθ"; test_braille("UEB", expr, "⠎⠔⠨⠹")?; return Ok(()); @@ -1612,8 +1919,8 @@ fn spacing_9_3_3_5() -> Result<()> { #[test] fn spacing_9_3_3_6() -> Result<()> { - let expr = "sin2β= - 2sinβcosβ"; + let expr = "sin2β= + 2sinβcosβ"; test_braille("UEB", expr, "⠎⠔⠼⠃⠨⠃⠀⠐⠶⠀⠼⠃⠎⠊⠝⠨⠃⠉⠕⠎⠨⠃")?; return Ok(()); @@ -1662,7 +1969,7 @@ fn set_10_1() -> Result<()> { #[test] fn set_10_3() -> Result<()> { - let expr = "3AB"; + let expr = "3AB"; test_braille("UEB", expr, "⠼⠉⠀⠘⠑⠀⠠⠁⠨⠦⠠⠃")?; return Ok(()); @@ -1670,7 +1977,7 @@ fn set_10_3() -> Result<()> { #[test] fn set_10_4() -> Result<()> { - let expr = "ABAB"; + let expr = "ABAB"; test_braille("UEB", expr, "⠠⠁⠨⠦⠠⠃⠀⠘⠣⠀⠠⠁⠨⠖⠠⠃")?; return Ok(()); @@ -1679,7 +1986,7 @@ fn set_10_4() -> Result<()> { fn set_10_5() -> Result<()> { let expr = "A'B'= (AB)'"; - test_braille("UEB", expr, "⠰⠰⠰⠠⠁⠶⠨⠖⠠⠃⠶⠀⠐⠶⠀⠐⠣⠠⠁⠨⠦⠠⠃⠐⠜⠶⠰⠄")?; + test_braille("UEB", expr, "⠰⠰⠠⠁⠶⠨⠖⠠⠃⠶⠀⠐⠶⠀⠐⠣⠠⠁⠨⠦⠠⠃⠐⠜⠰⠶")?; return Ok(()); } @@ -1687,11 +1994,9 @@ fn set_10_5() -> Result<()> { #[test] fn set_10_6() -> Result<()> { // Note: example uses the wrong char "├" in the display -- should be "⊢" - let expr = "[(pq)¬p] + let expr = "[(pq)¬p] q"; - // Acceptable: GTM does uses a G1 passage indicator: "⠰⠰⠰⠨⠣⠐⠣⠏⠈⠖⠟⠐⠜⠈⠦⠈⠹⠏⠨⠜⠀⠸⠒⠀⠟⠰⠄" - // However, the BANA G1 standing alone rule ("...before a single letter standing alone") applies, so start in G2 mode. - // Corrected to remove the passage indicator + // GTM / BANA 2026: grade 1 passage test_braille("UEB", expr, "⠨⠣⠐⠣⠏⠈⠖⠟⠐⠜⠈⠦⠈⠹⠏⠨⠜⠀⠸⠒⠀⠰⠟")?; return Ok(()); @@ -1708,9 +2013,7 @@ fn example_11_5_1_2() -> Result<()> { #[test] fn example_11_5_1_3() -> Result<()> { let expr = "f'(x)"; - // Acceptable: GTM uses a G1 start indicator: "⠰⠰⠋⠶⠐⠣⠭⠐⠜" - // However, BANA says don't use a word indicator if G1 is in first 3 cells (the ':' needs it) - // Corrected to avoid word indicator + // GTM / BANA 2026: grade 1 word indicator test_braille("UEB", expr, "⠋⠰⠶⠐⠣⠭⠐⠜")?; return Ok(()); @@ -1718,7 +2021,7 @@ fn example_11_5_1_3() -> Result<()> { #[test] fn example_11_5_1_4() -> Result<()> { - let expr = "yx"; + let expr = "yx"; test_braille("UEB", expr, "⠰⠰⠷⠈⠙⠽⠨⠌⠈⠙⠭⠾")?; return Ok(()); @@ -1726,11 +2029,11 @@ fn example_11_5_1_4() -> Result<()> { #[test] fn example_11_5_2() -> Result<()> { - let expr = "23(2x+1)dx + let expr = "23(2x+1)dx =x2+x23 =(32+3)-(22+2) =12-6=6"; - test_braille("UEB", expr, "⠰⠰⠰⠮⠢⠼⠃⠔⠼⠉⠐⠣⠼⠃⠭⠐⠖⠼⠁⠐⠜⠙⠭⠀⠐⠶⠀⠨⠣⠭⠔⠼⠃⠐⠖⠭⠨⠜⠢⠼⠃⠔⠼⠉⠀⠐⠶⠀⠐⠣⠼⠉⠔⠼⠃⠐⠖⠼⠉⠐⠜⠐⠤⠐⠣⠼⠃⠔⠼⠃⠐⠖⠼⠃⠐⠜⠀⠐⠶⠀⠼⠁⠃⠐⠤⠼⠋⠀⠐⠶⠀⠼⠋⠰⠄")?; + test_braille("UEB", expr, "⠰⠰⠮⠢⠼⠃⠔⠼⠉⠐⠣⠼⠃⠭⠐⠖⠼⠁⠐⠜⠙⠭⠀⠐⠶⠀⠨⠣⠭⠰⠔⠼⠃⠐⠖⠭⠨⠜⠢⠼⠃⠔⠼⠉⠀⠐⠶⠀⠐⠣⠼⠉⠔⠼⠃⠐⠖⠼⠉⠐⠜⠐⠤⠐⠣⠼⠃⠔⠼⠃⠐⠖⠼⠃⠐⠜⠀⠐⠶⠀⠼⠁⠃⠐⠤⠼⠋⠀⠐⠶⠀⠼⠋")?; return Ok(()); } @@ -1750,19 +2053,19 @@ fn example_11_5_3() -> Result<()> { = n! - r!(nr)! + r!(nr)! "; // modified to use "shape" as recommended in a comment on this example - test_braille("UEB", expr, "⠰⠰⠰⠔⠝⠠⠉⠢⠗⠀⠐⠶⠀⠐⠣⠝⠰⠻⠗⠐⠜⠀⠐⠶⠀⠷⠝⠖⠨⠌⠗⠖⠐⠣⠝⠐⠤⠗⠐⠜⠖⠾⠰⠄")?; + test_braille("UEB", expr, "⠰⠰⠔⠝⠠⠉⠢⠗⠀⠐⠶⠀⠐⠣⠝⠰⠻⠗⠐⠜⠀⠐⠶⠀⠰⠰⠷⠝⠖⠨⠌⠗⠖⠐⠣⠝⠐⠤⠗⠐⠜⠖⠾")?; return Ok(()); } #[test] fn example_11_5_4() -> Result<()> { - let expr = "a(bc) - =(ab)(ac)"; + let expr = "a(bc) + =(ab)(ac)"; test_braille("UEB", expr, "⠁⠐⠔⠐⠣⠃⠐⠴⠉⠐⠜⠀⠐⠶⠀⠐⠣⠁⠐⠔⠃⠐⠜⠐⠴⠐⠣⠁⠐⠔⠉⠐⠜")?; return Ok(()); @@ -1773,11 +2076,11 @@ fn example_11_5_5_2() -> Result<()> { let expr = " f - 1 + 1 : Y - + X "; test_braille("UEB", expr, "⠰⠰⠰⠋⠔⠣⠐⠤⠼⠁⠜⠒⠀⠠⠽⠀⠳⠕⠀⠠⠭⠰⠄")?; @@ -1789,17 +2092,17 @@ fn example_11_5_5_2() -> Result<()> { fn example_11_5_5_3() -> Result<()> { // this comes from MathJax let expr = " - + y - + Y - + x - + X "; - test_braille("UEB", expr, "⠰⠰⠰⠘⠁⠽⠀⠘⠑⠀⠠⠽⠀⠘⠢⠭⠀⠘⠑⠀⠠⠭⠰⠄")?; + test_braille("UEB", expr, "⠘⠁⠽⠀⠘⠑⠀⠰⠠⠽⠀⠘⠢⠭⠀⠘⠑⠀⠰⠠⠭")?; return Ok(()); } @@ -1812,7 +2115,7 @@ fn example_11_5_6() -> Result<()> { x + y = 6 return Ok(()); } "; - test_braille("UEB", expr, "⠰⠰⠰⠸⠣⠐⠣⠭⠂⠀⠽⠐⠜⠀⠸⠳⠀⠭⠐⠖⠽⠀⠐⠶⠀⠼⠋⠸⠜⠰⠄")?; + test_braille("UEB", expr, "⠸⠣⠐⠣⠰⠭⠂⠀⠰⠽⠐⠜⠀⠸⠳⠀⠭⠐⠖⠽⠀⠐⠶⠀⠼⠋⠸⠜")?; return Ok(()); } @@ -1836,7 +2139,7 @@ fn example_11_6() -> Result<()> { fn bar_over_12_1_1() -> Result<()> { let expr = "x_= 10+11+123"; - test_braille("UEB", expr, "⠰⠰⠰⠭⠱⠀⠐⠶⠀⠷⠼⠁⠚⠐⠖⠼⠁⠁⠐⠖⠼⠁⠃⠨⠌⠼⠉⠾⠰⠄")?; + test_braille("UEB", expr, "⠭⠰⠱⠀⠐⠶⠀⠰⠷⠼⠁⠚⠐⠖⠼⠁⠁⠐⠖⠼⠁⠃⠨⠌⠼⠉⠾")?; return Ok(()); } @@ -1867,8 +2170,8 @@ fn dot_12_1_4() -> Result<()> { #[test] fn dot_12_1_5() -> Result<()> { - let expr = "0.561˙ - 23˙"; + let expr = "0.561˙ + 23˙"; test_braille("UEB", expr, "⠼⠚⠲⠑⠋⠣⠼⠁⠜⠘⠲⠼⠃⠣⠼⠉⠜⠘⠲")?; return Ok(()); @@ -1876,7 +2179,7 @@ fn dot_12_1_5() -> Result<()> { #[test] fn dot_12_1_6_single() -> Result<()> { - let expr = "x˙"; + let expr = "x˙"; test_braille("UEB", expr, "⠭⠘⠲")?; return Ok(()); @@ -1884,7 +2187,7 @@ fn dot_12_1_6_single() -> Result<()> { #[test] fn dot_12_1_6_double() -> Result<()> { - let expr = "x¨"; + let expr = "x¨"; test_braille("UEB", expr, "⠰⠰⠭⠨⠔⠣⠲⠲⠜")?; return Ok(()); @@ -1893,9 +2196,7 @@ fn dot_12_1_6_double() -> Result<()> { #[test] fn hat_12_1_7() -> Result<()> { let expr = "AB^C"; - // Acceptable: GTM uses a G1 start indicator: "⠰⠰⠠⠁⠠⠃⠐⠱⠠⠉" - // BANA says use a word indicator if G1 not in first 3 cells (modified it to not count cap indicators since that helps with GTM compatibility) - // Corrected to skip the G1 indicator at the start (it's debatable as to which is better) + // GTM / BANA 2026: grade 1 word indicator test_braille("UEB", expr, "⠠⠁⠠⠃⠰⠐⠱⠠⠉")?; return Ok(()); @@ -1921,7 +2222,7 @@ fn arrow_under_12() -> Result<()> { #[test] fn bar_12_2_1() -> Result<()> { - let expr = "xy¯"; + let expr = "xy¯"; test_braille("UEB", expr, "⠰⠰⠭⠔⠣⠽⠱⠜")?; return Ok(()); @@ -1929,7 +2230,7 @@ fn bar_12_2_1() -> Result<()> { #[test] fn bar_12_2_2() -> Result<()> { - let expr = "xy¯"; + let expr = "xy¯"; test_braille("UEB", expr, "⠰⠰⠣⠭⠔⠽⠜⠱")?; return Ok(()); @@ -1937,7 +2238,7 @@ fn bar_12_2_2() -> Result<()> { #[test] fn shape_14_1_1_1() -> Result<()> { - let expr = " ABC"; + let expr = " ABC"; test_braille("UEB", expr, "⠰⠫⠼⠉⠀⠠⠠⠁⠃⠉")?; return Ok(()); @@ -1945,7 +2246,7 @@ fn shape_14_1_1_1() -> Result<()> { #[test] fn shape_14_1_2_1() -> Result<()> { - let expr = "ABC"; + let expr = "ABC"; test_braille("UEB", expr, "⠰⠫⠼⠉⠱⠠⠠⠁⠃⠉")?; return Ok(()); @@ -1954,11 +2255,11 @@ fn shape_14_1_2_1() -> Result<()> { #[test] fn shape_14_1_2_2() -> Result<()> { // the for the shapes are wrong -- but it isn't clear what they should be (from WIRIS editor) - let expr = "{, -  , -  , + let expr = "{, +  , +  , return Ok(()); -   }"; +   }"; test_braille("UEB", expr, "⠸⠣⠰⠫⠼⠙⠱⠂⠀⠨⠫⠿⠱⠂⠀⠸⠫⠼⠉⠱⠂⠀⠨⠫⠼⠙⠀⠲⠲⠲⠸⠜")?; return Ok(()); } @@ -2105,11 +2406,8 @@ fn omission_15_4_1() -> Result<()> { #[test] fn chem_16_2_8() -> Result<()> { let expr = "Ca(OH)2"; - // Acceptable: GTM does not use a G1 start indicator: "⠠⠉⠁⠐⠣⠠⠕⠠⠓⠐⠜⠰⠢⠼⠃" - // However, BANA says use a word indicator if G1 not in first 3 cells (it is before the subscript near the end); use passage if >=2 whitespace - // This seems like a debateable choice in this case since there is only one G1 indicator, but that's the BANA guidance so... - // Corrected to use word indicator - test_braille("UEB", expr, "⠰⠰⠠⠉⠁⠐⠣⠠⠕⠠⠓⠐⠜⠢⠼⠃")?; + // GTM / BANA 2026: single G1 symbol for subscript (no word indicator) + test_braille("UEB", expr, "⠠⠉⠁⠐⠣⠠⠕⠠⠓⠐⠜⠰⠢⠼⠃")?; return Ok(()); } @@ -2144,7 +2442,7 @@ fn chem_16_2_9() -> Result<()> { - + @@ -2179,11 +2477,8 @@ fn chem_16_2_9() -> Result<()> { "; - // Acceptable: GTM does not use a G1 start indicator: "⠠⠉⠥⠠⠎⠠⠕⠰⠢⠼⠙⠐⠲⠼⠑⠠⠓⠢⠼⠃⠠⠕" - // However, BANA says use a word indicator if G1 not in first 3 cells (it is before the subscript); use passage if >=2 whitespace - // This seems like a debatable choice in this case since there is only one G1 indicator, but that's the BANA guidance so... - // Corrected to use word indicator - test_braille("UEB", expr, "⠰⠰⠠⠉⠥⠠⠎⠠⠕⠢⠼⠙⠐⠲⠼⠑⠠⠓⠢⠼⠃⠠⠕")?; + // GTM / BANA 2026: single G1 symbol for first subscript (no word indicator) + test_braille("UEB", expr, "⠠⠉⠥⠠⠎⠠⠕⠰⠢⠼⠙⠐⠲⠼⠑⠠⠓⠢⠼⠃⠠⠕")?; return Ok(()); } @@ -2214,14 +2509,14 @@ fn chem_16_2_12() -> Result<()> { R - + CH ( OH ) - + CH @@ -2229,7 +2524,7 @@ fn chem_16_2_12() -> Result<()> { 2 - + CH @@ -2237,7 +2532,7 @@ fn chem_16_2_12() -> Result<()> { 2 - + CO @@ -2247,9 +2542,8 @@ fn chem_16_2_12() -> Result<()> { H "; - // GTM uses G2 mode and has two G1 indicators (in middle and near end). That definitely violates BANA guidelines and maybe there guidelines. - // I have switched it to G1 word mode, which seems better (same length, but no switching) - test_braille("UEB", expr, "⠰⠰⠠⠠⠠⠗⠐⠲⠉⠓⠐⠣⠕⠓⠐⠜⠐⠲⠉⠓⠢⠼⠃⠐⠲⠉⠓⠢⠼⠃⠐⠲⠉⠕⠢⠼⠃⠰⠓⠠⠄")?; + // GTM / BANA 2026: G2 with inline G1 symbols (not forced word mode) + test_braille("UEB", expr, "⠠⠠⠠⠗⠐⠲⠡⠐⠣⠕⠓⠐⠜⠐⠲⠡⠰⠢⠼⠃⠐⠲⠉⠓⠢⠼⠃⠐⠲⠉⠕⠢⠼⠃⠰⠓⠠⠄")?; return Ok(()); } @@ -2336,7 +2630,7 @@ fn chem_16_5_1() -> Result<()> { - + Na @@ -2382,7 +2676,7 @@ fn chem_16_5_1() -> Result<()> { "#; - test_braille("UEB", expr, "⠰⠰⠰⠼⠃⠠⠝⠁⠠⠕⠠⠓⠐⠖⠠⠓⠢⠼⠃⠠⠎⠠⠕⠢⠼⠙⠀⠳⠕⠀⠠⠝⠁⠢⠼⠃⠠⠎⠠⠕⠢⠼⠙⠐⠖⠼⠃⠠⠓⠢⠼⠃⠠⠕⠰⠄")?; + test_braille("UEB", expr, "⠼⠃⠠⠝⠁⠠⠕⠠⠓⠐⠖⠠⠓⠢⠼⠃⠠⠎⠠⠕⠢⠼⠙⠀⠰⠳⠕⠀⠠⠝⠁⠰⠢⠼⠃⠠⠎⠠⠕⠢⠼⠙⠐⠖⠼⠃⠠⠓⠢⠼⠃⠠⠕")?; return Ok(()); } @@ -2393,7 +2687,7 @@ fn chem_16_5_2() -> Result<()> { let expr = r#" N2 - Haber processH2 + Haber processH2 N H3 @@ -2473,7 +2767,7 @@ fn chem_16_5_4() -> Result<()> { - + @@ -2504,7 +2798,7 @@ fn chem_16_5_4() -> Result<()> { - + @@ -2516,7 +2810,7 @@ fn chem_16_5_4() -> Result<()> { - + @@ -2526,7 +2820,7 @@ fn chem_16_5_4() -> Result<()> { - + @@ -2561,7 +2855,7 @@ fn chem_16_5_4() -> Result<()> { - + @@ -2573,7 +2867,7 @@ fn chem_16_5_4() -> Result<()> { - + @@ -2632,16 +2926,16 @@ fn chem_16_5_5() -> Result<()> { - + - + - + - + @@ -2649,7 +2943,7 @@ fn chem_16_5_5() -> Result<()> { "#; - test_braille("UEB", expr, "⠰⠰⠰⠠⠏⠃⠔⠣⠐⠖⠐⠖⠜⠐⠖⠼⠃⠰⠑⠀⠘⠸⠶⠀⠠⠏⠃⠰⠄")?; + test_braille("UEB", expr, "⠰⠰⠠⠏⠃⠔⠣⠐⠖⠐⠖⠜⠐⠖⠼⠃⠰⠑⠀⠰⠘⠸⠶⠀⠠⠏⠃")?; return Ok(()); } diff --git a/tests/braille/UEB/other.rs b/tests/braille/UEB/other.rs index f1beb55cc..210323d21 100644 --- a/tests/braille/UEB/other.rs +++ b/tests/braille/UEB/other.rs @@ -12,7 +12,7 @@ use anyhow::Result; #[test] fn overscript_grouping_aph_5_4_8() -> Result<()> { // this test was added because #220 (failed to add grouping around overscript) - let expr = " MN "; + let expr = " MN "; test_braille("UEB", expr, "⠰⠰⠣⠠⠠⠍⠝⠜⠨⠔⠳⠺⠗⠕")?; return Ok(()); @@ -39,7 +39,7 @@ fn blank_aph_7_1_ex5() -> Result<()> { #[test] fn word_symbol_aph_10_3_11() -> Result<()> { // this test was added because ⊻ (U+22bb) uses a 'G1 Word mode' char, so is different than others - let expr = " p q "; + let expr = " p q "; test_braille("UEB", expr, "⠰⠰⠏⠈⠖⠠⠱⠟")?; return Ok(()); @@ -110,7 +110,7 @@ fn double_tilde_prefix_bug_244() -> Result<()> { #[test] fn space_hack_between_digits() -> Result<()> { // https://github.com/NSoiffer/MathCAT/issues/144 - let expr = "1 ⁣3 ⁣5"; + let expr = "1 ⁣3 ⁣5"; test_braille("UEB", expr, "⠼⠁⠀⠼⠉⠀⠼⠑")?; return Ok(()); @@ -119,7 +119,7 @@ fn space_hack_between_digits() -> Result<()> { #[test] fn space_hack_around_operator() -> Result<()> { // https://github.com/NSoiffer/MathCAT/issues/144 - let expr = "y ⁣= ⁣5"; + let expr = "y ⁣= ⁣5"; test_braille("UEB", expr, "⠰⠽⠀⠐⠶⠀⠼⠑")?; return Ok(()); From 1589c695a3687aad1f96af4790099e4e35efb28c Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Tue, 25 Aug 2026 01:25:26 -0700 Subject: [PATCH 3/9] removed an option --- docs/users_old.md | 3 --- fuzz/fuzz_targets/fuzz_target_1.rs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/users_old.md b/docs/users_old.md index d666e8398..5acaecc8e 100644 --- a/docs/users_old.md +++ b/docs/users_old.md @@ -160,9 +160,6 @@ ClearSpeak has a number of options. These were designed for authors to use, but * ✓UseSpacesAroundAllOperators: [false] * Options: true/false * Description: The UEB guidelines suggest that for lower grades, adding space around operators such as `+` and `-` can be a good idea. Normally, space is only added around relational operators such as `=` and `<`. - * G1_BANA_Style: [true] - * Options: true/false - * Description: chooses how grade 1 indicators are placed. `true` follows the BANA 2019 provisional guidance (grade 1 word/passage decided for the whole expression); `false` follows ICEB GTM 1.7 (grade 1 indicators chosen per symbols-sequence, with a passage only when three or more sequences need grade 1). Braille codes often have author-definable characters. MathCAT provides some options: diff --git a/fuzz/fuzz_targets/fuzz_target_1.rs b/fuzz/fuzz_targets/fuzz_target_1.rs index bfd4ddb4e..c9a8b9abc 100644 --- a/fuzz/fuzz_targets/fuzz_target_1.rs +++ b/fuzz/fuzz_targets/fuzz_target_1.rs @@ -101,7 +101,7 @@ fn value_choices_for_key(key: &str, default: &str) -> Vec { "CopyAs" => vec!["MathML", "LaTeX", "ASCIIMath"], "BrailleNavHighlight" => vec!["Off", "FirstChar", "EndPoints", "All"], "UEB_StartMode" => vec!["Grade1", "Grade2"], - "LaTeX_UseShortName" | "Vietnam_UseDropNumbers" | "UEB_G1_BANA_Style" => vec!["true", "false"], + "LaTeX_UseShortName" | "Vietnam_UseDropNumbers" => vec!["true", "false"], "DecimalSeparator" => vec!["Auto", ".", ","], _ if key.starts_with("ClearSpeak_") => vec![ "Auto", "Verbose", "Ordinal", "Over", "None", "Speak", "TrigInverse", "AbsEnd", From b33de5d6538e2ebd7a461fe9dd203bc6a3c8683d Mon Sep 17 00:00:00 2001 From: nsoiffer Date: Tue, 25 Aug 2026 02:15:13 -0700 Subject: [PATCH 4/9] fix contractions involving 'be, con, dis' which can only be at the start of a 'word' (where 'word' has a specific UEB meaning) --- src/braille.rs | 183 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 120 insertions(+), 63 deletions(-) diff --git a/src/braille.rs b/src/braille.rs index 6cd8918bd..6c1ad6877 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -1454,6 +1454,39 @@ fn is_left_intervening_char(ch: char) -> bool { matches!(ch, 'B' | 'I' | '𝔹' | 'S' | 'T' | 'D' | 'C' | '𝐶' | 's' | 'w') } +/// RUEB 10.6.2 / 2.6.2: lower word signs may only begin a word when preceded by whitespace, +/// hyphen/dash, an opening quote/bracket/typeform, capitals, or the start of the sequence. +/// `word_start` is the index of the first char of a grade-2 letter run ('L', 'C', or 'A'). +fn allows_lower_word_sign_at(chars: &[char], word_start: usize) -> bool { + if word_start >= chars.len() { + return false; + } + if word_start == 0 { + return true; + } + let mut j = word_start - 1; + while j > 0 { + let ch = chars[j]; + if matches!(ch, 'o' | 'b') { + return false; // fraction/group open -- not a literary word start (RUEB 10.6.2) + } + if is_left_intervening_char(ch) || matches!(ch, 'e' | 'c' | 's' | 'w') { + j -= 1; + continue; + } + break; + } + if chars[j] == 'L' { + return false; + } + return lower_word_sign_may_follow(chars[j]); + + fn lower_word_sign_may_follow(ch: char) -> bool { + "W𝐖-—―".contains(ch) || + matches!(ch, 'C' | '𝐶' | 'I' | 'B' | '𝔹' | 'S' | 'T' | 'D' | 'G' | 'V') + } +} + /// Return value for use_g1_word_mode() #[derive(Debug, PartialEq)] enum Grade1WordIndicator { @@ -1722,7 +1755,7 @@ fn remove_unneeded_mode_changes(raw_braille: &str, start_mode: UEB_Mode, start_d _ => { if let Some(start) = start_g2_letter { if !cap_word_mode { - result = handle_contractions(&chars[start..i], result); + result = handle_contractions(&chars, start, i, result); } cap_word_mode = false; start_g2_letter = None; // not start of char sequence @@ -1738,7 +1771,7 @@ fn remove_unneeded_mode_changes(raw_braille: &str, start_mode: UEB_Mode, start_d } if mode != UEB_Mode::Grade2 && !cap_word_mode && let Some(start) = start_g2_letter { - result = handle_contractions(&chars[start..i], result); + result = handle_contractions(&chars, start, i, result); start_g2_letter = None; // not start of char sequence } }, @@ -1758,7 +1791,7 @@ fn remove_unneeded_mode_changes(raw_braille: &str, start_mode: UEB_Mode, start_d } if mode == UEB_Mode::Grade2 && let Some(start) = start_g2_letter { - result = handle_contractions(&chars[start..i], result); + result = handle_contractions(&chars, start, i, result); } return result; @@ -1890,11 +1923,12 @@ fn stands_alone(chars: &[char], i: usize) -> (bool, &[char], usize) { /// Return a modified result if chars can be contracted. /// Otherwise, the original string is returned -fn handle_contractions(chars: &[char], mut result: String) -> String { +fn handle_contractions(full_chars: &[char], start: usize, end: usize, mut result: String) -> String { struct Replacement { pattern: String, replacement: &'static str, skip_if_word_in: Option<&'static phf::Set<&'static str>>, + word_start_only: bool, } const ASCII_TO_UNICODE: &[char] = &[ @@ -1935,66 +1969,66 @@ fn handle_contractions(chars: &[char], mut result: String) -> String { // It would be much better from an extensibility point of view to read the table in from a file static CONTRACTIONS: LazyLock> = LazyLock::new(|| { vec![ // 10.9: initial-letter (dot-5) wordsigns -- whole word only - Replacement{ pattern: format!("^{}$", to_unicode_braille("time")), replacement: "⠐⠞", skip_if_word_in: None }, - Replacement{ pattern: format!("^{}$", to_unicode_braille("work")), replacement: "⠐⠺", skip_if_word_in: None }, - Replacement{ pattern: format!("^{}$", to_unicode_braille("leverage")), replacement: "⠇⠐⠑⠁⠛⠑", skip_if_word_in: None }, - - // 10.10: shortform prefix "dis" - Replacement{ pattern: format!("^{}", to_unicode_braille("dis")), replacement: "⠲", skip_if_word_in: None }, - // liblouis/brailletranslators "con" prefix - Replacement{ pattern: format!("^{}", to_unicode_braille("con")), replacement: "⠒", skip_if_word_in: None }, + Replacement{ pattern: format!("^{}$", to_unicode_braille("time")), replacement: "⠐⠞", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: format!("^{}$", to_unicode_braille("work")), replacement: "⠐⠺", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: format!("^{}$", to_unicode_braille("leverage")), replacement: "⠇⠐⠑⠁⠛⠑", skip_if_word_in: None, word_start_only: false }, // 10.7.1: dot-5 initial-letter contractions (as wordsigns / groupsigns) - Replacement{ pattern: format!("^{}", to_unicode_braille("through")), replacement: "⠐⠹", skip_if_word_in: None }, - Replacement{ pattern: format!("(?PL.){}(?PL.)", to_unicode_braille("part")), replacement: "${s}⠐⠏${e}", skip_if_word_in: None }, + Replacement{ pattern: format!("^{}", to_unicode_braille("through")), replacement: "⠐⠹", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: format!("(?PL.){}(?PL.)", to_unicode_braille("part")), replacement: "${s}⠐⠏${e}", skip_if_word_in: None, word_start_only: false }, // 10.3: Strong contractions - Replacement{ pattern: to_unicode_braille("and"), replacement: "L⠯", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("for"), replacement: "L⠿", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("of"), replacement: "L⠷", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("the"), replacement: "L⠮", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("with"), replacement: "L⠾", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("and"), replacement: "L⠯", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("for"), replacement: "L⠿", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("of"), replacement: "L⠷", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("the"), replacement: "L⠮", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("with"), replacement: "L⠾", skip_if_word_in: None, word_start_only: false }, // 10.8: final-letter group signs (these need to precede 'en' and any other shorter contraction) - Replacement{ pattern: "(?PL.)L⠍L⠑L⠝L⠞".to_string(), replacement: "${s}L⠰L⠞", skip_if_word_in: None }, // ment - Replacement{ pattern: "(?PL.)L⠞L⠊L⠕L⠝".to_string(), replacement: "${s}L⠰L⠝", skip_if_word_in: None }, // tion - Replacement{ pattern: "(?PL.)L⠊L⠞L⠽".to_string(), replacement: "${s}L⠰L⠽", skip_if_word_in: None }, // ity - Replacement{ pattern: "(?PL.)L⠁L⠝L⠉L⠑".to_string(), replacement: "${s}L⠨L⠑", skip_if_word_in: None }, // ance - Replacement{ pattern: "(?PL.)L⠎L⠊L⠕L⠝".to_string(), replacement: "${s}L⠨L⠝", skip_if_word_in: None }, // sion - Replacement{ pattern: "(?PL.)L⠑L⠝L⠉L⠑".to_string(), replacement: "${s}L⠰L⠑", skip_if_word_in: None }, // ence - Replacement{ pattern: "(?PL.)L⠝L⠑L⠎L⠎".to_string(), replacement: "${s}L⠰L⠎", skip_if_word_in: None }, // ness - Replacement{ pattern: "(?PL.)L⠕L⠥L⠝L⠙".to_string(), replacement: "${s}L⠨L⠙", skip_if_word_in: None }, // ound - Replacement{ pattern: "(?PL.)L⠕L⠥L⠝L⠞".to_string(), replacement: "${s}L⠨L⠞", skip_if_word_in: None }, // ount - Replacement{ pattern: "(?PL.)L⠇L⠑L⠎L⠎".to_string(), replacement: "${s}L⠨L⠎", skip_if_word_in: None }, // less - Replacement{ pattern: "(?PL.)L⠕L⠝L⠛".to_string(), replacement: "${s}L⠰L⠛", skip_if_word_in: None }, // ong - Replacement{ pattern: "(?PL.)L⠋L⠥L⠇".to_string(), replacement: "${s}L⠰L⠇", skip_if_word_in: None }, // ful + Replacement{ pattern: "(?PL.)L⠍L⠑L⠝L⠞".to_string(), replacement: "${s}L⠰L⠞", skip_if_word_in: None, word_start_only: false }, // ment + Replacement{ pattern: "(?PL.)L⠞L⠊L⠕L⠝".to_string(), replacement: "${s}L⠰L⠝", skip_if_word_in: None, word_start_only: false }, // tion + Replacement{ pattern: "(?PL.)L⠊L⠞L⠽".to_string(), replacement: "${s}L⠰L⠽", skip_if_word_in: None, word_start_only: false }, // ity + Replacement{ pattern: "(?PL.)L⠁L⠝L⠉L⠑".to_string(), replacement: "${s}L⠨L⠑", skip_if_word_in: None, word_start_only: false }, // ance + Replacement{ pattern: "(?PL.)L⠎L⠊L⠕L⠝".to_string(), replacement: "${s}L⠨L⠝", skip_if_word_in: None, word_start_only: false }, // sion + Replacement{ pattern: "(?PL.)L⠑L⠝L⠉L⠑".to_string(), replacement: "${s}L⠰L⠑", skip_if_word_in: None, word_start_only: false }, // ence + Replacement{ pattern: "(?PL.)L⠝L⠑L⠎L⠎".to_string(), replacement: "${s}L⠰L⠎", skip_if_word_in: None, word_start_only: false }, // ness + Replacement{ pattern: "(?PL.)L⠕L⠥L⠝L⠙".to_string(), replacement: "${s}L⠨L⠙", skip_if_word_in: None, word_start_only: false }, // ound + Replacement{ pattern: "(?PL.)L⠕L⠥L⠝L⠞".to_string(), replacement: "${s}L⠨L⠞", skip_if_word_in: None, word_start_only: false }, // ount + Replacement{ pattern: "(?PL.)L⠇L⠑L⠎L⠎".to_string(), replacement: "${s}L⠨L⠎", skip_if_word_in: None, word_start_only: false }, // less + Replacement{ pattern: "(?PL.)L⠕L⠝L⠛".to_string(), replacement: "${s}L⠰L⠛", skip_if_word_in: None, word_start_only: false }, // ong + Replacement{ pattern: "(?PL.)L⠋L⠥L⠇".to_string(), replacement: "${s}L⠰L⠇", skip_if_word_in: None, word_start_only: false }, // ful // 10.4: Strong group signs - Replacement{ pattern: to_unicode_braille("ch"), replacement: "L⠡", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("gh"), replacement: "L⠣", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("sh"), replacement: "L⠩", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("th"), replacement: "L⠹", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("wh"), replacement: "L⠱", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("ed"), replacement: "L⠫", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("er"), replacement: "L⠻", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("ou"), replacement: "L⠳", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("ow"), replacement: "L⠪", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("st"), replacement: "L⠌", skip_if_word_in: None }, - Replacement{ pattern: "(?PL.)L⠊L⠝L⠛".to_string(), replacement: "${s}L⠬", skip_if_word_in: None }, // 'ing', not at start - Replacement{ pattern: to_unicode_braille("ar"), replacement: "L⠜", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("ch"), replacement: "L⠡", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("gh"), replacement: "L⠣", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("sh"), replacement: "L⠩", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("th"), replacement: "L⠹", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("wh"), replacement: "L⠱", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("ed"), replacement: "L⠫", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("er"), replacement: "L⠻", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("ou"), replacement: "L⠳", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("ow"), replacement: "L⠪", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("st"), replacement: "L⠌", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: "(?PL.)L⠊L⠝L⠛".to_string(), replacement: "${s}L⠬", skip_if_word_in: None, word_start_only: false }, // 'ing', not at start + Replacement{ pattern: to_unicode_braille("ar"), replacement: "L⠜", skip_if_word_in: None, word_start_only: false }, // 10.6.5: Lower group signs preceded and followed by letters // FIX: don't match if after/before a cap letter -- can't use negative pattern (?!...) in regex package - Replacement{ pattern: "(?PL.)L⠑L⠁(?PL.)".to_string(), replacement: "${s}L⠂${e}", skip_if_word_in: Some(&EA_EXCEPTION_WORDS) }, // ea - Replacement{ pattern: "(?PL.)L⠃L⠃(?PL.)".to_string(), replacement: "${s}L⠆${e}", skip_if_word_in: None }, // bb - Replacement{ pattern: "(?PL.)L⠉L⠉(?PL.)".to_string(), replacement: "${s}L⠒${e}", skip_if_word_in: Some(&CC_EXCEPTION_WORDS) }, // cc - Replacement{ pattern: "(?PL.)L⠋L⠋(?PL.)".to_string(), replacement: "${s}L⠖${e}", skip_if_word_in: None }, // ff - Replacement{ pattern: "(?PL.)L⠛L⠛(?PL.)".to_string(), replacement: "${s}L⠶${e}", skip_if_word_in: None }, // gg - - // 10.6.8: Lower group signs ("in" also 10.5.4 lower word signs) + Replacement{ pattern: "(?PL.)L⠑L⠁(?PL.)".to_string(), replacement: "${s}L⠂${e}", skip_if_word_in: Some(&EA_EXCEPTION_WORDS), word_start_only: false }, // ea + Replacement{ pattern: "(?PL.)L⠃L⠃(?PL.)".to_string(), replacement: "${s}L⠆${e}", skip_if_word_in: None, word_start_only: false }, // bb + Replacement{ pattern: "(?PL.)L⠉L⠉(?PL.)".to_string(), replacement: "${s}L⠒${e}", skip_if_word_in: Some(&CC_EXCEPTION_WORDS), word_start_only: false }, // cc + Replacement{ pattern: "(?PL.)L⠋L⠋(?PL.)".to_string(), replacement: "${s}L⠖${e}", skip_if_word_in: None, word_start_only: false }, // ff + Replacement{ pattern: "(?PL.)L⠛L⠛(?PL.)".to_string(), replacement: "${s}L⠶${e}", skip_if_word_in: None, word_start_only: false }, // gg + + // 10.6.2: Lower word signs -- only at the beginning of a word (RUEB 10.6.1-10.6.4) + Replacement{ pattern: format!("^{}", to_unicode_braille("be")), replacement: "L⠃L⠑", skip_if_word_in: None, word_start_only: true }, + Replacement{ pattern: format!("^{}", to_unicode_braille("con")), replacement: "L⠉L⠕L⠝", skip_if_word_in: None, word_start_only: true }, + Replacement{ pattern: format!("^{}", to_unicode_braille("dis")), replacement: "L⠙L⠊L⠌", skip_if_word_in: None, word_start_only: true }, + + // 10.6.8: lower group signs; also 10.6.2 word signs when at word start ("sin", etc.) // FIX: these need restrictions about only applying when upper dots are present - Replacement{ pattern: to_unicode_braille("en"), replacement: "⠢", skip_if_word_in: None }, - Replacement{ pattern: to_unicode_braille("in"), replacement: "⠔", skip_if_word_in: None }, + Replacement{ pattern: to_unicode_braille("en"), replacement: "⠢", skip_if_word_in: None, word_start_only: false }, + Replacement{ pattern: to_unicode_braille("in"), replacement: "⠔", skip_if_word_in: None, word_start_only: false }, ] }); @@ -2002,6 +2036,8 @@ fn handle_contractions(chars: &[char], mut result: String) -> String { static CONTRACTION_PATTERNS: LazyLock = LazyLock::new(|| init_patterns(&CONTRACTIONS)); static CONTRACTION_REGEX: LazyLock> = LazyLock::new(|| init_regex(&CONTRACTIONS)); + let word_start_ok = allows_lower_word_sign_at(full_chars, start); + let chars = &full_chars[start..end]; let mut chars_as_str = chars.iter().collect::(); let original_chars_as_str = chars_as_str.clone(); // debug!(" handle_contractions: examine '{}'", &chars_as_str); @@ -2013,6 +2049,9 @@ fn handle_contractions(chars: &[char], mut result: String) -> String { continue; } } + if element.word_start_only && !word_start_ok { + continue; + } // debug!(" replacing '{}' with '{}' in '{}'", element.pattern, element.replacement, &chars_as_str); result.truncate(result.len() - chars_as_str.len()); chars_as_str = CONTRACTION_REGEX[i].replace_all(&chars_as_str, element.replacement).to_string(); @@ -3536,6 +3575,24 @@ mod tests { }); } + #[test] + fn ueb_lower_word_sign_contractions() -> Result<()> { + return braille_test(|| { + init_braille_mathml("distance")?; + set_preference("BrailleCode", "UEB")?; + let distance = get_braille("")?; + assert!(distance.starts_with("⠙⠊⠌"), "dis lower word sign at word start: got '{distance}'"); + assert!(!distance.starts_with("⠙⠊⠎"), "unexpected uncontracted dis at word start"); + init_braille_mathml("distancetime")?; + let frac = get_braille("")?; + assert!(frac.contains("⠙⠊⠎"), "dis not contracted immediately after fraction open: got '{frac}'"); + assert!(!frac.contains("⠙⠊⠌"), "dis wrongly contracted after fraction open"); + init_braille_mathml("include")?; + assert!(get_braille("")?.starts_with("⠔"), "in lower word sign at word start"); + return Ok(()); + }); + } + #[test] // This test probably should be repeated for each braille code and be taken out of here fn find_mathml_from_braille() -> Result<()> { @@ -3665,7 +3722,7 @@ mod tests { }) .collect(); let input: String = l_chars.iter().collect(); - let output = handle_contractions(&l_chars, input); + let output = handle_contractions(&l_chars, 0, l_chars.len(), input); output.chars().filter(|&c| c != 'L').collect() } @@ -3681,7 +3738,7 @@ mod tests { ("work", "⠐⠺"), ("time", "⠐⠞"), ("speed", "⠎⠏⠑⠫"), - ("distance", "⠲⠞⠨⠑"), + ("distance", "⠙⠊⠌⠨⠑"), ("area", "⠜⠑⠁"), ("height", "⠓⠑⠊⠣⠞"), ("weight", "⠺⠑⠊⠣⠞"), @@ -3698,7 +3755,7 @@ mod tests { ("interest", "⠔⠞⠻⠑⠌"), ("revenue", "⠗⠑⠧⠢⠥⠑"), ("clearance", "⠉⠇⠑⠜⠨⠑"), - ("conductance", "⠒⠙⠥⠉⠞⠨⠑"), + ("conductance", "⠉⠕⠝⠙⠥⠉⠞⠨⠑"), ("capacitance", "⠉⠁⠏⠁⠉⠊⠞⠨⠑"), ("resistance", "⠗⠑⠎⠊⠌⠨⠑"), ("impedance", "⠊⠍⠏⠫⠨⠑"), @@ -3716,9 +3773,9 @@ mod tests { ("valence", "⠧⠁⠇⠰⠑"), ("stiffness", "⠌⠊⠖⠰⠎"), ("thickness", "⠹⠊⠉⠅⠰⠎"), - ("displacement", "⠲⠏⠇⠁⠉⠑⠰⠞"), - ("concentration", "⠒⠉⠢⠞⠗⠁⠰⠝"), - ("consumption", "⠒⠎⠥⠍⠏⠰⠝"), + ("displacement", "⠙⠊⠌⠏⠇⠁⠉⠑⠰⠞"), + ("concentration", "⠉⠕⠝⠉⠢⠞⠗⠁⠰⠝"), + ("consumption", "⠉⠕⠝⠎⠥⠍⠏⠰⠝"), ("duration", "⠙⠥⠗⠁⠰⠝"), ("elevation", "⠑⠇⠑⠧⠁⠰⠝"), ("fraction", "⠋⠗⠁⠉⠰⠝"), @@ -3754,7 +3811,7 @@ mod tests { ("breadth", "⠃⠗⠂⠙⠹"), ("strength", "⠌⠗⠢⠛⠹"), ("spread", "⠎⠏⠗⠂⠙"), - ("discount", "⠲⠉⠨⠞"), + ("discount", "⠙⠊⠌⠉⠨⠞"), ("count", "⠉⠨⠞"), ("version", "⠧⠻⠨⠝"), ("inductance", "⠔⠙⠥⠉⠞⠨⠑"), @@ -3807,7 +3864,7 @@ mod tests { ("cohesion", "⠉⠕⠓⠑⠨⠝"), ("compliance", "⠉⠕⠍⠏⠇⠊⠨⠑"), ("compression", "⠉⠕⠍⠏⠗⠑⠎⠨⠝"), - ("conductivity", "⠒⠙⠥⠉⠞⠊⠧⠰⠽"), + ("conductivity", "⠉⠕⠝⠙⠥⠉⠞⠊⠧⠰⠽"), ("coverage", "⠉⠕⠧⠻⠁⠛⠑"), ("damping", "⠙⠁⠍⠏⠬"), ("deflection", "⠙⠑⠋⠇⠑⠉⠰⠝"), @@ -3815,9 +3872,9 @@ mod tests { ("depreciation", "⠙⠑⠏⠗⠑⠉⠊⠁⠰⠝"), ("deviation", "⠙⠑⠧⠊⠁⠰⠝"), ("diffusion", "⠙⠊⠖⠥⠨⠝"), - ("dispersion", "⠲⠏⠻⠨⠝"), - ("dissipation", "⠲⠎⠊⠏⠁⠰⠝"), - ("distortion", "⠲⠞⠕⠗⠰⠝"), + ("dispersion", "⠙⠊⠌⠏⠻⠨⠝"), + ("dissipation", "⠙⠊⠌⠎⠊⠏⠁⠰⠝"), + ("distortion", "⠙⠊⠌⠕⠗⠰⠝"), ("divergence", "⠙⠊⠧⠻⠛⠰⠑"), ("downforce", "⠙⠪⠝⠿⠉⠑"), ("elasticity", "⠑⠇⠁⠌⠊⠉⠰⠽"), From 4f09ef4cb4f71475ce1fef79feb22a2eda2829c4 Mon Sep 17 00:00:00 2001 From: "U-rainshowers\\neils" Date: Wed, 26 Aug 2026 12:05:10 -0700 Subject: [PATCH 5/9] Updated to UEB to the latest guidance for grade 1 indicators. This adds new tests and changes a few tests. Also added more contractions. --- AGENTS.md | 1 + Rules/Braille/UEB/UEB_Rules.yaml | 5 +- src/braille.rs | 341 +++++++++++++++++++++++++------ tests/braille/UEB/iceb.rs | 32 ++- tests/braille/UEB/other.rs | 17 ++ 5 files changed, 317 insertions(+), 79 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 76ea6a889..7093cf698 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,7 @@ but add common mistakes of AI agents here instead. - Do not mirror README content here; keep guidance agent-specific. - Avoid broad formatting sweeps; do not run `cargo fmt` in this repo. - Keep code/rule changes focused and validate with targeted tests first: `cargo test ` +- Do **not** change test goldens (expected braille/speech/nav strings) unless the user explicitly says to. Prefer fixing code/rules to match existing goldens; if a golden looks wrong vs the spec, ask before editing it. - do not do any git commands unless explicitly asked for - Rust coverage is in `target/coverage/`. - When working with GitHub, e.g. looking at PRs and issues, check if the GitHub CLI is installed (`gh --version`). diff --git a/Rules/Braille/UEB/UEB_Rules.yaml b/Rules/Braille/UEB/UEB_Rules.yaml index 5e1df38a7..57ca8e515 100644 --- a/Rules/Braille/UEB/UEB_Rules.yaml +++ b/Rules/Braille/UEB/UEB_Rules.yaml @@ -107,7 +107,7 @@ replace: - t: "1⠷" - x: "*[1]" - - t: "1⠨⠌" + - t: "⠨⠌" - x: "*[2]" - t: "1⠾" @@ -256,9 +256,10 @@ tag: mo # add spaces around comparison operators unless they are in a script position, variables: # ratio not spaced in UEB; also make sure it is an infix operator (e.g., "~" can be prefix) + # ∴ / ∵ / ∎ are spaced like comparison signs (GTM 1.7.9 "∴ force = …"; GTM 11) - AddSpaces: "$UseSpacesAroundAllOperators or ( parent::*[self::m:mrow] and $NewScriptContext='' and - .!='∶' and IsInDefinition(., 'Braille', 'NemethComparisonOperators') )" + .!='∶' and (IsInDefinition(., 'Braille', 'NemethComparisonOperators') or .='∴' or .='∵' or .='∎') )" match: "." replace: - test: diff --git a/src/braille.rs b/src/braille.rs index 6c1ad6877..f5b684fe1 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -1039,11 +1039,9 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str let use_only_grade1 = pref_manager.pref_to_string("UEB_START_MODE").as_str() == "Grade1"; - // '𝐖' is a hard break -- basically, it separates exprs - let mut result = result.split('𝐖') - .map(|str| pick_start_mode(str, use_only_grade1) + "W") - .collect::(); - result.pop(); // we added a 'W' at the end that needs to be removed. + // 𝐖 is a unit hard-break. For GTM 1.7 it is still a symbols-sequence boundary (same as W), + // so G1 passage decisions (1.7.3(b) / 1.7.5(c)) see the whole expression. + let result = pick_start_mode(&result, use_only_grade1); let result = result.replace("tW", "W"); @@ -1072,13 +1070,15 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str // Count G1 on the original (with capital markers) so standing-alone letters still force // passage when needed (BANA Ex 5-15), but apply mode changes after stripping C/𝐶 so // Grade 2 contractions are not blocked by cap_word_mode (chem "ch" → ⠡). - let use_cap_passage = is_cap_passage_mode_good(raw_braille); + let had_unit_break = raw_braille.contains('𝐖'); + let raw_braille = raw_braille.replace('𝐖', "W"); + let use_cap_passage = is_cap_passage_mode_good(&raw_braille); let stripped_caps = raw_braille.replace(['C', '𝐶'], ""); - let apply_to = if use_cap_passage { stripped_caps.as_str() } else { raw_braille }; + let apply_to = if use_cap_passage { stripped_caps.as_str() } else { raw_braille.as_str() }; let result = if use_only_grade1 { remove_unneeded_mode_changes(apply_to, UEB_Mode::Grade1, UEB_Duration::Passage) } else { - gtm_1_7_mode(raw_braille, apply_to) + gtm_1_7_mode(&raw_braille, apply_to, had_unit_break) }; if use_cap_passage { return convert_to_cap_passage_mode(&result); @@ -1180,34 +1180,122 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str .count(); } + /// True if this symbols-sequence contains a literary English word (4+ letters). + /// GTM 1.7.5: short math names (sin, cos, lim, min, …) and single-letter variables + /// are not treated as words. + fn sequence_contains_word(raw_word: &str) -> bool { + let chars: Vec = raw_word.chars().collect(); + let mut run = 0usize; + let mut i = 0usize; + while i < chars.len() { + let ch = chars[i]; + if ch == 'L' { + run += 1; + i += 1; + if i < chars.len() { + i += 1; // skip the braille cell after 'L' + } + if run >= 4 { + return true; + } + } else if ch == 'A' { + run += 1; + i += 1; + if run >= 4 { + return true; + } + } else if is_math_alphabet_typeform(ch) { + // Double-struck/script/fraktur/sans-serif letters are not part of an English word. + let end = math_typeform_item_end(&chars, i); + i = if end > i { end } else { i + 1 }; + run = 0; + } else if matches!(ch, 'C' | '𝐶' | 'G' | 'V' | 'B' | 'I' | 's' | 'w' | 'e') { + i += 1; // letter prefixes / literary typeform — do not break the run + } else { + run = 0; + i += 1; + } + } + false + } + + /// G2 form of a general-fraction sequence when two symbol indicators keep words + /// contracted (GTM 1.7.5(a)). `None` if more than two G1 symbols would remain. + fn g2_two_symbol_form(raw_word: &str) -> Option { + // Only the GTM 1.7.5(a) case: a general fraction whose numerator/denominator + // contain literary words. Superscripts, radicals, etc. keep 1.7.3 word indicators. + if !(raw_word.contains('⠷') && raw_word.contains("⠨⠌")) { + return None; + } + let g2 = remove_unneeded_mode_changes(raw_word, UEB_Mode::Grade2, UEB_Duration::Symbol); + let chars: Vec = g2.chars().collect(); + let n_g1 = chars.iter().enumerate() + .filter(|&(i, &ch)| ch == '1' && !is_forced_grade1(&chars, i)) + .count(); + if n_g1 <= 2 { + Some(g2) + } else { + None + } + } + /// ICEB GTM 1.7 grade 1 indicator placement (per symbols-sequence). - /// See GTM 1.7.3. Note: the 1.7.5 two-symbol allowance and contraction-preserving - /// word/passage placement for expressions containing English words are only partially - /// realized here; those cases depend on separate contraction-rule improvements. + /// GTM 1.7.3: one grade 1 symbol per sequence, or a word indicator if more than one. + /// GTM 1.7.5(a): for a sequence containing word(s), allow two grade 1 symbol + /// indicators when that keeps the words in their usual contracted form + /// (e.g. ⠷…⠾ around work/distance). More than two still uses a word indicator. /// `count_src` decides passage vs word vs symbol; changes are applied to `apply_to` /// (may differ when capital markers were stripped for capital-passage mode). - fn gtm_1_7_mode(count_src: &str, apply_to: &str) -> String { + fn gtm_1_7_mode(count_src: &str, apply_to: &str, had_unit_break: bool) -> String { + let count_words: Vec<&str> = count_src.split('W').collect(); + let apply_words: Vec<&str> = apply_to.split('W').collect(); + assert_eq!(count_words.len(), apply_words.len(), + "gtm_1_7_mode: count_src and apply_to must have the same number of symbols-sequences"); + // Count how many symbols-sequences (whitespace-separated) need a non-forced grade 1 indicator. - let n_seq_needing = count_src.split('W') - .filter(|raw_word| grade1_count(raw_word) >= 1) - .count(); + let n_seq_needing = count_words.iter().filter(|raw_word| grade1_count(raw_word) >= 1).count(); - // GTM 1.7.3(b): use a grade 1 passage if three or more sequences each need grade 1. + // GTM 1.7.3(b) / 1.7.5(c): use a grade 1 passage if three or more sequences + // each need grade 1. 1.7.5(c) starts the passage at the first such sequence so + // preceding words (e.g. "speed") stay contracted. That delayed start is used when + // spaced units (𝐖) joined the expression; otherwise a passage wraps the whole + // expression (chem, roots, labelled equations). if n_seq_needing >= 3 { + let first = count_words.iter().position(|w| grade1_count(w) >= 1) + .expect("n_seq_needing >= 3 implies a sequence that needs grade 1"); + let delay_start = had_unit_break && first > 0; + if delay_start { + let prefix: Vec = apply_words[..first].iter() + .map(|w| remove_unneeded_mode_changes(w, UEB_Mode::Grade2, UEB_Duration::Symbol)) + .collect(); + let rest = apply_words[first..].join("W"); + let passage = remove_unneeded_mode_changes(&rest, UEB_Mode::Grade1, UEB_Duration::Passage); + let mut out = prefix.join("W"); + if !out.is_empty() { + out.push('W'); + } + out.push_str("⠰⠰⠰"); + out.push_str(&passage); + out.push_str("⠰⠄"); + return out; + } let grade1_passage = remove_unneeded_mode_changes(apply_to, UEB_Mode::Grade1, UEB_Duration::Passage); return "⠰⠰⠰".to_string() + &grade1_passage + "⠰⠄"; } // GTM 1.7.3(a): per sequence, allow one grade 1 symbol indicator, or a grade 1 word // indicator if the sequence needs more than one. - let count_words: Vec<&str> = count_src.split('W').collect(); - let apply_words: Vec<&str> = apply_to.split('W').collect(); - assert_eq!(count_words.len(), apply_words.len(), - "gtm_1_7_mode: count_src and apply_to must have the same number of symbols-sequences"); let words = count_words.iter().zip(apply_words.iter()) .map(|(&count_word, &apply_word)| { - if grade1_count(count_word) >= 2 { - "⠰⠰".to_string() + &remove_unneeded_mode_changes(apply_word, UEB_Mode::Grade1, UEB_Duration::Word) + let n_g1 = grade1_count(count_word); + if n_g1 >= 2 { + // GTM 1.7.5(a): two symbol indicators if that preserves contractions + if sequence_contains_word(count_word) + && let Some(g2) = g2_two_symbol_form(apply_word) { + g2 + } else { + "⠰⠰".to_string() + &remove_unneeded_mode_changes(apply_word, UEB_Mode::Grade1, UEB_Duration::Word) + } } else { // 0 or 1 grade 1 indicators: the grade 2 form leaves a single symbol indicator inline remove_unneeded_mode_changes(apply_word, UEB_Mode::Grade2, UEB_Duration::Symbol) @@ -1454,8 +1542,71 @@ fn is_left_intervening_char(ch: char) -> bool { matches!(ch, 'B' | 'I' | '𝔹' | 'S' | 'T' | 'D' | 'C' | '𝐶' | 's' | 'w') } -/// RUEB 10.6.2 / 2.6.2: lower word signs may only begin a word when preceded by whitespace, -/// hyphen/dash, an opening quote/bracket/typeform, capitals, or the start of the sequence. +/// Double-struck / script / fraktur / sans-serif: math alphabets, not literary emphasis. +fn is_math_alphabet_typeform(ch: char) -> bool { + matches!(ch, '𝔹' | 'T' | 'D' | 'S') +} + +/// Advance past one letter with optional capital, Greek, and accent prefixes (`C? G|V? A…? L cell`). +fn skip_one_prefixed_letter(chars: &[char], mut i: usize) -> usize { + if i >= chars.len() { + return i; + } + if matches!(chars[i], 'C' | '𝐶') { + i += 1; + } + if i < chars.len() && matches!(chars[i], 'G' | 'V') { + i += 1; + } + if i < chars.len() && chars[i] == 'A' { + match index_after_accent_to_l(chars, i + 1) { + Some(i_l) => i = i_l, + None => return i, + } + } + if i < chars.len() && chars[i] == 'L' { + i += 1; + if i < chars.len() { + i += 1; // braille cell + } + } + i +} + +/// End index of a math-alphabet typeform item starting at `i` (`𝔹`/`T`/`D`/`S`). +/// `s` covers one letter; `w` covers the typeform word until its terminator. +fn math_typeform_item_end(chars: &[char], i: usize) -> usize { + if i >= chars.len() || !is_math_alphabet_typeform(chars[i]) { + return i; + } + let typeform = chars[i]; + let mut j = i + 1; + let word_mode = j < chars.len() && chars[j] == 'w'; + if j < chars.len() && matches!(chars[j], 's' | 'w') { + j += 1; + } + if word_mode { + loop { + let next = skip_one_prefixed_letter(chars, j); + if next == j { + break; + } + j = next; + } + if j + 1 < chars.len() && chars[j] == typeform && chars[j + 1] == 'e' { + j += 2; + } else if j < chars.len() && chars[j] == 'e' { + j += 1; + } + return j; + } + return skip_one_prefixed_letter(chars, j); +} + +/// RUEB 10.6.2: lower groupsigns "be", "con", "dis" only at the beginning of a word. +/// Beginning of a word = letters-sequence after space, hyphen or dash, optionally with +/// intervening punctuation/indicators from RUEB 2.6.2 (opening brackets/quotes, typeform, +/// capitals). Math operators and fraction indicators are not word boundaries. /// `word_start` is the index of the first char of a grade-2 letter run ('L', 'C', or 'A'). fn allows_lower_word_sign_at(chars: &[char], word_start: usize) -> bool { if word_start >= chars.len() { @@ -1464,27 +1615,18 @@ fn allows_lower_word_sign_at(chars: &[char], word_start: usize) -> bool { if word_start == 0 { return true; } - let mut j = word_start - 1; + let mut j = word_start; while j > 0 { + j -= 1; let ch = chars[j]; - if matches!(ch, 'o' | 'b') { - return false; // fraction/group open -- not a literary word start (RUEB 10.6.2) - } - if is_left_intervening_char(ch) || matches!(ch, 'e' | 'c' | 's' | 'w') { - j -= 1; + // 2.6.2 intervening symbols (opening punctuation flags, typeform, capitals, etc.) + if is_left_intervening_char(ch) || matches!(ch, 'e' | 'c' | 's' | 'w' | 'o' | 'b' | 'G' | 'V') { continue; } - break; - } - if chars[j] == 'L' { - return false; - } - return lower_word_sign_may_follow(chars[j]); - - fn lower_word_sign_may_follow(ch: char) -> bool { - "W𝐖-—―".contains(ch) || - matches!(ch, 'C' | '𝐶' | 'I' | 'B' | '𝔹' | 'S' | 'T' | 'D' | 'G' | 'V') + // Must be space, hyphen or dash (RUEB 10.6.2 Note) + return "W𝐖-—―".contains(ch); } + true } /// Return value for use_g1_word_mode() @@ -1654,6 +1796,34 @@ fn remove_unneeded_mode_changes(raw_braille: &str, start_mode: UEB_Mode, start_d } // debug!("Grade 2: ch={}, duration: {:?}", ch, duration); match ch { + '𝔹' | 'T' | 'D' | 'S' => { + // Math-alphabet typeform: this letter is not part of an English word + // (GTM 11.6). Close any current contraction run, emit the typeform + // item on its own, then let following letters start a new word. + if let Some(start) = start_g2_letter { + if !cap_word_mode { + result = handle_contractions(&chars, start, i, result); + } + cap_word_mode = false; + start_g2_letter = None; + } + let unit_end = math_typeform_item_end(&chars, i); + // Typeform prefix (`𝔹s`) must precede the grade 1 indicator: ⠈⠆⠰⠠⠗, not ⠰⠈⠆⠠⠗. + let mut letter_start = i + 1; + if letter_start < unit_end && matches!(chars[letter_start], 's' | 'w') { + letter_start += 1; + } + result.extend(chars[i..letter_start].iter().copied()); + if let Some(letter_idx) = chars[i..unit_end].iter().position(|&c| c == 'L') { + let (is_alone, _, n_letters) = stands_alone(&chars, i + letter_idx); + if is_alone && n_letters == 1 { + result.push('1'); + mode = UEB_Mode::Grade1; + } + } + result.extend(chars[letter_start..unit_end].iter().copied()); + i = unit_end; + }, 'L' => { if start_g2_letter.is_none() { start_g2_letter = Some(i); @@ -1735,6 +1905,16 @@ fn remove_unneeded_mode_changes(raw_braille: &str, start_mode: UEB_Mode, start_d } }, '1' => { + // Contract the preceding letter run before consuming '1'. The '1' arm used + // to increment first, so handle_contractions saw "work1" and the whole-word + // patterns `^work$` / `^time$` failed (GTM 1.7.9 work/distance, 1.7.5(a) time). + if let Some(start) = start_g2_letter { + if !cap_word_mode { + result = handle_contractions(&chars, start, i, result); + } + cap_word_mode = false; + start_g2_letter = None; + } result.push(ch); i += 1; mode = UEB_Mode::Grade1; @@ -1966,6 +2146,12 @@ fn handle_contractions(full_chars: &[char], start: usize, end: usize, mut result "L⠗L⠑L⠁L⠉L⠞L⠊L⠧L⠊L⠞L⠽", // reactivity }; + // Words where "be" is not the first syllable (RUEB 10.6.1 "But:" cases and similar). + static BE_EXCEPTION_WORDS: phf::Set<&str> = phf_set! { + "L⠃L⠑L⠁L⠗L⠊L⠝L⠛", // bearing + "L⠃L⠑L⠝L⠉L⠓L⠍L⠁L⠗L⠅", // benchmark + }; + // It would be much better from an extensibility point of view to read the table in from a file static CONTRACTIONS: LazyLock> = LazyLock::new(|| { vec![ // 10.9: initial-letter (dot-5) wordsigns -- whole word only @@ -1983,6 +2169,12 @@ fn handle_contractions(full_chars: &[char], start: usize, end: usize, mut result Replacement{ pattern: to_unicode_braille("of"), replacement: "L⠷", skip_if_word_in: None, word_start_only: false }, Replacement{ pattern: to_unicode_braille("the"), replacement: "L⠮", skip_if_word_in: None, word_start_only: false }, Replacement{ pattern: to_unicode_braille("with"), replacement: "L⠾", skip_if_word_in: None, word_start_only: false }, + + // 10.6.1-10.6.4 / 10.10.4: lower groupsigns be/con/dis -- only at word start, + // and preferred over other groupsigns (e.g. "distance" is ⠲⠞⠨⠑, not di+st+ance). + Replacement{ pattern: format!("^{}", to_unicode_braille("be")), replacement: "⠆", skip_if_word_in: Some(&BE_EXCEPTION_WORDS), word_start_only: true }, + Replacement{ pattern: format!("^{}", to_unicode_braille("con")), replacement: "⠒", skip_if_word_in: None, word_start_only: true }, + Replacement{ pattern: format!("^{}", to_unicode_braille("dis")), replacement: "⠲", skip_if_word_in: None, word_start_only: true }, // 10.8: final-letter group signs (these need to precede 'en' and any other shorter contraction) Replacement{ pattern: "(?PL.)L⠍L⠑L⠝L⠞".to_string(), replacement: "${s}L⠰L⠞", skip_if_word_in: None, word_start_only: false }, // ment @@ -2020,11 +2212,6 @@ fn handle_contractions(full_chars: &[char], start: usize, end: usize, mut result Replacement{ pattern: "(?PL.)L⠋L⠋(?PL.)".to_string(), replacement: "${s}L⠖${e}", skip_if_word_in: None, word_start_only: false }, // ff Replacement{ pattern: "(?PL.)L⠛L⠛(?PL.)".to_string(), replacement: "${s}L⠶${e}", skip_if_word_in: None, word_start_only: false }, // gg - // 10.6.2: Lower word signs -- only at the beginning of a word (RUEB 10.6.1-10.6.4) - Replacement{ pattern: format!("^{}", to_unicode_braille("be")), replacement: "L⠃L⠑", skip_if_word_in: None, word_start_only: true }, - Replacement{ pattern: format!("^{}", to_unicode_braille("con")), replacement: "L⠉L⠕L⠝", skip_if_word_in: None, word_start_only: true }, - Replacement{ pattern: format!("^{}", to_unicode_braille("dis")), replacement: "L⠙L⠊L⠌", skip_if_word_in: None, word_start_only: true }, - // 10.6.8: lower group signs; also 10.6.2 word signs when at word start ("sin", etc.) // FIX: these need restrictions about only applying when upper dots are present Replacement{ pattern: to_unicode_braille("en"), replacement: "⠢", skip_if_word_in: None, word_start_only: false }, @@ -2038,6 +2225,13 @@ fn handle_contractions(full_chars: &[char], start: usize, end: usize, mut result let word_start_ok = allows_lower_word_sign_at(full_chars, start); let chars = &full_chars[start..end]; + // A non-English letter glued to an English word (GTM 1.7.9 Δtime, or a + // double-struck/script/fraktur letter) is not part of the English + // letters-sequence; contract the following word on its own (time → ⠐⠞). + let skip = leading_non_word_letter_len(chars); + if skip > 0 && skip < chars.len() { + return handle_contractions(full_chars, start + skip, end, result); + } let mut chars_as_str = chars.iter().collect::(); let original_chars_as_str = chars_as_str.clone(); // debug!(" handle_contractions: examine '{}'", &chars_as_str); @@ -2062,6 +2256,31 @@ fn handle_contractions(full_chars: &[char], start: usize, end: usize, mut result + fn leading_non_word_letter_len(chars: &[char]) -> usize { + if chars.is_empty() { + return 0; + } + if is_math_alphabet_typeform(chars[0]) { + let end = math_typeform_item_end(chars, 0); + return if end < chars.len() { end } else { 0 }; + } + let mut i = 0; + if i < chars.len() && matches!(chars[i], 'C' | '𝐶') { + i += 1; + } + if i < chars.len() && matches!(chars[i], 'G' | 'V') { + i += 1; + if i < chars.len() && chars[i] == 'L' { + i += 1; + if i < chars.len() { + i += 1; // braille cell + } + return i; + } + } + 0 + } + fn init_patterns(contractions: &[Replacement]) -> RegexSet { let mut vec: Vec<&str> = Vec::with_capacity(contractions.len()); for contraction in contractions { @@ -3581,14 +3800,16 @@ mod tests { init_braille_mathml("distance")?; set_preference("BrailleCode", "UEB")?; let distance = get_braille("")?; - assert!(distance.starts_with("⠙⠊⠌"), "dis lower word sign at word start: got '{distance}'"); - assert!(!distance.starts_with("⠙⠊⠎"), "unexpected uncontracted dis at word start"); + // RUEB 10.6.1 / 10.10.4: distance at word start uses dis lower groupsign + assert_eq!(distance, "⠲⠞⠨⠑", "dis lower groupsign at word start: got '{distance}'"); + // After fraction open (not a RUEB 10.6.2 word start), dis must not apply; st+ance may. init_braille_mathml("distancetime")?; let frac = get_braille("")?; - assert!(frac.contains("⠙⠊⠎"), "dis not contracted immediately after fraction open: got '{frac}'"); - assert!(!frac.contains("⠙⠊⠌"), "dis wrongly contracted after fraction open"); + assert!(!frac.contains("⠲"), "dis wrongly used after fraction open: got '{frac}'"); + assert!(frac.contains("⠙⠊⠌⠨⠑") || frac.contains("⠙⠊⠎⠞⠁⠝⠉⠑"), + "expected st+ance or uncontracted distance after fraction open: got '{frac}'"); init_braille_mathml("include")?; - assert!(get_braille("")?.starts_with("⠔"), "in lower word sign at word start"); + assert!(get_braille("")?.starts_with("⠔"), "in lower groupsign at word start"); return Ok(()); }); } @@ -3738,7 +3959,7 @@ mod tests { ("work", "⠐⠺"), ("time", "⠐⠞"), ("speed", "⠎⠏⠑⠫"), - ("distance", "⠙⠊⠌⠨⠑"), + ("distance", "⠲⠞⠨⠑"), ("area", "⠜⠑⠁"), ("height", "⠓⠑⠊⠣⠞"), ("weight", "⠺⠑⠊⠣⠞"), @@ -3755,7 +3976,7 @@ mod tests { ("interest", "⠔⠞⠻⠑⠌"), ("revenue", "⠗⠑⠧⠢⠥⠑"), ("clearance", "⠉⠇⠑⠜⠨⠑"), - ("conductance", "⠉⠕⠝⠙⠥⠉⠞⠨⠑"), + ("conductance", "⠒⠙⠥⠉⠞⠨⠑"), ("capacitance", "⠉⠁⠏⠁⠉⠊⠞⠨⠑"), ("resistance", "⠗⠑⠎⠊⠌⠨⠑"), ("impedance", "⠊⠍⠏⠫⠨⠑"), @@ -3773,9 +3994,9 @@ mod tests { ("valence", "⠧⠁⠇⠰⠑"), ("stiffness", "⠌⠊⠖⠰⠎"), ("thickness", "⠹⠊⠉⠅⠰⠎"), - ("displacement", "⠙⠊⠌⠏⠇⠁⠉⠑⠰⠞"), - ("concentration", "⠉⠕⠝⠉⠢⠞⠗⠁⠰⠝"), - ("consumption", "⠉⠕⠝⠎⠥⠍⠏⠰⠝"), + ("displacement", "⠲⠏⠇⠁⠉⠑⠰⠞"), + ("concentration", "⠒⠉⠢⠞⠗⠁⠰⠝"), + ("consumption", "⠒⠎⠥⠍⠏⠰⠝"), ("duration", "⠙⠥⠗⠁⠰⠝"), ("elevation", "⠑⠇⠑⠧⠁⠰⠝"), ("fraction", "⠋⠗⠁⠉⠰⠝"), @@ -3811,7 +4032,7 @@ mod tests { ("breadth", "⠃⠗⠂⠙⠹"), ("strength", "⠌⠗⠢⠛⠹"), ("spread", "⠎⠏⠗⠂⠙"), - ("discount", "⠙⠊⠌⠉⠨⠞"), + ("discount", "⠲⠉⠨⠞"), ("count", "⠉⠨⠞"), ("version", "⠧⠻⠨⠝"), ("inductance", "⠔⠙⠥⠉⠞⠨⠑"), @@ -3864,7 +4085,7 @@ mod tests { ("cohesion", "⠉⠕⠓⠑⠨⠝"), ("compliance", "⠉⠕⠍⠏⠇⠊⠨⠑"), ("compression", "⠉⠕⠍⠏⠗⠑⠎⠨⠝"), - ("conductivity", "⠉⠕⠝⠙⠥⠉⠞⠊⠧⠰⠽"), + ("conductivity", "⠒⠙⠥⠉⠞⠊⠧⠰⠽"), ("coverage", "⠉⠕⠧⠻⠁⠛⠑"), ("damping", "⠙⠁⠍⠏⠬"), ("deflection", "⠙⠑⠋⠇⠑⠉⠰⠝"), @@ -3872,9 +4093,9 @@ mod tests { ("depreciation", "⠙⠑⠏⠗⠑⠉⠊⠁⠰⠝"), ("deviation", "⠙⠑⠧⠊⠁⠰⠝"), ("diffusion", "⠙⠊⠖⠥⠨⠝"), - ("dispersion", "⠙⠊⠌⠏⠻⠨⠝"), - ("dissipation", "⠙⠊⠌⠎⠊⠏⠁⠰⠝"), - ("distortion", "⠙⠊⠌⠕⠗⠰⠝"), + ("dispersion", "⠲⠏⠻⠨⠝"), + ("dissipation", "⠲⠎⠊⠏⠁⠰⠝"), + ("distortion", "⠲⠞⠕⠗⠰⠝"), ("divergence", "⠙⠊⠧⠻⠛⠰⠑"), ("downforce", "⠙⠪⠝⠿⠉⠑"), ("elasticity", "⠑⠇⠁⠌⠊⠉⠰⠽"), diff --git a/tests/braille/UEB/iceb.rs b/tests/braille/UEB/iceb.rs index 86a1dbb34..882d14306 100644 --- a/tests/braille/UEB/iceb.rs +++ b/tests/braille/UEB/iceb.rs @@ -486,7 +486,9 @@ fn grade1_1_7_4_3() -> Result<()> { fn grade1_1_7_5_a() -> Result<()> { let expr = "speed= distancetime"; - test_braille("UEB", expr, "⠎⠏⠑⠫⠀⠐⠶⠀⠰⠰⠷⠙⠊⠎⠞⠁⠝⠉⠑⠨⠌⠞⠊⠍⠑⠾")?; + // GTM 1.7.5(a) (2025): two grade 1 symbol indicators so the words stay contracted. + // After the fraction open, "dis" is not a word start (RUEB 10.6.2), so st+ance: ⠙⠊⠌⠨⠑. + test_braille("UEB", expr, "⠎⠏⠑⠫⠀⠐⠶⠀⠰⠷⠙⠊⠌⠨⠑⠨⠌⠐⠞⠰⠾")?; return Ok(()); } @@ -570,7 +572,8 @@ fn grade1_1_7_9_4_unit() -> Result<()> { #[test] fn grade1_1_7_9_5() -> Result<()> { let expr = "y=x2"; - test_braille("UEB", expr, "⠰⠽⠀⠐⠶⠀⠰⠰⠷⠭⠨⠌⠼⠃⠾")?; + // GTM 1.7.9 (July 2025): one symbol indicator; numeric mode covers the closer. + test_braille("UEB", expr, "⠰⠽⠀⠐⠶⠀⠰⠷⠭⠨⠌⠼⠃⠾")?; return Ok(()); } @@ -763,7 +766,7 @@ fn grade1_1_7_9_26() -> Result<()> { #[test] fn grade1_1_7_9_27() -> Result<()> { let expr = "work=force×distance"; - test_braille("UEB", expr, "⠐⠺⠀⠐⠶⠀⠿⠉⠑⠐⠦⠲⠞⠨⠑")?; + test_braille("UEB", expr, "⠐⠺⠀⠐⠶⠀⠿⠉⠑⠐⠦⠙⠊⠌⠨⠑")?; return Ok(()); } @@ -1492,14 +1495,6 @@ fn fraction_6_3_1() -> Result<()> { } -#[test] -fn fraction_6_4_1() -> Result<()> { - let expr = "y=x2"; - test_braille("UEB", expr, "⠰⠽⠀⠐⠶⠀⠰⠰⠷⠭⠨⠌⠼⠃⠾")?; - return Ok(()); - -} - #[test] fn fraction_6_4_2() -> Result<()> { let expr = " @@ -1544,8 +1539,9 @@ fn fraction_6_4_5() -> Result<()> { #[test] fn fraction_6_4_6() -> Result<()> { let expr = "speed=distancetime"; - // GTM lists two options: "⠎⠏⠑⠫⠀⠐⠶⠀⠰⠰⠷⠙⠊⠎⠞⠁⠝⠉⠑⠨⠌⠞⠊⠍⠑⠾" and "⠰⠰⠰⠎⠏⠑⠑⠙⠀⠐⠶⠀⠷⠙⠊⠎⠞⠁⠝⠉⠑⠨⠌⠞⠊⠍⠑⠾⠰⠄" - test_braille("UEB", expr, "⠎⠏⠑⠫⠀⠐⠶⠀⠰⠰⠷⠙⠊⠎⠞⠁⠝⠉⠑⠨⠌⠞⠊⠍⠑⠾")?; + // GTM 2014 §6.4 listed word-indicator and passage alternatives (uncontracted words). + // GTM 1.7.5(a) (2025) prefers two grade 1 symbol indicators with contracted words. + test_braille("UEB", expr, "⠎⠏⠑⠫⠀⠐⠶⠀⠰⠷⠙⠊⠌⠨⠑⠨⠌⠐⠞⠰⠾")?; return Ok(()); } @@ -1869,7 +1865,9 @@ fn spacing_9_3_2_4() -> Result<()> { #[test] fn spacing_9_3_2_5() -> Result<()> { let expr = "Limx2"; - test_braille("UEB", expr, "⠰⠰⠠⠇⠊⠍⠷⠭⠨⠌⠼⠃⠾")?; + // GTM 2014 9.3.2 used a word indicator (;;,lim(x./#b)). GTM 1.7.3(a): one + // symbol indicator on the fraction open; numeric mode covers the closer. + test_braille("UEB", expr, "⠠⠇⠊⠍⠰⠷⠭⠨⠌⠼⠃⠾")?; return Ok(()); } @@ -1949,9 +1947,9 @@ fn text_9_7_1() -> Result<()> { fn stat_9_7_2() -> Result<()> { let expr = "Exp(R)= n2+1"; - // GTM uses "⠰⠰⠰⠠⠑⠭⠏⠐⠣⠠⠗⠐⠜⠀⠐⠶⠀⠷⠝⠨⠌⠼⠃⠾⠐⠖⠼⠁⠰⠄", - // but "⠠⠑⠭⠏⠐⠣⠠⠗⠐⠜⠀⠐⠶⠀⠰⠰⠷⠝⠨⠌⠼⠃⠾⠐⠖⠼⠁" is shorter and is consistent with omission_3_6_7 and fraction_6_4_6 - test_braille("UEB", expr, "⠠⠑⠭⠏⠐⠣⠠⠗⠐⠜⠀⠐⠶⠀⠰⠰⠷⠝⠨⠌⠼⠃⠾⠐⠖⠼⠁")?; + // GTM 2014 9.7 used a passage around the whole equation. GTM 1.7.3(a): one + // symbol indicator on n/2 (same as grade1_1_7_9_5). + test_braille("UEB", expr, "⠠⠑⠭⠏⠐⠣⠠⠗⠐⠜⠀⠐⠶⠀⠰⠷⠝⠨⠌⠼⠃⠾⠐⠖⠼⠁")?; return Ok(()); } diff --git a/tests/braille/UEB/other.rs b/tests/braille/UEB/other.rs index 210323d21..2d679b0ef 100644 --- a/tests/braille/UEB/other.rs +++ b/tests/braille/UEB/other.rs @@ -247,6 +247,23 @@ fn ueb_no_italic_typeform_for_math_letters() -> Result<()> { return Ok(()); } +#[test] +fn ueb_space_because_and_qed() -> Result<()> { + // ∵ and ∎ are spaced like ∴ (GTM 11 miscellaneous; same mo AddSpaces path as 1.7.9). + test_braille("UEB", "x=1", "⠈⠌⠀⠰⠭⠀⠐⠶⠀⠼⠁")?; + test_braille("UEB", "x=1", "⠰⠭⠀⠐⠶⠀⠼⠁⠀⠸⠫⠼⠙⠱")?; + return Ok(()); +} + +#[test] +fn ueb_math_typeform_not_part_of_word() -> Result<()> { + // Double-struck/script/fraktur letters are math alphabets, not part of an English + // letters-sequence, so a following whole word still takes its contraction (time → ⠐⠞). + test_braille("UEB", "Rtime", "⠈⠆⠠⠗⠐⠞")?; + test_braille("UEB", "R", "⠈⠆⠰⠠⠗")?; + return Ok(()); +} + #[test] fn ueb_italic_typeform_for_digits() -> Result<()> { // UEB uses italic typeform for digits (GTM 2.7 emphasis of numeric material). From 33257d257af6937023771c1d9e33f41773841351 Mon Sep 17 00:00:00 2001 From: "U-rainshowers\\neils" Date: Mon, 31 Aug 2026 13:48:49 +1000 Subject: [PATCH 6/9] update rc # --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 26749dc34..e4d07336f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mathcat" -version = "0.7.6-rc.3" +version = "0.7.6-rc.4" authors = ["Neil Soiffer "] license = "MIT" description = "MathCAT: Math Capable Assistive Technology ('Speech and braille from MathML')" From f3e57712db32559e93cc6628f28c8ef856c86e49 Mon Sep 17 00:00:00 2001 From: "U-rainshowers\\neils" Date: Mon, 31 Aug 2026 14:06:12 +1000 Subject: [PATCH 7/9] fix new clippy warnings --- src/braille.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/braille.rs b/src/braille.rs index 95a19e371..c893ee435 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -1152,11 +1152,10 @@ fn ueb_cleanup(pref_manager: Ref, raw_braille: String) -> Str let body = braille.replace(['C', '𝐶'], ""); const G1_START: &str = "⠰⠰⠰"; const G1_END: &str = "⠰⠄"; - if let Some(rest) = body.strip_prefix(G1_START) { - if let Some(mid) = rest.strip_suffix(G1_END) { + if let Some(rest) = body.strip_prefix(G1_START) + && let Some(mid) = rest.strip_suffix(G1_END) { return format!("{G1_START}CCC{mid}Ce{G1_END}"); } - } return format!("CCC{body}Ce"); } @@ -2247,11 +2246,10 @@ fn handle_contractions(full_chars: &[char], start: usize, end: usize, mut result let matches = CONTRACTION_PATTERNS.matches(&chars_as_str); for i in matches.iter() { let element = &CONTRACTIONS[i]; - if let Some(exceptions) = element.skip_if_word_in { - if exceptions.contains(&original_chars_as_str) { + if let Some(exceptions) = element.skip_if_word_in + && exceptions.contains(&original_chars_as_str) { continue; } - } if element.word_start_only && !word_start_ok { continue; } @@ -2689,11 +2687,10 @@ fn remove_optional_group_indicators(braille: &str) -> String { for (i, &ch) in chars.iter().enumerate() { if ch == 'b' { stack.push(i); - } else if ch == 'e' { - if let Some(b_idx) = stack.pop() { + } else if ch == 'e' + && let Some(b_idx) = stack.pop() { pairs.push((b_idx, i)); } - } } let mut convert_to_oc = vec![false; chars.len()]; @@ -2842,7 +2839,7 @@ fn french_cleanup(_pref_manager: Ref, raw_braille: String) -> // This reuses the code just for getting rid of unnecessary "L"s and "N"s let result = remove_unneeded_mode_changes(&result, UEB_Mode::Grade1, UEB_Duration::Passage); - debug!(" after removing mode changes={}", &result); + debug!(" after removing mode changes={}", result); // remove any grouping pair at the start or end -- we ensure they are a pair but making sure there is no "o" inside the string let result = if result.starts_with('o') && result.ends_with('c') && !result[1..result.len() - 1].contains('o') { @@ -4196,7 +4193,7 @@ mod tests { #[test] fn french_remove_optional_group_indicators() { assert_eq!(remove_optional_group_indicators("bLxe+"), "Lx+"); - assert_eq!(remove_optional_group_indicators("bLxeLy"), "oLxLy"); + assert_eq!(remove_optional_group_indicators("bLxeLy"), "oLxcLy"); assert_eq!(remove_optional_group_indicators("bLxbe+NeLz"), "oLx+NcLz"); // inner e is followed by outer e, then L — resolve outer pair first assert_eq!(remove_optional_group_indicators("bbLx+NeeLy"), "ooLx+NccLy"); From 54d638ca131ece9bcb251d02218e9f2218be51eb Mon Sep 17 00:00:00 2001 From: "U-rainshowers\\neils" Date: Mon, 31 Aug 2026 14:14:45 +1000 Subject: [PATCH 8/9] fix up Vietanmese tests --- Rules/Braille/Vietnam/Vietnam_Rules.yaml | 2 +- Rules/Languages/vi/SharedRules/default.yaml | 2 +- Rules/Languages/vi/SharedRules/geometry.yaml | 2 +- Rules/Languages/vi/unicode-full.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Rules/Braille/Vietnam/Vietnam_Rules.yaml b/Rules/Braille/Vietnam/Vietnam_Rules.yaml index 2b43067be..85146aeb7 100644 --- a/Rules/Braille/Vietnam/Vietnam_Rules.yaml +++ b/Rules/Braille/Vietnam/Vietnam_Rules.yaml @@ -389,7 +389,7 @@ - name: overparen tag: mover - match: "*[2][.='⏜']" + match: "*[2][.='⏜' or .='⌢' or .='⌒']" replace: - t: "⠫⠠⠗" # shape indicator, overparen - x: "*[1]" diff --git a/Rules/Languages/vi/SharedRules/default.yaml b/Rules/Languages/vi/SharedRules/default.yaml index 9eeb6267f..b70dff013 100644 --- a/Rules/Languages/vi/SharedRules/default.yaml +++ b/Rules/Languages/vi/SharedRules/default.yaml @@ -231,7 +231,7 @@ - name: overparen tag: mover - match: "*[2][.='⏜']" + match: "*[2][.='⏜' or .='⌢' or .='⌒']" replace: - x: "*[2]" # shape indicator, overparen - x: "*[1]" diff --git a/Rules/Languages/vi/SharedRules/geometry.yaml b/Rules/Languages/vi/SharedRules/geometry.yaml index 9623c5556..eaafd3a01 100644 --- a/Rules/Languages/vi/SharedRules/geometry.yaml +++ b/Rules/Languages/vi/SharedRules/geometry.yaml @@ -38,7 +38,7 @@ replace: # if: "$Verbosity='Verbose'" # then: [T: "the"] - - T: "vòng cung" + - T: "cung" # - test: - x: "*[1]" - x: "*[2]" diff --git a/Rules/Languages/vi/unicode-full.yaml b/Rules/Languages/vi/unicode-full.yaml index db70be9c7..f79caf2cf 100644 --- a/Rules/Languages/vi/unicode-full.yaml +++ b/Rules/Languages/vi/unicode-full.yaml @@ -1053,7 +1053,7 @@ - "⌏": [t: "cây trồng trên cùng bên trái"] # 0x230f (en: 'top left crop') - "⌐": [t: "đảo ngược không ký"] # 0x2310 (en: 'reversed not sign') - "⌑": [t: "hình vuông"] # 0x2311 (en: 'square lozenge') - - "⌒": [t: "vòng cung"] # 0x2312 (en: 'arc') + - "⌒": [t: "cung"] # 0x2312 (en: 'arc') - "⌓": [t: "bộ phận"] # 0x2313 (en: 'segment') - "⌔": [t: "khu vực"] # 0x2314 (en: 'sector') - "⌕": [t: "máy ghi âm điện thoại"] # 0x2315 (en: 'telephone recorder') From d9b766654b401aeb813dc29587c75bdf181bc346 Mon Sep 17 00:00:00 2001 From: "U-rainshowers\\neils" Date: Mon, 31 Aug 2026 15:40:37 +1000 Subject: [PATCH 9/9] fix str compile issue with no-unsafe feature --- src/braille.rs | 2 +- src/chemistry.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/braille.rs b/src/braille.rs index c893ee435..86eef3953 100644 --- a/src/braille.rs +++ b/src/braille.rs @@ -3809,7 +3809,7 @@ impl NeedsToBeGrouped { }; debug!("needs_grouping_for_french: mathml (sans '-') = {}", mml_to_string(mathml)); - let node_name = name(mathml); + let node_name = as_str!(name(mathml)); let children = mathml.children(); match node_name { "mn" | "mi" | "mtext" => { diff --git a/src/chemistry.rs b/src/chemistry.rs index e71750887..5bec42313 100644 --- a/src/chemistry.rs +++ b/src/chemistry.rs @@ -534,9 +534,9 @@ fn set_marked_chemistry_attr(mathml: Element, chem: &str) { // inherit from the base let base = as_element(mathml.children()[0]); if let Some(base_chem) = base.attribute(CHEM_EQUATION_OPERATOR) { - mathml.set_attribute_value(CHEM_EQUATION_OPERATOR, base_chem.value()); + mathml.set_attribute_value(CHEM_EQUATION_OPERATOR, as_str!(base_chem.value())); } else if let Some(base_chem) = base.attribute(CHEM_FORMULA_OPERATOR) { - mathml.set_attribute_value(CHEM_FORMULA_OPERATOR, base_chem.value()); + mathml.set_attribute_value(CHEM_FORMULA_OPERATOR, as_str!(base_chem.value())); } } _ => error!("Internal error: {} should not be marked as 'MAYBE_CHEMISTRY'", tag_name),