use crate::code_type::CodeType;
#[derive(Copy, Clone, PartialEq, Eq)]
pub(crate) enum Side {
Before,
After,
Both,
}
impl Side {
const fn before(self) -> bool {
matches!(self, Side::Before | Side::Both)
}
const fn after(self) -> bool {
matches!(self, Side::After | Side::Both)
}
}
pub(crate) struct Gap {
space: char,
spaced: &'static [(char, Side)],
}
pub(crate) fn of(code: CodeType) -> Option<&'static Gap> {
match code {
CodeType::MenkShape => Some(&MENK_SHAPE),
CodeType::Z52 => Some(&Z52),
_ => None,
}
}
pub(crate) static MENK_SHAPE: Gap = Gap {
space: '\u{E263}',
spaced: MENK_SHAPE_SPACED,
};
pub(crate) static Z52: Gap = Gap {
space: '\u{202F}',
spaced: Z52_SPACED,
};
const MENK_SHAPE_SPACED: &[(char, Side)] = &[
('\u{E235}', Side::Before), ('\u{E236}', Side::Before), ('\u{E237}', Side::Before), ('\u{E238}', Side::Before), ('\u{E239}', Side::Before), ('\u{E23C}', Side::Before), ('\u{E23D}', Side::Before), ('\u{E243}', Side::Both), ('\u{E24E}', Side::Before), ('\u{E24F}', Side::Before), ('\u{E250}', Side::Before), ('\u{E251}', Side::Before), ('\u{E252}', Side::Before), ('\u{E253}', Side::Before), ('\u{E254}', Side::After), ('\u{E255}', Side::Before), ('\u{E256}', Side::After), ('\u{E257}', Side::Before), ('\u{E258}', Side::After), ('\u{E259}', Side::Before), ('\u{E25A}', Side::After), ('\u{E25B}', Side::Before), ('\u{E25C}', Side::After), ('\u{E25D}', Side::Before), ];
const Z52_SPACED: &[(char, Side)] = &[
('\u{184F}', Side::Both), ('\u{1850}', Side::Before), ('\u{1851}', Side::Before), ('\u{1852}', Side::Before), ('\u{1853}', Side::Before), ('\u{1854}', Side::Before), ('\u{1855}', Side::Before), ('\u{1856}', Side::After), ('\u{1857}', Side::Before), ('\u{1858}', Side::After), ('\u{1859}', Side::Before), ('\u{185A}', Side::After), ('\u{185B}', Side::Before), ('\u{185C}', Side::After), ('\u{185D}', Side::Before), ('\u{185E}', Side::After), ('\u{185F}', Side::Before), ];
impl Gap {
fn side(&self, c: char) -> Option<Side> {
self.spaced
.binary_search_by_key(&c, |&(mark, _)| mark)
.ok()
.map(|index| self.spaced[index].1)
}
pub(crate) fn insert(&self, text: &str) -> String {
let chars: Vec<char> = text.chars().collect();
let mut out = String::with_capacity(text.len() + chars.len());
for (index, &c) in chars.iter().enumerate() {
let Some(side) = self.side(c) else {
out.push(c);
continue;
};
if side.before() {
match out.chars().next_back() {
None => {}
Some(' ') => {
out.pop();
out.push(self.space);
}
Some(previous) if previous == self.space || previous.is_whitespace() => {}
Some(_) => out.push(self.space),
}
}
out.push(c);
if side.after() {
match chars.get(index + 1) {
None => {}
Some(' ') => out.push(self.space),
Some(&next) if next == self.space || next.is_whitespace() => {}
Some(_) => out.push(self.space),
}
}
}
let mut collapsed = String::with_capacity(out.len());
let mut previous = None;
for c in out.chars() {
if c == ' ' && previous == Some(self.space) {
continue;
}
collapsed.push(c);
previous = Some(c);
}
collapsed
}
pub(crate) fn strip(&self, text: &str) -> String {
let chars: Vec<char> = text.chars().collect();
let mut out = String::with_capacity(text.len());
let mut skip_next = false;
for (index, &c) in chars.iter().enumerate() {
if skip_next {
skip_next = false;
continue;
}
if c == self.space
&& chars
.get(index + 1)
.is_some_and(|&next| self.side(next).is_some_and(Side::before))
{
continue;
}
out.push(c);
if self.side(c).is_some_and(Side::after) && chars.get(index + 1) == Some(&self.space) {
skip_next = true;
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
const MENK_GAP: &str = "\u{E263}";
const Z52_GAP: &str = "\u{202F}";
#[test]
fn both_tables_are_sorted_so_the_search_finds_every_mark() {
for gap in [&MENK_SHAPE, &Z52] {
assert!(gap.spaced.windows(2).all(|pair| pair[0].0 < pair[1].0));
for &(mark, _) in gap.spaced {
assert!(gap.side(mark).is_some(), "{mark:?} not found");
}
}
let count = |gap: &Gap, want: Side| gap.spaced.iter().filter(|(_, s)| *s == want).count();
assert_eq!(MENK_SHAPE.spaced.len(), 24);
assert_eq!(count(&MENK_SHAPE, Side::Before), 18);
assert_eq!(count(&MENK_SHAPE, Side::After), 5);
assert_eq!(count(&MENK_SHAPE, Side::Both), 1);
assert_eq!(Z52.spaced.len(), 17);
assert_eq!(count(&Z52, Side::Before), 11);
assert_eq!(count(&Z52, Side::After), 5);
assert_eq!(count(&Z52, Side::Both), 1);
}
#[test]
fn only_the_encoding_that_owns_a_mark_spaces_it() {
assert_eq!(
Z52.insert("\u{E2B5}\u{E236}\u{E271}"),
"\u{E2B5}\u{E236}\u{E271}"
);
assert_eq!(
MENK_SHAPE.insert("\u{186B}\u{1852}\u{1867}"),
"\u{186B}\u{1852}\u{1867}"
);
}
#[test]
fn marks_that_take_no_gap_are_untouched() {
for mark in [
'\u{E234}', '\u{E23A}', '\u{E23B}', '\u{E25E}', '\u{E25F}', '\u{E260}', '\u{E261}',
] {
let text = format!("\u{E2B5}{mark}\u{E271}");
assert_eq!(MENK_SHAPE.insert(&text), text, "{mark:?}");
assert_eq!(MENK_SHAPE.strip(&text), text, "{mark:?}");
}
for mark in [
'\u{1860}', '\u{1861}', '\u{1862}', '\u{1863}', '\u{1802}', '\u{1800}',
] {
let text = format!("\u{186B}{mark}\u{1867}");
assert_eq!(Z52.insert(&text), text, "{mark:?}");
assert_eq!(Z52.strip(&text), text, "{mark:?}");
}
}
#[test]
fn a_gap_is_written_on_the_side_the_mark_takes() {
assert_eq!(
MENK_SHAPE.insert("\u{E2B5}\u{E236}\u{E271}"),
format!("\u{E2B5}{MENK_GAP}\u{E236}\u{E271}")
);
assert_eq!(
MENK_SHAPE.insert("\u{E2B5}\u{E254}\u{E271}"),
format!("\u{E2B5}\u{E254}{MENK_GAP}\u{E271}")
);
assert_eq!(
MENK_SHAPE.insert("\u{E2B5}\u{E243}\u{E271}"),
format!("\u{E2B5}{MENK_GAP}\u{E243}{MENK_GAP}\u{E271}")
);
assert_eq!(
Z52.insert("\u{186B}\u{1852}\u{1867}"),
format!("\u{186B}{Z52_GAP}\u{1852}\u{1867}")
);
assert_eq!(
Z52.insert("\u{186B}\u{1856}\u{1867}"),
format!("\u{186B}\u{1856}{Z52_GAP}\u{1867}")
);
assert_eq!(
Z52.insert("\u{186B}\u{184F}\u{1867}"),
format!("\u{186B}{Z52_GAP}\u{184F}{Z52_GAP}\u{1867}")
);
}
#[test]
fn an_ordinary_space_is_replaced_not_doubled() {
assert_eq!(
MENK_SHAPE.insert("\u{E2B5} \u{E236}"),
format!("\u{E2B5}{MENK_GAP}\u{E236}")
);
assert_eq!(
MENK_SHAPE.insert("\u{E254} \u{E271}"),
format!("\u{E254}{MENK_GAP}\u{E271}")
);
assert_eq!(
Z52.insert("\u{186B} \u{1852}"),
format!("\u{186B}{Z52_GAP}\u{1852}")
);
let once = MENK_SHAPE.insert("\u{E2B5}\u{E263}\u{E236}\u{E271}");
assert_eq!(once, "\u{E2B5}\u{E263}\u{E236}\u{E271}");
assert_eq!(MENK_SHAPE.insert(&once), once);
}
#[test]
fn other_whitespace_already_separates_the_mark() {
for space in ["\t", "\n", "\r\n"] {
let text = format!("\u{E2B5}{space}\u{E236}");
assert_eq!(MENK_SHAPE.insert(&text), text, "{space:?}");
}
assert_eq!(MENK_SHAPE.insert("\u{E236}\u{E271}"), "\u{E236}\u{E271}");
assert_eq!(MENK_SHAPE.insert("\u{E2B5}\u{E254}"), "\u{E2B5}\u{E254}");
}
#[test]
fn two_marks_side_by_side_share_one_gap() {
assert_eq!(
Z52.insert("\u{186B}\u{184F}\u{1854}\u{1867}"),
format!("\u{186B}{Z52_GAP}\u{184F}{Z52_GAP}\u{1854}\u{1867}")
);
}
#[test]
fn strip_undoes_insert() {
for (gap, text) in [
(&MENK_SHAPE, "\u{E2B5}\u{E236}\u{E271}"),
(&MENK_SHAPE, "\u{E2B5}\u{E254}\u{E271}"),
(&MENK_SHAPE, "\u{E2B5}\u{E243}\u{E271}"),
(&MENK_SHAPE, "\u{E2B5}\u{E236}\u{E271}\u{E254}\u{E2B5}"),
(&MENK_SHAPE, "\u{E2B5}\u{E234}\u{E271}"),
(&MENK_SHAPE, "plain text"),
(&MENK_SHAPE, ""),
(&Z52, "\u{186B}\u{1852}\u{1867}"),
(&Z52, "\u{186B}\u{1856}\u{1867}"),
(&Z52, "\u{186B}\u{184F}\u{1854}\u{1867}"),
(&Z52, "\u{186B}\u{1802}\u{1867}"),
] {
assert_eq!(gap.strip(&gap.insert(text)), text, "{text:?}");
}
}
#[test]
fn a_space_the_text_meant_to_have_survives_the_round_trip() {
assert_eq!(
MENK_SHAPE.strip("\u{E2B5}\u{E263}\u{E271}"),
"\u{E2B5}\u{E263}\u{E271}"
);
assert_eq!(
MENK_SHAPE.strip("\u{E236}\u{E263}\u{E271}"),
"\u{E236}\u{E263}\u{E271}"
);
assert_eq!(
MENK_SHAPE.strip("\u{E2B5}\u{E263}\u{E254}"),
"\u{E2B5}\u{E263}\u{E254}"
);
assert_eq!(
Z52.strip("\u{186B}\u{202F}\u{1867}"),
"\u{186B}\u{202F}\u{1867}"
);
}
}