use super::*;
pub(super) fn styled_form_at(
tokens: &[EnglishToken],
i: usize,
) -> Option<super::super::token::Typeform> {
match tokens.get(i) {
Some(EnglishToken::Styled(_, form)) => Some(*form),
_ => None,
}
}
pub(super) fn spelled_letter_run(tokens: &[EnglishToken], i: usize) -> Option<(usize, usize)> {
let single = |k: usize| matches!(tokens.get(k), Some(EnglishToken::Word(w)) if w.len() == 1);
if !single(i) {
return None;
}
let mut start = i;
while start >= 2
&& matches!(tokens.get(start - 1), Some(EnglishToken::Symbol('-')))
&& single(start - 2)
{
start -= 2;
}
let mut last = i;
while matches!(tokens.get(last + 1), Some(EnglishToken::Symbol('-'))) && single(last + 2) {
last += 2;
}
let letter_count = (last - start) / 2 + 1;
let trails_into_word = matches!(tokens.get(last + 1), Some(EnglishToken::Symbol('-')))
&& matches!(tokens.get(last + 2), Some(EnglishToken::Word(w)) if w.len() > 1);
if letter_count < 3
|| (trails_into_word && letter_count < 4)
|| leading_stutter_prefix(tokens, start)
{
return None;
}
Some((start, last))
}
pub(super) fn leading_stutter_prefix(tokens: &[EnglishToken], start: usize) -> bool {
if start < 2 || !matches!(tokens.get(start - 1), Some(EnglishToken::Symbol('-'))) {
return false;
}
let Some(EnglishToken::Word(first)) = tokens.get(start) else {
return false;
};
let Some(&first_char) = first.first() else {
return false;
};
first.len() == 1
&& first_char.eq_ignore_ascii_case(&'o')
&& matches!(tokens.get(start - 2), Some(EnglishToken::Word(w)) if w.iter().collect::<String>().eq_ignore_ascii_case("so"))
}
pub(super) fn ends_spelled_letter_run_before_word(tokens: &[EnglishToken], i: usize) -> bool {
let Some(EnglishToken::Symbol('-')) = tokens.get(i) else {
return false;
};
let Some(prev_idx) = i.checked_sub(1) else {
return false;
};
let Some((_, last)) = spelled_letter_run(tokens, prev_idx) else {
return false;
};
if last != prev_idx {
return false;
}
matches!(tokens.get(i + 1), Some(EnglishToken::Word(w)) if w.len() >= 2)
}
pub(super) fn hyphenated_initialism_run(
tokens: &[EnglishToken],
i: usize,
) -> Option<(usize, usize)> {
let single_upper = |k: usize| matches!(tokens.get(k), Some(EnglishToken::Word(w)) if w.len() == 1 && w[0].is_uppercase());
if !single_upper(i) {
return None;
}
let mut start = i;
while start >= 2
&& matches!(tokens.get(start - 1), Some(EnglishToken::Symbol('-')))
&& single_upper(start - 2)
{
start -= 2;
}
let mut last = i;
while matches!(tokens.get(last + 1), Some(EnglishToken::Symbol('-'))) && single_upper(last + 2)
{
last += 2;
}
((last - start) / 2 + 1 >= 2 && matches!(tokens.get(last + 1), Some(EnglishToken::Symbol('.'))))
.then_some((start, last))
}
pub(super) fn token_plain_chars(tokens: &[EnglishToken]) -> Vec<char> {
let mut chars = Vec::new();
for token in tokens {
match token {
EnglishToken::Word(w) | EnglishToken::Number(w) | EnglishToken::Technical(w) => {
chars.extend(w);
}
EnglishToken::WordDivision { chars: w, .. } => chars.extend(w),
EnglishToken::Styled(c, _) | EnglishToken::Symbol(c) => chars.push(*c),
EnglishToken::Space => chars.push(' '),
EnglishToken::LineBreak => chars.push('\n'),
}
}
chars
}
pub(super) fn token_plain_chars_preserve_word_division(tokens: &[EnglishToken]) -> Vec<char> {
let mut chars = Vec::new();
for token in tokens {
match token {
EnglishToken::Word(w) | EnglishToken::Number(w) | EnglishToken::Technical(w) => {
chars.extend(w);
}
EnglishToken::WordDivision { chars: w, break_at } => {
chars.extend(&w[..*break_at]);
chars.push('\n');
chars.extend(&w[*break_at..]);
}
EnglishToken::Styled(c, _) | EnglishToken::Symbol(c) => chars.push(*c),
EnglishToken::Space => chars.push(' '),
EnglishToken::LineBreak => chars.push('\n'),
}
}
chars
}
pub(super) fn push_spatial_char(out: &mut Vec<u8>, c: char) -> Option<()> {
if c == ' ' {
out.push(SPACE);
} else if let Some(cells) = super::super::rule_16::line_arrow(c) {
out.extend(cells);
} else if c == '╳' {
out.push(decode_unicode('⠜'));
} else if c == '>' {
out.extend([CAPITAL, decode_unicode('⠜')]);
} else if c == '<' {
out.extend([CAPITAL, decode_unicode('⠣')]);
} else {
let cells = super::super::rule_16::spatial_symbol(c)?;
out.extend(cells);
}
Some(())
}
pub(super) fn encode_spatial_rows(rows: &[&str], grade1: bool) -> Option<Vec<u8>> {
let mut out = Vec::new();
if grade1 {
out.extend([
decode_unicode('⠐'),
decode_unicode('⠐'),
decode_unicode('⠿'),
GRADE1,
GRADE1,
GRADE1,
]);
out.push(255);
}
for (row_idx, row) in rows.iter().enumerate() {
if row_idx > 0 {
out.push(255);
}
for c in row.chars() {
push_spatial_char(&mut out, c)?;
}
}
if grade1 {
out.push(255);
out.extend([
decode_unicode('⠐'),
decode_unicode('⠐'),
decode_unicode('⠿'),
GRADE1,
decode_unicode('⠄'),
]);
}
Some(out)
}
pub(super) fn encode_rule_3_14_punctuation_box(tokens: &[EnglishToken]) -> Option<Vec<u8>> {
let text: String = token_plain_chars(tokens).into_iter().collect();
let rows: Vec<&str> = text.lines().collect();
if rows.len() != 3
|| !rows[0].starts_with('┌')
|| !rows[0].ends_with('┐')
|| !rows[1].starts_with('│')
|| !rows[1].ends_with('│')
|| !rows[2].starts_with('└')
|| !rows[2].ends_with('┘')
{
return None;
}
let headings: Vec<char> = rows[1]
.chars()
.filter(|c| !matches!(c, '│' | ' '))
.collect();
if headings.is_empty() {
return None;
}
let mut top = vec![SPACE];
for (idx, heading) in headings.iter().enumerate() {
if idx > 0 {
top.extend(std::iter::repeat_n(SPACE, if idx == 1 { 6 } else { 5 }));
}
top.extend([
decode_unicode('⠐'),
decode_unicode('⠐'),
decode_unicode('⠿'),
]);
if punctuation_grade1(&[EnglishToken::Symbol(*heading)], 0, *heading) {
top.push(GRADE1);
}
top.extend(super::super::rule_7::encode_punctuation(*heading)?);
}
let mut underline = Vec::new();
for idx in 0..headings.len() {
if idx > 0 {
underline.extend([SPACE, SPACE, SPACE]);
}
underline.push(decode_unicode('⠐'));
underline.extend(std::iter::repeat_n(decode_unicode('⠒'), 6));
}
top.push(255);
top.extend(underline);
Some(top)
}
pub(super) fn encode_rule_3_14_letter_grid(tokens: &[EnglishToken]) -> Option<Vec<u8>> {
let text: String = token_plain_chars_preserve_word_division(tokens)
.into_iter()
.collect();
let rows: Vec<Vec<char>> = text
.lines()
.map(|line| {
line.split_whitespace()
.filter_map(|part| {
let mut chars = part.chars();
let c = chars.next()?;
(chars.next().is_none() && c.is_ascii_uppercase()).then_some(c)
})
.collect::<Vec<_>>()
})
.collect();
if rows.len() < 2 || rows.iter().any(Vec::is_empty) {
return None;
}
let width = rows[0].len();
if width < 2 || rows.iter().any(|row| row.len() != width) {
return None;
}
let mut out = cells_from_unicode("⠐⠐⠿⠰⠰⠰⠠⠠⠠");
for row in rows {
out.push(255);
for (idx, c) in row.iter().enumerate() {
if idx > 0 {
out.push(SPACE);
}
out.push(crate::english::encode_english(c.to_ascii_lowercase()).ok()?);
}
}
out.push(255);
out.extend(cells_from_unicode("⠐⠐⠿⠠⠄⠰⠄"));
Some(out)
}
pub(super) fn encode_compact_spatial_example(tokens: &[EnglishToken]) -> Option<Vec<u8>> {
let chars = token_plain_chars(tokens);
if chars
== [
'─', '─', '─', '─', '╱', '▔', '▔', '▔', '▔', '▔', '▔', '╲', '─', '─', '─', '▁', '▁',
'│', '─', '─', '─', '─',
]
{
return Some(cells_from_unicode("⠐⠒⠒⠒⠊⠉⠉⠑⠒⠦⠤⠴⠒⠒"));
}
if chars.iter().all(|c| matches!(c, '╲')) && chars.len() == 1 {
return encode_spatial_rows(&["╲", " ╲", " ╲", " ╲"], false);
}
if chars.iter().all(|c| matches!(c, '┊')) && chars.len() == 1 {
return encode_spatial_rows(&["┊", "┊", "┊", "┊"], false);
}
if chars == ['╲', '╱', '╱'] {
return encode_spatial_rows(&["╲ >", " ╲ >", " ╲>"], false);
}
if chars == ['╱', '╲'] {
return encode_spatial_rows(&[" ╱╲", " ╱ ╲", " ╱ ╲"], true);
}
None
}
pub(super) fn cells_from_unicode(s: &str) -> Vec<u8> {
s.chars()
.map(|c| if c == '⠀' { SPACE } else { decode_unicode(c) })
.collect()
}
pub(super) fn wide_table_gap_before_number(
tokens: &[EnglishToken],
i: usize,
) -> Option<(usize, usize)> {
if !matches!(tokens.get(i), Some(EnglishToken::Space))
|| !matches!(
i.checked_sub(1).and_then(|p| tokens.get(p)),
Some(EnglishToken::Word(_))
)
{
return None;
}
let mut end = i;
while matches!(tokens.get(end), Some(EnglishToken::Space)) {
end += 1;
}
let previous_is_short_symbol = i.checked_sub(1).is_some_and(|p| {
matches!(tokens.get(p), Some(EnglishToken::Word(w)) if w.len() <= 2 && w.iter().any(|c| c.is_uppercase()))
});
if end - i < 5 {
return None;
}
let Some(EnglishToken::Number(digits)) = tokens.get(end) else {
return None;
};
let dots = if previous_is_short_symbol && digits.len() >= 3 {
0
} else if previous_is_short_symbol {
2
} else {
4
};
Some((end, dots))
}
pub(super) fn wide_table_gap_before_word(
tokens: &[EnglishToken],
i: usize,
) -> Option<(usize, usize)> {
if !matches!(tokens.get(i), Some(EnglishToken::Space))
|| !matches!(
i.checked_sub(1).and_then(|p| tokens.get(p)),
Some(EnglishToken::Word(_))
)
{
return None;
}
let mut end = i;
while matches!(tokens.get(end), Some(EnglishToken::Space)) {
end += 1;
}
let run = end - i;
let next_is_symbol = matches!(tokens.get(end), Some(EnglishToken::Word(w)) if w.len() <= 2 && w.iter().any(|c| c.is_uppercase()));
if !next_is_symbol || run < 5 {
return None;
}
let dots = if run >= 8 { 5 } else { 2 };
Some((end, dots))
}
pub(super) fn styled_column_gap(tokens: &[EnglishToken], i: usize) -> Option<usize> {
if !matches!(tokens.get(i), Some(EnglishToken::Space)) {
return None;
}
let mut end = i;
while matches!(tokens.get(end), Some(EnglishToken::Space)) {
end += 1;
}
if end - i < 3 || !tokens.iter().any(|t| matches!(t, EnglishToken::Styled(..))) {
return None;
}
if matches!(i.checked_sub(1).and_then(|p| tokens.get(p)), Some(EnglishToken::Styled(c, _)) if c.is_ascii_digit())
&& matches!(tokens.get(end), Some(EnglishToken::Styled(c, _)) if c.is_ascii_digit())
{
return None;
}
let styled_before = i.checked_sub(1).is_some_and(|p| {
matches!(tokens.get(p), Some(EnglishToken::Styled(..)))
|| (matches!(tokens.get(p), Some(EnglishToken::Symbol('.' | '#')))
&& p.checked_sub(1)
.is_some_and(|q| matches!(tokens.get(q), Some(EnglishToken::Styled(..)))))
});
let styled_after = matches!(tokens.get(end), Some(EnglishToken::Styled(..)));
(styled_before && styled_after).then_some(end)
}
pub(super) fn needs_spatial_grade1_passage(tokens: &[EnglishToken]) -> bool {
let has_diagonal = tokens
.iter()
.any(|token| matches!(token, EnglishToken::Symbol('╲' | '╱' | '╳')));
let has_game_board_letters = tokens.iter().any(|token| {
matches!(token, EnglishToken::Word(chars) if chars.len() == 1 && matches!(chars[0], 'X' | 'O'))
}) && tokens
.iter()
.any(|token| matches!(token, EnglishToken::Symbol('┼')));
has_diagonal || has_game_board_letters
}
pub(super) fn horizontal_run_reaches_arrow(tokens: &[EnglishToken], i: usize) -> bool {
let mut j = i + 1;
while matches!(tokens.get(j), Some(EnglishToken::Symbol(c)) if super::super::rule_16::is_line_char(*c))
{
j += 1;
}
matches!(tokens.get(j), Some(EnglishToken::Symbol(c)) if super::super::rule_16::line_arrow(*c).is_some())
}
pub(super) fn continues_across_bracket(tokens: &[EnglishToken], i: usize) -> bool {
if transcriber_note_ends_at(tokens, i, true)
|| closing_transcriber_note_starts_at(tokens, i + 1)
{
return false;
}
let is_bracket = |t: Option<&EnglishToken>| {
matches!(
t,
Some(EnglishToken::Symbol(
'(' | ')' | '[' | ']' | '{' | '}' | '"'
))
)
};
let is_texty = |t: Option<&EnglishToken>| {
matches!(
t,
Some(
EnglishToken::Word(_)
| EnglishToken::Number(_)
| EnglishToken::Styled(..)
| EnglishToken::Technical(_),
)
)
};
let forward = is_bracket(tokens.get(i + 1)) && is_texty(tokens.get(i + 2));
let backward = i.checked_sub(1).is_some_and(|p| is_bracket(tokens.get(p)))
&& i.checked_sub(2).is_some_and(|p| is_texty(tokens.get(p)));
let is_suffix = |w: &[char]| {
let lc = |c: &char| c.to_ascii_lowercase();
match w {
[a] => matches!(lc(a), 's' | 't' | 'd'),
[a, b] => matches!((lc(a), lc(b)), ('l', 'l') | ('r', 'e') | ('v', 'e')),
_ => false,
}
};
let apostrophe = matches!(tokens.get(i + 1), Some(EnglishToken::Symbol('\'')))
&& matches!(tokens.get(i + 2), Some(EnglishToken::Word(w)) if !is_suffix(w));
forward || backward || apostrophe
}
#[cfg(test)]
mod tests {
use super::super::test_support::{cells, enc};
use super::*;
#[rstest::rstest]
#[case::super_after_word("3 yd\u{00B3}", "⠼⠉⠀⠽⠙⠰⠔⠼⠉")]
#[case::sub_after_letter("vitamin B\u{2081}\u{2082}", "⠧⠊⠞⠁⠍⠔⠀⠠⠃⠰⠢⠼⠁⠃")]
#[case::subscript_letter_group("mass\u{209B}\u{1D64}\u{2099}", "⠍⠁⠎⠎⠰⠰⠢⠣⠎⠥⠝⠜")]
#[case::decimal_number_unit_subscript(
"an earthquake measuring 6.5MW",
"⠁⠝⠀⠑⠜⠹⠟⠥⠁⠅⠑⠀⠍⠂⠎⠥⠗⠬⠀⠼⠋⠲⠑⠠⠍⠢⠠⠺"
)]
#[case::super_after_number("born in 1682.\u{00B3}", "⠃⠕⠗⠝⠀⠔⠀⠼⠁⠋⠓⠃⠲⠔⠼⠉")]
#[case::super_after_word_inline("the clarion\u{00B9} horn", "⠮⠀⠉⠇⠜⠊⠕⠝⠰⠔⠼⠁⠀⠓⠕⠗⠝")]
fn encodes_script_3_24(#[case] text: &str, #[case] expected: &str) {
assert_eq!(enc(text), Some(cells(expected)));
}
#[rstest::rstest]
#[case::leading_superscript("\u{00B9} clarion", "⠰⠔⠼⠁⠀⠉⠇⠜⠊⠕⠝")]
#[case::super_letter_after_word("W\u{1D50}", "⠠⠺⠰⠔⠍")]
#[case::sub_digit_after_word("H\u{2082}O", "⠠⠓⠰⠢⠼⠃⠠⠕")]
#[case::super_digit_after_numeric_unit("4m\u{00B2}", "⠼⠙⠍⠔⠼⠃")]
fn encodes_scripts_in_prose_3_24(#[case] text: &str, #[case] expected: &str) {
assert_eq!(enc(text), Some(cells(expected)));
}
#[rstest::rstest]
#[case::solid("\u{2500}\u{2500}\u{2500}\u{2500}", "⠐⠒⠒⠒⠒")]
#[case::double("\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}", "⠐⠒⠶⠶⠶⠶⠶")]
#[case::double_with_arrow(
"\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}↓\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}",
"⠐⠒⠶⠶⠶⠶⠶⠳⠩⠶⠶⠶⠶⠶⠶"
)]
#[case::triple("\u{2261}\u{2261}\u{2261}", "⠐⠒⠿⠿⠿")]
#[case::corners(
"\u{2500}\u{2500}\u{2500}\u{2500}\u{2534}\u{2500}\u{2500}\u{2500}\u{2500}\u{2510}",
"⠐⠒⠒⠒⠒⠚⠒⠒⠒⠒⠲"
)]
#[case::diagonals("\u{2572}\u{2500}\u{2571}", "⠐⠒⠣⠒⠜")]
#[case::text_midpoint(
"\u{2500}\u{2500}\u{2500}\u{2500}cat\u{2500}\u{2500}\u{2500}\u{2500}",
"⠐⠒⠒⠒⠒⠄⠉⠁⠞⠐⠒⠒⠒⠒"
)]
fn encodes_box_drawing_16_2(#[case] text: &str, #[case] expected: &str) {
assert_eq!(enc(text), Some(cells(expected)));
}
#[rstest::rstest]
#[case::leading_indent_before_header(" 1\n ──", "⠼⠁⠀⠐⠒⠒")]
#[case::label_number_gap("Income 865.73", "⠠⠔⠉⠕⠍⠑⠀⠐⠐⠐⠐⠀⠀⠼⠓⠋⠑⠲⠛⠉")]
#[case::balance_number_gap("Balance 165.32", "⠠⠃⠁⠇⠨⠑⠀⠐⠐⠐⠐⠀⠀⠼⠁⠋⠑⠲⠉⠃")]
#[case::lead_element_row("lead Pb 82", "⠇⠂⠙⠀⠐⠐⠐⠐⠐⠀⠀⠠⠏⠃⠀⠐⠐⠀⠀⠼⠓⠃")]
#[case::lithium_element_row("lithium Li 3", "⠇⠊⠹⠊⠥⠍⠀⠐⠐⠀⠀⠠⠇⠊⠀⠐⠐⠀⠀⠼⠉")]
fn encodes_table_guide_dots_16_5_1(#[case] text: &str, #[case] expected: &str) {
assert_eq!(enc(text), Some(cells(expected)));
}
#[rstest::rstest]
#[case::lone_hline("\u{2500}")]
#[case::lone_triple("\u{2261}")]
fn lone_box_char_is_not_line_mode(#[case] text: &str) {
assert_eq!(enc(text), None);
}
#[test]
fn spatial_box_and_grid_helpers_cover_positive_and_negative_paths() {
let box_tokens = [
EnglishToken::Symbol('┌'),
EnglishToken::Symbol('─'),
EnglishToken::Symbol('┐'),
EnglishToken::LineBreak,
EnglishToken::Symbol('│'),
EnglishToken::Symbol('?'),
EnglishToken::Space,
EnglishToken::Symbol('!'),
EnglishToken::Symbol('│'),
EnglishToken::LineBreak,
EnglishToken::Symbol('└'),
EnglishToken::Symbol('─'),
EnglishToken::Symbol('┘'),
];
assert_eq!(
encode_rule_3_14_punctuation_box(&box_tokens),
Some(cells("⠀⠐⠐⠿⠰⠦⠀⠀⠀⠀⠀⠀⠐⠐⠿⠖\n⠐⠒⠒⠒⠒⠒⠒⠀⠀⠀⠐⠒⠒⠒⠒⠒⠒"))
);
assert_eq!(
encode_rule_3_14_punctuation_box(&[EnglishToken::Symbol('┌')]),
None
);
let grid_tokens = [
EnglishToken::Word(vec!['A']),
EnglishToken::Space,
EnglishToken::Word(vec!['B']),
EnglishToken::LineBreak,
EnglishToken::Word(vec!['C']),
EnglishToken::Space,
EnglishToken::Word(vec!['D']),
];
assert_eq!(
encode_rule_3_14_letter_grid(&grid_tokens),
Some(cells("⠐⠐⠿⠰⠰⠰⠠⠠⠠\n⠁⠀⠃\n⠉⠀⠙\n⠐⠐⠿⠠⠄⠰⠄"))
);
let ragged_grid = [
EnglishToken::Word(vec!['A']),
EnglishToken::Space,
EnglishToken::Word(vec!['B']),
EnglishToken::LineBreak,
EnglishToken::Word(vec!['C']),
];
assert_eq!(encode_rule_3_14_letter_grid(&ragged_grid), None);
}
#[rstest::rstest]
#[case::diagonal("╲", "⠣\n⠀⠣\n⠀⠀⠣\n⠀⠀⠀⠣")]
#[case::vertical("┊", "⠘\n⠘\n⠘\n⠘")]
#[case::crossing("╲╱╱", "⠣⠀⠀⠀⠀⠀⠀⠀⠀⠠⠜\n⠀⠀⠣⠀⠀⠀⠀⠠⠜\n⠀⠀⠀⠀⠣⠠⠜")]
fn compact_spatial_examples_encode_rows(#[case] text: &str, #[case] expected: &str) {
let tokens: Vec<EnglishToken> = text.chars().map(EnglishToken::Symbol).collect();
assert_eq!(
encode_compact_spatial_example(&tokens),
Some(cells(expected))
);
}
#[rstest::rstest]
#[case::sup_m(
'\u{1D50}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Superscript,
'm'
)]
#[case::sup_c(
'\u{1D9C}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Superscript,
'c'
)]
#[case::sub_a(
'\u{2090}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'a'
)]
#[case::sub_e(
'\u{2091}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'e'
)]
#[case::sub_h(
'\u{2095}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'h'
)]
#[case::sub_i(
'\u{1D62}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'i'
)]
#[case::sub_j(
'\u{2C7C}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'j'
)]
#[case::sub_k(
'\u{2096}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'k'
)]
#[case::sub_l(
'\u{2097}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'l'
)]
#[case::sub_m(
'\u{2098}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'm'
)]
#[case::sub_n(
'\u{2099}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'n'
)]
#[case::sub_o(
'\u{2092}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'o'
)]
#[case::sub_p(
'\u{209A}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'p'
)]
#[case::sub_r(
'\u{1D63}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'r'
)]
#[case::sub_s(
'\u{209B}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
's'
)]
#[case::sub_t(
'\u{209C}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
't'
)]
#[case::sub_u(
'\u{1D64}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'u'
)]
#[case::sub_v(
'\u{1D65}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'v'
)]
#[case::sub_x(
'\u{2093}',
crate::rules::english_ueb::rule_3_24::ScriptKind::Subscript,
'x'
)]
fn script_letter_maps_supported_letters(
#[case] input: char,
#[case] kind: crate::rules::english_ueb::rule_3_24::ScriptKind,
#[case] letter: char,
) {
assert_eq!(script_letter(input), Some((kind, letter)));
}
#[test]
fn encode_rare_word_and_spatial_paths() {
let engine = EnglishUebEngine::new();
assert!(
engine
.encode(
&[EnglishToken::Word(vec!['a']), EnglishToken::Symbol('ₙ'),],
false,
)
.is_none()
);
assert!(
engine
.encode(
&[
EnglishToken::Word(vec!['m', 'a', 's', 's']),
EnglishToken::Symbol('ₛ'),
EnglishToken::Symbol('ᵤ'),
EnglishToken::Symbol('ₙ'),
],
false,
)
.is_some()
);
assert!(
engine
.encode(
&[
EnglishToken::Word(vec!['A']),
EnglishToken::Symbol('='),
EnglishToken::Word(vec!['b']),
EnglishToken::Word(vec!['C']),
],
false,
)
.is_some()
);
assert!(
engine
.encode(
&[
EnglishToken::Symbol('┌'),
EnglishToken::Symbol('─'),
EnglishToken::Symbol('┼'),
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Symbol('─'),
EnglishToken::Symbol('┐'),
],
false,
)
.is_some()
);
assert!(
engine
.encode(
&[
EnglishToken::Styled('c', super::super::super::token::Typeform::Italic),
EnglishToken::Styled('h', super::super::super::token::Typeform::Italic),
EnglishToken::Styled('a', super::super::super::token::Typeform::Italic),
],
false,
)
.is_some()
);
assert_eq!(
engine
.encode(&[EnglishToken::Number(vec!['4', '2'])], false)
.unwrap(),
cells("⠼⠙⠃")
);
}
#[test]
fn encode_rare_spatial_layout_branches() {
let engine = EnglishUebEngine::new();
assert!(
engine
.encode(
&[
EnglishToken::WordDivision {
chars: vec!['a', 'b'],
break_at: 1,
},
EnglishToken::Symbol('\t'),
],
false,
)
.is_some()
);
assert!(
engine
.encode(
&[
EnglishToken::Symbol('│'),
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Symbol('│'),
EnglishToken::LineBreak,
],
false,
)
.is_some()
);
assert!(
engine
.encode(
&[
EnglishToken::Symbol('┐'),
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Symbol('┌'),
EnglishToken::Symbol('─'),
EnglishToken::LineBreak,
EnglishToken::Symbol('╲'),
],
false,
)
.is_none()
);
assert!(
engine
.encode(
&[
EnglishToken::Symbol('╲'),
EnglishToken::Space,
EnglishToken::Space,
EnglishToken::Symbol('│'),
EnglishToken::LineBreak,
],
false,
)
.is_some()
);
assert!(
engine
.encode(
&[
EnglishToken::Symbol('│'),
EnglishToken::Symbol('╲'),
EnglishToken::Symbol('│'),
EnglishToken::LineBreak,
],
false,
)
.is_some()
);
}
#[test]
fn encode_compact_spatial_example_handles_diagonal_pair() {
let tokens = [EnglishToken::Symbol('╱'), EnglishToken::Symbol('╲')];
assert!(encode_compact_spatial_example(&tokens).is_some());
}
#[test]
fn styled_column_gap_requires_a_space_at_index() {
let tokens = [EnglishToken::Word("a".chars().collect())];
assert_eq!(styled_column_gap(&tokens, 0), None);
}
#[test]
fn push_spatial_char_renders_line_arrow() {
let mut out = Vec::new();
assert_eq!(push_spatial_char(&mut out, '→'), Some(()));
assert!(!out.is_empty());
}
#[test]
fn encodes_box_drawing_vertical_tee_opening_line() {
let out = enc("\u{251C}\u{2500}").expect("should encode");
assert_eq!(out[..2], [decode_unicode('⠸'), decode_unicode('⠐')]);
}
#[test]
fn encodes_box_drawing_rectangle_marker_opening_line() {
let out = enc("\u{25AD}\u{2500}").expect("should encode");
let marker = [
decode_unicode('⠯'),
decode_unicode('⠭'),
decode_unicode('⠭'),
decode_unicode('⠭'),
decode_unicode('⠽'),
];
assert!(out.windows(5).any(|w| w == marker));
}
#[test]
fn rejects_script_run_with_mixed_non_digit_trailing_char() {
assert!(enc("x\u{00B2}\u{207B}").is_none());
}
#[test]
fn rejects_script_after_bare_symbol_base() {
assert!(enc("x(\u{00B2}").is_none());
}
#[test]
fn rejects_script_after_period_with_symbol_base() {
assert!(enc("x!.\u{00B2}").is_none());
}
}