///|
// Port of unicode-width 0.2.2's non-CJK string-width state machine.
// Copyright 2012-2025 The Rust Project Developers. See THIRD_PARTY_NOTICES.md.
const UW_DEFAULT : Int = 0
///|
const UW_LINE_FEED : Int = 1
///|
const UW_EMOJI_MODIFIER : Int = 2
///|
const UW_REGIONAL_INDICATOR : Int = 3
///|
const UW_SEVERAL_REGIONAL : Int = 4
///|
const UW_EMOJI : Int = 5
///|
const UW_ZWJ_EMOJI : Int = 0x1006
///|
const UW_VS16_ZWJ_EMOJI : Int = 0x9006
///|
const UW_KEYCAP_ZWJ : Int = 0x1007
///|
const UW_VS16_KEYCAP_ZWJ : Int = 0x9007
///|
const UW_REGIONAL_ZWJ : Int = 9
///|
const UW_EVEN_REGIONAL_ZWJ : Int = 10
///|
const UW_ODD_REGIONAL_ZWJ : Int = 11
///|
const UW_TAG_END : Int = 16
///|
const UW_TAG_D1 : Int = 17
///|
const UW_TAG_D2 : Int = 18
///|
const UW_TAG_D3 : Int = 19
///|
const UW_TAG_A1 : Int = 25
///|
const UW_TAG_A2 : Int = 26
///|
const UW_TAG_A3 : Int = 27
///|
const UW_TAG_A4 : Int = 28
///|
const UW_TAG_A5 : Int = 29
///|
const UW_TAG_A6 : Int = 30
///|
const UW_KIRAT_E : Int = 32
///|
const UW_KIRAT_AI : Int = 33
///|
const UW_VS123 : Int = 0x0200
///|
const UW_VS15 : Int = 0x4000
///|
const UW_VS16 : Int = 0x8000
///|
const UW_ALEF : Int = 0x30ff
///|
const UW_ZWJ_LAMED : Int = 0x3c00
///|
const UW_ZWJ_BUGINESE_YA : Int = 0x3c01
///|
const UW_BUGINESE_I_ZWJ_YA : Int = 0x3c02
///|
const UW_TIFINAGH : Int = 0x3803
///|
const UW_ZWJ_TIFINAGH : Int = 0x3c03
///|
const UW_TIFINAGH_JOINER : Int = 0x3c04
///|
const UW_LISU_TONE : Int = 0x3c05
///|
const UW_ZWJ_OLD_TURKIC_I : Int = 0x3c06
///|
const UW_KHMER_COENG : Int = 0x3c07
///|
fn unicode_metadata(ch : Char) -> Int {
let cp = ch.to_int()
for lo = 0, hi = unicode_width_ranges.length(); lo < hi; {
let mid = lo + (hi - lo) / 2
let (start, end_, value) = unicode_width_ranges[mid]
if cp < start {
continue lo, mid
}
if cp > end_ {
continue mid + 1, hi
}
return value
}
1
}
///|
/// Return the width of one Unicode scalar. Control characters have width zero.
/// Ambiguous-width characters are narrow, as in tabled's default width policy.
pub fn get_char_width(ch : Char) -> Int {
let cp = ch.to_int()
if cp < 0x20 || (cp >= 0x7f && cp < 0xa0) {
0
} else {
unicode_metadata(ch) & 3
}
}
///|
fn unicode_set_presentation(info : Int, selector : Int) -> Int {
if (info & 0x2000) == 0x2000 ||
(selector == UW_VS16 && (info & 0x9000) == 0x1000) {
info | selector
} else {
selector
}
}
///|
fn unicode_ligature_transparent(cp : Int) -> Bool {
match cp {
0x034f
| 0x17b4..=0x17b5
| 0x180b..=0x180d
| 0x180f
| 0x200d
| 0xfe00..=0xfe0f
| 0xe0100..=0xe01ef => true
_ => false
}
}
///|
fn unicode_width_step(ch : Char, next_info : Int) -> (Int, Int) {
let cp = ch.to_int()
let metadata = unicode_metadata(ch)
let mut next = next_info
if (next & UW_VS16) != 0 {
if (metadata & (1 << 18)) != 0 {
let width = if (next & 0xb000) == 0x9000 { 0 } else { 2 }
return (width, UW_EMOJI)
} else {
next = if (next & 0x2000) == 0x2000 {
next & UW_VS16.lnot()
} else {
UW_DEFAULT
}
}
}
if cp <= 0xa0 {
return match cp {
10 => (1, UW_LINE_FEED)
13 if next == UW_LINE_FEED => (0, UW_DEFAULT)
_ => (1, UW_DEFAULT)
}
}
if next != UW_DEFAULT {
match cp {
0xfe0f => return (0, unicode_set_presentation(next, UW_VS16))
0xfe01 => return (0, unicode_set_presentation(next, UW_VS123))
0xfe0e => return (0, unicode_set_presentation(next, UW_VS15))
_ => ()
}
if (next & UW_VS15) != 0 {
if (metadata & (1 << 19)) != 0 {
return (1, UW_DEFAULT)
}
next = next & UW_VS15.lnot()
} else if (next & UW_VS123) != 0 {
if cp is (0x2018 | 0x2019 | 0x201c | 0x201d) {
return (2, UW_DEFAULT)
}
next = next & UW_VS123.lnot()
}
if (next & 0x0800) != 0 {
if cp == 0x200d {
return (0, next | 0x0400)
}
if unicode_ligature_transparent(cp) {
return (0, next)
}
}
match (next, cp) {
// Arabic Lam-Alef and Hebrew Alef-ZWJ-Lamed.
(UW_ALEF, 0x0644 | 0x06b5..=0x06b8 | 0x076a | 0x08a6 | 0x08c7) =>
return (0, UW_DEFAULT)
(UW_ALEF, _) if (metadata & (1 << 21)) != 0 => return (0, UW_ALEF)
(UW_ZWJ_LAMED, 0x05d0) => return (0, UW_DEFAULT)
// Khmer, Buginese, Tifinagh, Lisu and Old Turkic ligatures.
(UW_KHMER_COENG, 0x17d2) => return (-1, UW_DEFAULT)
(UW_ZWJ_BUGINESE_YA, 0x1a17) => return (0, UW_BUGINESE_I_ZWJ_YA)
(UW_BUGINESE_I_ZWJ_YA, 0x1a15) => return (0, UW_DEFAULT)
(UW_TIFINAGH | UW_ZWJ_TIFINAGH, 0x2d7f) => return (1, UW_TIFINAGH_JOINER)
(UW_ZWJ_TIFINAGH, 0x2d31..=0x2d65 | 0x2d6f) => return (0, UW_DEFAULT)
(UW_TIFINAGH_JOINER, 0x2d31..=0x2d65 | 0x2d6f) => return (-1, UW_DEFAULT)
(UW_LISU_TONE, 0xa4f8..=0xa4fb) => return (0, UW_DEFAULT)
(UW_ZWJ_OLD_TURKIC_I, 0x10c32) => return (0, UW_DEFAULT)
// Emoji modifiers, regional indicators and ZWJ presentation sequences.
(UW_EMOJI_MODIFIER, _) if (metadata & (1 << 20)) != 0 =>
return (0, UW_EMOJI)
(UW_REGIONAL_INDICATOR | UW_SEVERAL_REGIONAL, 0x1f1e6..=0x1f1ff) =>
return (1, UW_SEVERAL_REGIONAL)
(
UW_EMOJI
| UW_SEVERAL_REGIONAL
| UW_EVEN_REGIONAL_ZWJ
| UW_ODD_REGIONAL_ZWJ
| UW_EMOJI_MODIFIER,
0x200d,
) => return (0, UW_ZWJ_EMOJI)
(UW_ZWJ_EMOJI, 0x20e3) => return (0, UW_KEYCAP_ZWJ)
(UW_VS16_ZWJ_EMOJI, _) if (metadata & (1 << 18)) != 0 =>
return (0, UW_EMOJI)
(UW_VS16_KEYCAP_ZWJ, 0x0030..=0x0039 | 0x0023 | 0x002a) =>
return (0, UW_EMOJI)
(UW_ZWJ_EMOJI, 0x1f1e6..=0x1f1ff) => return (1, UW_REGIONAL_ZWJ)
(UW_REGIONAL_ZWJ | UW_ODD_REGIONAL_ZWJ, 0x1f1e6..=0x1f1ff) =>
return (-1, UW_EVEN_REGIONAL_ZWJ)
(UW_EVEN_REGIONAL_ZWJ, 0x1f1e6..=0x1f1ff) =>
return (3, UW_ODD_REGIONAL_ZWJ)
(UW_ZWJ_EMOJI, 0x1f3fb..=0x1f3ff) => return (0, UW_EMOJI_MODIFIER)
// Emoji tag sequences.
(UW_ZWJ_EMOJI, 0xe007f) => return (0, UW_TAG_END)
(UW_TAG_END, 0xe0061..=0xe007a) => return (0, UW_TAG_A1)
(UW_TAG_A1, 0xe0061..=0xe007a) => return (0, UW_TAG_A2)
(UW_TAG_A2, 0xe0061..=0xe007a) => return (0, UW_TAG_A3)
(UW_TAG_A3, 0xe0061..=0xe007a) => return (0, UW_TAG_A4)
(UW_TAG_A4, 0xe0061..=0xe007a) => return (0, UW_TAG_A5)
(UW_TAG_A5, 0xe0061..=0xe007a) => return (0, UW_TAG_A6)
(
UW_TAG_END
| UW_TAG_A1
| UW_TAG_A2
| UW_TAG_A3
| UW_TAG_A4,
0xe0030..=0xe0039,
) => return (0, UW_TAG_D1)
(UW_TAG_D1, 0xe0030..=0xe0039) => return (0, UW_TAG_D2)
(UW_TAG_D2, 0xe0030..=0xe0039) => return (0, UW_TAG_D3)
(UW_TAG_A3 | UW_TAG_A4 | UW_TAG_A5 | UW_TAG_A6 | UW_TAG_D3, 0x1f3f4) =>
return (0, UW_EMOJI)
(UW_ZWJ_EMOJI, _) if ((metadata >> 2) & 0xffff) == UW_EMOJI =>
return (0, UW_EMOJI)
// Kirat Rai vowel combinations.
(UW_KIRAT_E, 0x16d63 | 0x16d69) => return (0, UW_DEFAULT)
(UW_KIRAT_E, 0x16d67) => return (0, UW_KIRAT_AI)
(UW_KIRAT_E, 0x16d68) => return (1, UW_KIRAT_E)
(UW_KIRAT_AI, 0x16d63) => return (0, UW_DEFAULT)
_ => ()
}
}
(metadata & 3, (metadata >> 2) & 0xffff)
}
///|
/// Measure Unicode text using Unicode 17.0.0 width and presentation rules.
/// This measures the string itself; use `get_line_width` to exclude ANSI codes.
///
/// ```mbt check
/// test {
/// assert_eq(@papergrid.get_string_width("👩🔬"), 2)
/// assert_eq(@papergrid.get_char_width('🏳'), 1)
/// assert_eq(@papergrid.get_string_width("🏳️"), 2)
/// }
/// ```
pub fn get_string_width(text : String) -> Int {
let mut width = 0
let mut state = UW_DEFAULT
for ch in text.rev_iter() {
let (add, next) = unicode_width_step(ch, state)
width += add
state = next
}
width
}