// Copyright (c) 2026 Christian Hansen
//
//
// Copyright (c) 2026 fundon
//
// MoonBit translation — Unicode Emoji Segmentation (UTS #51 v17.0.0)
//
// Licensed under the MIT License.
// ── Sequence types ──────────────────────────────────────────────────────
///|
pub(all) enum EmojiSequenceType {
BASIC
KEYCAP
FLAG
TAG
MODIFIER
ZWJ
} derive(Eq, Debug)
///|
pub(all) enum EmojiPresentationStyle {
DEFAULT
TEXT
EMOJI
} derive(Eq, Debug)
// Helper: check if style is DEFAULT
///|
fn style_is_default(style : EmojiPresentationStyle) -> Bool {
match style {
EmojiPresentationStyle::DEFAULT => true
_ => false
}
}
// ── UCD predicates ──────────────────────────────────────────────────────
///|
pub fn is_keycap_base(cp : Int) -> Bool {
cp == 0x0023 || cp == 0x002A || (cp >= 0x0030 && cp <= 0x0039)
}
///|
pub fn is_enclosing_keycap(cp : Int) -> Bool {
cp == 0x20E3
}
///|
pub fn is_tag_base(cp : Int) -> Bool {
cp == 0x1F3F4
}
///|
pub fn is_tag_spec(cp : Int) -> Bool {
cp >= 0xE0020 && cp <= 0xE007E
}
///|
pub fn is_tag_term(cp : Int) -> Bool {
cp == 0xE007F
}
///|
pub fn is_text_presentation_selector(cp : Int) -> Bool {
cp == 0xFE0E
}
///|
pub fn is_emoji_presentation_selector(cp : Int) -> Bool {
cp == 0xFE0F
}
///|
pub fn is_regional_indicator(cp : Int) -> Bool {
cp >= 0x1F1E6 && cp <= 0x1F1FF
}
///|
pub fn is_modifier(cp : Int) -> Bool {
cp >= 0x1F3FB && cp <= 0x1F3FF
}
// ── Trie lookup helper ──────────────────────────────────────────────────
///|
///|
pub fn dfa_is_accepting(state : State) -> Bool {
state >= State::TERMINAL && state <= State::RI
}
///|
pub fn dfa_is_boundary(state : State) -> Bool {
state == State::REJECT
}
///|
pub fn dfa_is_start(state : State) -> Bool {
state == State::START
}
///|
pub fn dfa_step(state : State, klass : Class) -> State {
int_to_state(dfa_table[state_to_int(state)][class_to_int(klass)])
}
///|
/// Step the DFA and record class/state bits into the bitmask accumulator.
pub fn dfa_step_record(
state : State,
klass : Class,
bitmask : Int,
) -> (State, Int) {
let next = dfa_table[state_to_int(state)][class_to_int(klass)]
let ns = int_to_state(next)
if ns != State::START && ns != State::REJECT {
(
ns,
bitmask | (1 << class_to_int(klass)) | (1 << (next + RECORD_STATE_SHIFT)),
)
} else {
(ns, bitmask)
}
}
// ── UCD classify ────────────────────────────────────────────────────────
///|
pub fn ucd_classify(cp : Int) -> Class {
if cp <= 0x200D {
if cp == 0x200D {
return Class::ZWJ
}
if cp == 0xA9 || cp == 0xAE {
return Class::EMOJI
}
if is_keycap_base(cp) {
return Class::KEYCAP_BASE
}
return Class::OTHER
}
if is_text_presentation_selector(cp) {
return Class::VS15
}
if is_emoji_presentation_selector(cp) {
return Class::VS16
}
if is_enclosing_keycap(cp) {
return Class::KEYCAP_TERM
}
if is_tag_base(cp) {
return Class::TAG_BASE
}
if is_tag_spec(cp) {
return Class::TAG_SPEC
}
if is_tag_term(cp) {
return Class::TAG_TERM
}
if is_regional_indicator(cp) {
return Class::RI
}
if is_modifier(cp) {
return Class::MODIFIER
}
if is_modifier_base(cp) {
return Class::MODIFIER_BASE
}
if is_emoji(cp) {
return Class::EMOJI
}
Class::OTHER
}
// ── DFA classify from recorded bitmask ──────────────────────────────────
///|
pub fn classify_type(bitmask : Int) -> EmojiSequenceType {
if (bitmask & (1 << class_to_int(Class::ZWJ))) != 0 {
return EmojiSequenceType::ZWJ
}
if (bitmask & (1 << (state_to_int(State::TAG_SPEC) + RECORD_STATE_SHIFT))) !=
0 &&
(bitmask & (1 << (state_to_int(State::TERMINAL) + RECORD_STATE_SHIFT))) != 0 {
return EmojiSequenceType::TAG
}
if (bitmask & (1 << (state_to_int(State::RI) + RECORD_STATE_SHIFT))) != 0 &&
(bitmask & (1 << (state_to_int(State::TERMINAL) + RECORD_STATE_SHIFT))) != 0 {
return EmojiSequenceType::FLAG
}
if (bitmask & (1 << class_to_int(Class::MODIFIER_BASE))) != 0 &&
(bitmask & (1 << class_to_int(Class::MODIFIER))) != 0 {
return EmojiSequenceType::MODIFIER
}
if (bitmask & (1 << class_to_int(Class::KEYCAP_TERM))) != 0 {
return EmojiSequenceType::KEYCAP
}
EmojiSequenceType::BASIC
}
///|
pub fn classify_style(bitmask : Int) -> EmojiPresentationStyle {
if (bitmask & (1 << class_to_int(Class::VS15))) != 0 {
return EmojiPresentationStyle::TEXT
}
if (bitmask & (1 << class_to_int(Class::VS16))) != 0 {
return EmojiPresentationStyle::EMOJI
}
EmojiPresentationStyle::DEFAULT
}
// ── Presentation resolution ─────────────────────────────────────────────
///|
pub fn resolve_default(
type_ : EmojiSequenceType,
leading_cp : Int,
) -> EmojiPresentationStyle {
match type_ {
EmojiSequenceType::BASIC =>
if is_presentation(leading_cp) {
EmojiPresentationStyle::EMOJI
} else {
EmojiPresentationStyle::TEXT
}
EmojiSequenceType::KEYCAP => EmojiPresentationStyle::TEXT
EmojiSequenceType::FLAG
| EmojiSequenceType::TAG
| EmojiSequenceType::MODIFIER
| EmojiSequenceType::ZWJ => EmojiPresentationStyle::EMOJI
}
}
///|
pub fn resolve(
type_ : EmojiSequenceType,
style : EmojiPresentationStyle,
leading_cp : Int,
) -> EmojiPresentationStyle {
if !style_is_default(style) {
return style
}
resolve_default(type_, leading_cp)
}
// ── Scanner ─────────────────────────────────────────────────────────────
///|
pub struct ScanRange {
start : Int
end : Int
}
///|
pub fn scan_strict(codepoints : Array[Int]) -> Array[ScanRange] {
let len = codepoints.length()
let out : Array[ScanRange] = Array::new(capacity=len)
let mut state = State::START
let mut start = 0
for i = 0; i < len; i = i + 1 {
let klass = ucd_classify(codepoints[i])
let next = dfa_step(state, klass)
if dfa_is_boundary(next) {
if dfa_is_accepting(state) {
out.push({ start, end: i - 1 })
}
let next2 = dfa_step(State::START, klass)
state = next2
start = i
} else {
state = next
}
if dfa_is_start(state) {
start = i + 1
}
}
if start < len && dfa_is_accepting(state) {
out.push({ start, end: len - 1 })
}
out
}
// ── Inline tests ────────────────────────────────────────────────────────
///|
pub fn scan_greedy(codepoints : Array[Int]) -> Array[ScanRange] {
let len = codepoints.length()
let out : Array[ScanRange] = Array::new(capacity=len)
let mut state = State::START
let mut start = 0
let mut end = 0
let mut has_accept = false
for i = 0; i < len; i = i + 1 {
let klass = ucd_classify(codepoints[i])
let next = dfa_step(state, klass)
if dfa_is_boundary(next) {
if has_accept {
out.push({ start, end })
has_accept = false
}
let next2 = dfa_step(State::START, klass)
state = next2
start = i
} else {
state = next
}
if dfa_is_start(state) {
start = i + 1
}
if dfa_is_accepting(state) {
has_accept = true
end = i
}
}
if has_accept {
out.push({ start, end })
}
out
}