use rio_unicode::grapheme::{grapheme_class, is_break, BreakState, GraphemeClass};
use std::sync::OnceLock;
const TABLE_LEN: usize = 0x2_0000;
static CLASS_TABLE: OnceLock<Box<[u8]>> = OnceLock::new();
static TRANSITIONS: OnceLock<Box<[u8]>> = OnceLock::new();
const CLASSES: usize = GraphemeClass::COUNT;
const STATES: usize = BreakState::COUNT;
const BREAK_BIT: u8 = 0x80;
fn class_table() -> &'static [u8] {
CLASS_TABLE.get_or_init(|| {
let mut table = vec![0u8; TABLE_LEN].into_boxed_slice();
for (cp, slot) in table.iter_mut().enumerate() {
if let Some(c) = char::from_u32(cp as u32) {
*slot = grapheme_class(c) as u8;
}
}
table
})
}
fn transitions() -> &'static [u8] {
TRANSITIONS.get_or_init(|| {
let mut table = vec![0u8; CLASSES * CLASSES * STATES].into_boxed_slice();
for prev in 0..CLASSES {
let prev_class = GraphemeClass::from_u8(prev as u8).unwrap();
for next in 0..CLASSES {
let next_class = GraphemeClass::from_u8(next as u8).unwrap();
for state in 0..STATES {
let mut break_state = BreakState::unpack(state as u8).unwrap();
let breaks = is_break(prev_class, next_class, &mut break_state);
table[(prev * CLASSES + next) * STATES + state] =
break_state.pack() | if breaks { BREAK_BIT } else { 0 };
}
}
}
table
})
}
#[inline]
pub fn class_of(c: char) -> u8 {
let cp = c as usize;
if cp < TABLE_LEN {
class_table()[cp]
} else {
grapheme_class(c) as u8
}
}
#[inline]
pub fn is_break_lut(prev: u8, next: u8, state: &mut u8) -> bool {
let entry = transitions()
[(prev as usize * CLASSES + next as usize) * STATES + *state as usize];
*state = entry & !BREAK_BIT;
entry & BREAK_BIT != 0
}
#[inline]
pub fn start_state(first: u8) -> u8 {
let entry = transitions()[(first as usize) * STATES]; entry & !BREAK_BIT
}
pub fn cluster_width(codepoints: &[u32]) -> (usize, u8) {
let Some(&first) = codepoints.first() else {
return (0, 0);
};
let Some(base) = char::from_u32(first) else {
return (1, 1);
};
let mut width = crate::codepoint_width::codepoint_width(first).unwrap_or(1);
let mut prev = class_of(base);
let mut state = start_state(prev);
let mut last_cp = base;
let mut len = 1;
while len < codepoints.len() {
let Some(c) = char::from_u32(codepoints[len]) else {
break;
};
let class = class_of(c);
let state_before = state;
if is_break_lut(prev, class, &mut state) {
break;
}
match c {
'\u{FE0F}' | '\u{FE0E}' => {
if crate::crosswords::vs_is_valid_base(last_cp, c) {
width = if c == '\u{FE0F}' { 2 } else { 1 };
prev = class;
last_cp = c;
} else {
state = state_before;
}
}
_ => {
if crate::codepoint_width::codepoint_width(codepoints[len]).unwrap_or(0)
> 0
{
width = 2;
}
prev = class;
last_cp = c;
}
}
len += 1;
}
(len, width)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tables_match_reference() {
for cp in (0..TABLE_LEN as u32 + 0x100).step_by(7) {
if let Some(c) = char::from_u32(cp) {
assert_eq!(class_of(c), grapheme_class(c) as u8, "class of U+{cp:04X}");
}
}
for prev in 0..CLASSES as u8 {
for next in 0..CLASSES as u8 {
for state in 0..STATES as u8 {
let mut reference = BreakState::unpack(state).unwrap();
let expected = is_break(
GraphemeClass::from_u8(prev).unwrap(),
GraphemeClass::from_u8(next).unwrap(),
&mut reference,
);
let mut packed = state;
let got = is_break_lut(prev, next, &mut packed);
assert_eq!(got, expected, "break ({prev},{next},{state})");
assert_eq!(packed, reference.pack(), "state ({prev},{next},{state})");
}
}
}
}
#[test]
fn cluster_width_measures_first_cluster() {
let cases: &[(&[u32], usize, u8)] = &[
(&[], 0, 0),
(&[0x61, 0x62], 1, 1), (&[0x4E00], 1, 2), (&[0x65, 0x301, 0x62], 2, 1), (&[0x2764, 0xFE0F], 2, 2), (&[0x231A, 0xFE0E], 2, 1), (&[0x61, 0xFE0F], 2, 1), (&[0x61, 0xFE0F, 0x301], 3, 1), (&[0x61, 0xFE0F, 0xFE0F], 3, 1), (&[0x231A, 0xFE0E, 0xFE0F], 3, 1), (&[0x31, 0xFE0F, 0x20E3], 3, 2), (&[0x31, 0x20E3], 2, 1), (&[0x1F468, 0x200D, 0x1F33E], 3, 2), (&[0x1F3F4, 0x200D, 0x2620, 0xFE0F], 4, 2), (&[0x1F468, 0x200D, 0x1F469, 0x200D, 0x1F467], 5, 2), (&[0x1F44D, 0x1F3FB], 2, 2), (&[0x1F1E7, 0x1F1F7, 0x1F1E7, 0x1F1F7], 2, 2), (&[0x110000, 0x61], 1, 1), (&[0x65, 0x301, 0xD800], 2, 1), (&[0x61, 0x110000], 1, 1), (&[0x301, 0x301], 2, 0), ];
for &(cps, len, width) in cases {
assert_eq!(cluster_width(cps), (len, width), "input {cps:04X?}");
}
}
#[test]
fn start_state_matches_reference() {
for class in 0..CLASSES as u8 {
let reference = BreakState::start(GraphemeClass::from_u8(class).unwrap());
assert_eq!(start_state(class), reference.pack(), "start({class})");
}
}
}