///|
/// State tracking for emoji ZWJ sequences (GB11).
priv enum EmojiState {
ES_None
ES_EP_Seen // Seen Extended_Pictographic
ES_EP_Extend_ZWJ // Seen Extend* ZWJ after EP
} derive(Eq)
///|
/// State tracking for InCB Conjunct (GB9c).
priv enum InCBState {
IC_None
IC_Consonant_Seen // Seen InCB_Consonant
IC_Consonant_Linker_Seen // Seen [Extend|Linker]* Linker after Consonant
} derive(Eq)
///|
/// Determines grapheme cluster boundary from prev and cur pair.
/// true = boundary (÷), false = no boundary (×)
fn check_boundary(
prev : GCBCategory,
cur : GCBCategory,
ri_count : Int,
emoji_state : EmojiState,
incb_state : InCBState,
) -> Bool {
// GB3: CR × LF
if prev == CR && cur == LF {
return false
}
// GB4: (Control|CR|LF) ÷
if prev == Control || prev == CR || prev == LF {
return true
}
// GB5: ÷ (Control|CR|LF)
if cur == Control || cur == CR || cur == LF {
return true
}
// GB6: L × (L|V|LV|LVT)
if prev == L && (cur == L || cur == V || cur == LV || cur == LVT) {
return false
}
// GB7: (LV|V) × (V|T)
if (prev == LV || prev == V) && (cur == V || cur == T) {
return false
}
// GB8: (LVT|T) × T
if (prev == LVT || prev == T) && cur == T {
return false
}
// GB9: × (Extend|ZWJ)
if cur == Extend || cur == ZWJ {
return false
}
// GB9a: × SpacingMark
if cur == SpacingMark {
return false
}
// GB9b: Prepend ×
if prev == Prepend {
return false
}
// GB9c: \p{InCB=Consonant} [\p{InCB=Extend}\p{InCB=Linker}]* \p{InCB=Linker}
// [\p{InCB=Extend}\p{InCB=Linker}]* × \p{InCB=Consonant}
if incb_state == IC_Consonant_Linker_Seen && cur == InCB_Consonant {
return false
}
// GB11: \p{Extended_Pictographic} Extend* ZWJ × \p{Extended_Pictographic}
if emoji_state == ES_EP_Extend_ZWJ && cur == Extended_Pictographic {
return false
}
// GB12/13: sot (RI RI)* RI × RI / [^RI] (RI RI)* RI × RI
if prev == Regional_Indicator &&
cur == Regional_Indicator &&
ri_count % 2 == 1 {
return false
}
// GB999: Any ÷ Any
true
}
///|
/// State transition for emoji ZWJ sequences.
fn next_emoji_state(state : EmojiState, cur_gcb : GCBCategory) -> EmojiState {
if cur_gcb == Extended_Pictographic {
ES_EP_Seen
} else if state == ES_EP_Seen && cur_gcb == Extend {
ES_EP_Seen
} else if state == ES_EP_Seen && cur_gcb == ZWJ {
ES_EP_Extend_ZWJ
} else {
ES_None
}
}
///|
/// State transition for InCB Conjunct.
fn next_incb_state(
state : InCBState,
cur_gcb : GCBCategory,
cp : Int,
) -> InCBState {
if cur_gcb == InCB_Consonant {
IC_Consonant_Seen
} else if state != IC_None && is_incb_linker(cp) {
IC_Consonant_Linker_Seen
} else if state != IC_None && is_incb_extend(cp) {
state
} else {
IC_None
}
}
///|
/// Encapsulates the UAX #29 grapheme segmentation state machine.
/// Both graphemes() and grapheme_iter() share this single implementation
/// to avoid state machine logic duplication.
priv struct SegmenterState {
mut prev_gcb : GCBCategory?
mut ri_count : Int
mut emoji_state : EmojiState
mut incb_state : InCBState
}
///|
fn SegmenterState::new() -> SegmenterState {
{ prev_gcb: None, ri_count: 0, emoji_state: ES_None, incb_state: IC_None }
}
///|
/// Feed a code point to the state machine.
/// Returns true if this code point starts a new grapheme cluster boundary.
fn SegmenterState::next(self : SegmenterState, cp : Int) -> Bool {
let cur_gcb = gcb_category(cp)
let is_boundary = match self.prev_gcb {
None => true
Some(prev) =>
check_boundary(
prev,
cur_gcb,
self.ri_count,
self.emoji_state,
self.incb_state,
)
}
self.ri_count = if cur_gcb == Regional_Indicator {
self.ri_count + 1
} else {
0
}
self.emoji_state = next_emoji_state(self.emoji_state, cur_gcb)
self.incb_state = next_incb_state(self.incb_state, cur_gcb, cp)
self.prev_gcb = Some(cur_gcb)
is_boundary
}