use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
pub(crate) fn joins_cluster(base: char, marks: &[char], c: char) -> bool {
const VIRTUAL_PREFIX: usize = 4096;
const INITIAL_TAIL: usize = 2;
let total = 1 + marks.len();
let mut take = INITIAL_TAIL;
loop {
let take_now = take.min(total);
let from_start = take_now == total;
let mut chunk = String::new();
if from_start {
chunk.push(base);
chunk.extend(marks.iter().copied());
} else {
chunk.extend(marks[marks.len() - take_now..].iter().copied());
}
let tail_bytes = chunk.len();
chunk.push(c);
let start = if from_start { 0 } else { VIRTUAL_PREFIX };
let mut cursor = GraphemeCursor::new(start + tail_bytes, start + chunk.len(), true);
match cursor.is_boundary(&chunk, start) {
Ok(is_break) => return !is_break,
Err(GraphemeIncomplete::PreContext(_)) if !from_start => {
take = take.saturating_mul(4);
}
Err(_) => return false,
}
}
}
pub(crate) fn width_may_change(c: char, first_join: bool, base_is_wide: bool) -> bool {
(is_variation_selector(c) && first_join) || (!base_is_wide && scalar_occupies_a_column(c))
}
fn is_variation_selector(c: char) -> bool {
('\u{FE00}'..='\u{FE0F}').contains(&c)
}
fn scalar_occupies_a_column(c: char) -> bool {
unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) > 0
}
#[cfg(test)]
mod tests {
use super::*;
fn extends(prev: &str, c: char) -> bool {
let mut it = prev.chars();
match it.next() {
None => false,
Some(base) => joins_cluster(base, &it.collect::<Vec<_>>(), c),
}
}
#[test]
fn combining_mark_extends() {
assert!(extends("e", '\u{0301}'));
}
#[test]
fn zwj_extends_then_joined_emoji_extends() {
assert!(extends("\u{1F468}", '\u{200D}'), "👨 + ZWJ");
assert!(extends("\u{1F468}\u{200D}", '\u{1F469}'), "👨 + 👩");
}
#[test]
fn skin_tone_modifier_extends() {
assert!(extends("\u{1F44D}", '\u{1F3FB}'));
}
#[test]
fn regional_indicator_pair_extends_but_the_third_breaks() {
assert!(extends("\u{1F1F0}", '\u{1F1F7}'), "🇰 + 🇷 = 🇰🇷");
assert!(!extends("\u{1F1F0}\u{1F1F7}", '\u{1F1FA}'), "🇰🇷 + 🇺 breaks");
}
#[test]
fn vs16_and_vs15_selectors_extend() {
assert!(extends("\u{25B6}", '\u{FE0F}'), "▶ + VS16");
assert!(extends("\u{25B6}", '\u{FE0E}'), "▶ + VS15");
}
#[test]
fn a_new_base_scalar_breaks() {
assert!(!extends("\u{1F468}", 'A'), "👨 + A breaks");
assert!(!extends("A", 'B'), "A + B breaks");
assert!(!extends("\u{4E2D}", '\u{6587}'), "中 + 文 breaks (CJK)");
}
#[test]
fn empty_prefix_never_extends() {
assert!(!extends("", 'A'));
assert!(!extends("", '\u{1F468}'));
}
#[test]
fn a_long_extend_run_before_a_zwj_still_reaches_its_emoji_base() {
let mut marks: Vec<char> = std::iter::repeat_n('\u{0301}', 500).collect();
marks.push('\u{200D}');
assert!(
joins_cluster('\u{1F468}', &marks, '\u{1F469}'),
"the joining emoji still finds its pictographic base 502 scalars back"
);
assert!(
!joins_cluster('x', &marks, '\u{1F469}'),
"no pictographic base 502 scalars back, so GB11 does not apply"
);
}
#[test]
fn regional_indicator_parity_survives_the_tail() {
assert!(joins_cluster('\u{1F1F0}', &[], '\u{1F1F7}'));
assert!(!joins_cluster('\u{1F1F0}', &['\u{1F1F7}'], '\u{1F1FA}'));
}
#[test]
fn width_gate_never_skips_a_real_change() {
use unicode_width::UnicodeWidthStr;
const BASES: [char; 14] = [
'x',
' ',
'中',
'\u{25B6}',
'\u{231A}',
'\u{1F468}',
'\u{1F600}',
'\u{0915}',
'\u{0600}',
'\u{1100}',
'\u{6F22}',
'\u{1F1F0}',
'\u{05D0}',
'\u{0644}',
];
const JOINERS: [char; 11] = [
'\u{FE0F}',
'\u{FE0E}',
'\u{FE01}',
'\u{0301}',
'\u{200D}',
'\u{1F1F7}',
'\u{1F3FB}',
'\u{20E3}',
'\u{0915}',
'\u{1F469}',
'\u{25B6}',
];
fn action(w: usize, is_wide: bool) -> (bool, bool) {
(w == 2 && !is_wide, w == 1 && is_wide)
}
let mut skipped = 0usize;
let mut consulted = 0usize;
for base in BASES {
let base_wide = UnicodeWidthStr::width(base.to_string().as_str()) == 2;
let mut frontier: Vec<(Vec<char>, bool)> = vec![(Vec::new(), base_wide)];
for _ in 0..3 {
let mut next: Vec<(Vec<char>, bool)> = Vec::new();
for (marks, is_wide) in &frontier {
for c in JOINERS {
if !joins_cluster(base, marks, c) {
continue;
}
let mut after = String::new();
after.push(base);
after.extend(marks.iter().copied());
after.push(c);
let w_true = UnicodeWidthStr::width(after.as_str());
let consult = width_may_change(c, marks.is_empty(), *is_wide);
let w_used = if consult {
consulted += 1;
w_true
} else {
skipped += 1;
if *is_wide { 2 } else { 1 }
};
assert_eq!(
action(w_used, *is_wide),
action(w_true, *is_wide),
"the gate changed what the caller does: base U+{:04X} marks {:X?} + U+{:04X} (oracle {w_true}, used {w_used}, wide {is_wide})",
base as u32,
marks.iter().map(|m| *m as u32).collect::<Vec<_>>(),
c as u32,
);
let (promote, demote) = action(w_used, *is_wide);
let mut grown = marks.clone();
grown.push(c);
next.push((grown, (*is_wide || promote) && !demote));
}
}
frontier = next;
}
}
assert!(
skipped > 0 && consulted > 0,
"{skipped} skipped, {consulted} consulted"
);
}
}