use core::ops::RangeInclusive;
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct LineBreakContext {
pub before_before: Option<char>,
pub before: char,
pub after: char,
}
pub type LineBreakOverrideFn = dyn Fn(LineBreakContext) -> Option<bool> + Send + Sync;
pub static CHROMIUM_LINE_BREAK_OVERRIDE: &LineBreakOverrideFn =
&(chromium_override as fn(LineBreakContext) -> Option<bool>);
fn chromium_override(cx: LineBreakContext) -> Option<bool> {
let LineBreakContext {
before_before,
before,
after,
..
} = cx;
if before == ' ' && after != ' ' {
return Some(true);
}
if before == '-' && after.is_ascii_digit() {
return Some(before_before.is_some_and(|c| c.is_ascii_alphanumeric()));
}
CHROMIUM_LINE_BREAK_TABLE.lookup(before, after)
}
static CHROMIUM_LINE_BREAK_TABLE: AsciiLineBreakTable<5> =
AsciiLineBreakTableBuilder::chromium().build::<5>();
#[derive(Clone)]
pub struct AsciiLineBreakTable<const N: usize> {
row_of: [u8; 128],
rows: [Row; N],
}
impl<const N: usize> AsciiLineBreakTable<N> {
pub const fn lookup(&self, before: char, after: char) -> Option<bool> {
let (b, a) = (before as u32, after as u32);
if b >= 128 || a >= 128 {
return None;
}
let row = &self.rows[self.row_of[b as usize] as usize];
if row.overridden & (1_u128 << a) == 0 {
return None;
}
Some(row.allow & (1_u128 << a) != 0)
}
}
#[derive(Clone, Copy)]
struct Row {
overridden: u128,
allow: u128,
}
#[derive(Clone)]
pub struct AsciiLineBreakTableBuilder {
overridden: [u128; 128],
allow: [u128; 128],
}
impl AsciiLineBreakTableBuilder {
pub const fn new() -> Self {
Self {
overridden: [0; 128],
allow: [0; 128],
}
}
pub const fn with_pairs(
mut self,
before: RangeInclusive<char>,
after: RangeInclusive<char>,
allow_break: bool,
) -> Self {
assert!(
*before.end() as u32 <= 0x7f && *after.end() as u32 <= 0x7f,
"only printable ASCII is supported",
);
let mut before_pos = *before.start() as u32;
while before_pos <= *before.end() as u32 {
let mut after_pos = *after.start() as u32;
while after_pos <= *after.end() as u32 {
let bit = 1_u128 << after_pos;
self.overridden[before_pos as usize] |= bit;
if allow_break {
self.allow[before_pos as usize] |= bit;
} else {
self.allow[before_pos as usize] &= !bit;
}
after_pos += 1;
}
before_pos += 1;
}
self
}
pub const fn build<const N: usize>(&self) -> AsciiLineBreakTable<N> {
let mut rows = [Row {
overridden: 0,
allow: 0,
}; N];
let mut len = 1_usize;
let mut row_of = [0_u8; 128];
let mut b = 0;
while b < 128 {
let (ov, al) = (self.overridden[b], self.allow[b]);
let mut idx = 0;
let mut found = usize::MAX;
while idx < len {
if rows[idx].overridden == ov && rows[idx].allow == al {
found = idx;
break;
}
idx += 1;
}
let r = if found != usize::MAX {
found
} else {
assert!(
len < N,
"N is too small to contain table. Repeat with a larger N."
);
rows[len] = Row {
overridden: ov,
allow: al,
};
len += 1;
len - 1
};
row_of[b] = r as u8;
b += 1;
}
assert!(
len == N,
"N is larger than required. Repeat with a smaller N."
);
AsciiLineBreakTable { row_of, rows }
}
const fn chromium() -> Self {
const ALL: RangeInclusive<char> = '!'..='\u{7f}';
Self::new()
.with_pairs(ALL, ALL, false)
.with_pairs(ALL, '('..='(', true)
.with_pairs(ALL, '<'..='<', true)
.with_pairs(ALL, '['..='[', true)
.with_pairs(ALL, '{'..='{', true)
.with_pairs('-'..='-', ALL, true)
.with_pairs('?'..='?', ALL, true)
.with_pairs('-'..='-', '$'..='$', false)
.with_pairs(ALL, '!'..='!', false)
.with_pairs('?'..='?', '"'..='"', false)
.with_pairs('?'..='?', '\''..='\'', false)
.with_pairs(ALL, ')'..=')', false)
.with_pairs(ALL, ','..=',', false)
.with_pairs(ALL, '.'..='.', false)
.with_pairs(ALL, '/'..='/', false)
.with_pairs('-'..='-', '0'..='9', false)
.with_pairs(ALL, ':'..=':', false)
.with_pairs(ALL, ';'..=';', false)
.with_pairs(ALL, '?'..='?', false)
.with_pairs(ALL, ']'..=']', false)
.with_pairs(ALL, '}'..='}', false)
.with_pairs('$'..='$', ALL, false)
.with_pairs('\''..='\'', ALL, false)
.with_pairs('('..='(', ALL, false)
.with_pairs('/'..='/', ALL, false)
.with_pairs('0'..='9', ALL, false)
.with_pairs('<'..='<', ALL, false)
.with_pairs('@'..='@', ALL, false)
.with_pairs('A'..='Z', ALL, false)
.with_pairs('['..='[', ALL, false)
.with_pairs('^'..='`', ALL, false)
.with_pairs('a'..='z', ALL, false)
.with_pairs('{'..='{', ALL, false)
.with_pairs('\u{7f}'..='\u{7f}', ALL, false)
}
}
impl Default for AsciiLineBreakTableBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::AsciiLineBreakTableBuilder;
use super::CHROMIUM_LINE_BREAK_TABLE;
use super::LineBreakContext;
use super::chromium_override;
fn cx(before_before: Option<char>, before: char, after: char) -> LineBreakContext {
LineBreakContext {
before_before,
before,
after,
}
}
#[test]
fn chromium_hyphen_digit_depends_on_preceding_char() {
assert_eq!(chromium_override(cx(Some('D'), '-', '1')), Some(true));
assert_eq!(chromium_override(cx(Some('4'), '-', '5')), Some(true));
assert_eq!(chromium_override(cx(Some(' '), '-', '1')), Some(false));
assert_eq!(chromium_override(cx(Some('('), '-', '1')), Some(false));
assert_eq!(chromium_override(cx(None, '-', '1')), Some(false));
}
#[test]
fn chromium_ignores_uax_14_lb13() {
for after in ['}', ')', ']', '!', '.', ',', '/', ':', ';', '?', 'b', '('] {
assert_eq!(
chromium_override(cx(None, ' ', after)),
Some(true),
"expected a break after the space, before {after:?}"
);
}
}
#[test]
fn chromium_hyphen_non_digit_defers_to_table() {
assert_eq!(chromium_override(cx(Some('D'), '-', 'b')), Some(true));
assert_eq!(chromium_override(cx(None, '-', 'b')), Some(true));
assert_eq!(chromium_override(cx(Some('D'), '-', 'é')), None);
}
#[test]
fn chromium_suppresses_ascii_punctuation_breaks() {
let t = &CHROMIUM_LINE_BREAK_TABLE;
assert_eq!(t.lookup('a', '/'), Some(false));
assert_eq!(t.lookup('/', 'b'), Some(false));
assert_eq!(t.lookup('a', '.'), Some(false));
assert_eq!(t.lookup('a', ':'), Some(false));
assert_eq!(t.lookup('a', 'b'), Some(false));
}
#[test]
fn chromium_allows_some_breaks() {
let t = &CHROMIUM_LINE_BREAK_TABLE;
assert_eq!(t.lookup(')', '('), Some(true));
assert_eq!(t.lookup(')', '<'), Some(true));
assert_eq!(t.lookup('a', '('), Some(false));
assert_eq!(t.lookup('-', 'b'), Some(true));
assert_eq!(t.lookup('?', 'b'), Some(true));
assert_eq!(t.lookup('-', '5'), Some(false));
assert_eq!(t.lookup('?', '"'), Some(false));
}
#[test]
fn non_ascii_pairs_defer_to_icu() {
let t = &CHROMIUM_LINE_BREAK_TABLE;
assert_eq!(t.lookup('a', 'é'), None);
assert_eq!(t.lookup('é', 'a'), None);
assert_eq!(t.lookup('a', ' '), None);
}
#[test]
fn empty_table_defers_to_icu() {
let t = AsciiLineBreakTableBuilder::new().build::<1>();
assert_eq!(t.lookup('a', '/'), None);
assert_eq!(t.lookup('a', '('), None);
}
#[test]
fn dedup_matches_dense_for_chromium() {
let builder = AsciiLineBreakTableBuilder::chromium();
for b in 0..128_u32 {
for a in 0..128_u32 {
let bit = 1_u128 << a;
let dense = if builder.overridden[b as usize] & bit == 0 {
None
} else {
Some(builder.allow[b as usize] & bit != 0)
};
let (before, after) = (char::from_u32(b).unwrap(), char::from_u32(a).unwrap());
assert_eq!(
CHROMIUM_LINE_BREAK_TABLE.lookup(before, after),
dense,
"mismatch at (before={b:#04x}, after={a:#04x})",
);
}
}
}
}