///|
pub enum WidthMode {
None
Wrap(Int, String)
Truncate(Int, String)
}
///|
pub enum HeightMode {
None
Increase(Int)
Limit(Int)
}
///|
pub fn WidthMode::none() -> WidthMode {
None
}
///|
pub fn WidthMode::wrap(width : Int) -> WidthMode {
Wrap(width, ".")
}
///|
pub fn WidthMode::wrap_with(width : Int, placeholder : String) -> WidthMode {
Wrap(width, placeholder)
}
///|
pub fn WidthMode::truncate(width : Int, suffix : String) -> WidthMode {
Truncate(width, suffix)
}
///|
pub fn HeightMode::none() -> HeightMode {
None
}
///|
pub fn HeightMode::increase(size : Int) -> HeightMode {
Increase(size)
}
///|
pub fn HeightMode::limit(size : Int) -> HeightMode {
Limit(size)
}
///|
pub fn split_lines(content : String) -> Array[String] {
let lines = content.split("\n").map(StringView::to_string).collect()
if lines.is_empty() {
[""]
} else {
lines
}
}
///|
/// Zero-width characters: combining marks, joiners, variation selectors,
/// bidi controls, etc.
fn is_zero_width_char(ch : Char) -> Bool {
let code = ch.to_int()
code == 0x00AD || // Soft Hyphen
code == 0x200B || // Zero Width Space
code == 0x200C || // Zero Width Non-Joiner
code == 0x200D || // Zero Width Joiner (Emoji ZWJ sequences)
code == 0x2060 || // Word Joiner
code == 0xFEFF || // BOM / Zero Width No-Break Space
(code >= 0x0300 && code <= 0x036F) || // Combining Diacritical Marks
(code >= 0x1AB0 && code <= 0x1AFF) || // Combining Diacritical Marks Extended
(code >= 0x1DC0 && code <= 0x1DFF) || // Combining Diacritical Marks Supplement
(code >= 0x20D0 && code <= 0x20FF) || // Combining Diacritical Marks for Symbols
(code >= 0xFE00 && code <= 0xFE0F) || // Variation Selectors
(code >= 0xFE20 && code <= 0xFE2F) || // Combining Half Marks
(code >= 0x200E && code <= 0x200F) || // LTR/RTL Marks
(code >= 0x202A && code <= 0x202E) || // Bidi control characters
(code >= 0x2066 && code <= 0x2069) || // Bidi Isolate characters
(code >= 0xE0100 && code <= 0xE01EF) // Variation Selectors Supplement
}
///|
/// East Asian Wide characters: CJK, Hangul, fullwidth forms, emoji, etc.
/// Based on Unicode East Asian Width property (UAX #11).
fn is_wide_char(ch : Char) -> Bool {
let code = ch.to_int()
// Hangul Jamo
(code >= 0x1100 && code <= 0x115F) ||
// Miscellaneous Technical (wide emoji)
code == 0x231A ||
code == 0x231B ||
code == 0x2329 ||
code == 0x232A ||
(code >= 0x23E9 && code <= 0x23F3) ||
(code >= 0x23F8 && code <= 0x23FA) ||
// Miscellaneous Symbols (wide emoji)
code == 0x25FD ||
code == 0x25FE ||
code == 0x2614 ||
code == 0x2615 ||
(code >= 0x2648 && code <= 0x2653) ||
code == 0x267F ||
code == 0x2693 ||
code == 0x26A1 ||
code == 0x26AA ||
code == 0x26AB ||
code == 0x26BD ||
code == 0x26BE ||
code == 0x26C4 ||
code == 0x26C5 ||
code == 0x26D4 ||
code == 0x26EA ||
code == 0x26F2 ||
code == 0x26F3 ||
code == 0x26F5 ||
code == 0x26FA ||
code == 0x26FD ||
// Dingbats (wide emoji)
code == 0x2702 ||
code == 0x2705 ||
(code >= 0x2708 && code <= 0x270D) ||
code == 0x270F ||
code == 0x2712 ||
code == 0x2714 ||
code == 0x2716 ||
code == 0x271D ||
code == 0x2721 ||
code == 0x2728 ||
code == 0x2733 ||
code == 0x2734 ||
code == 0x2744 ||
code == 0x2747 ||
code == 0x274C ||
code == 0x274E ||
(code >= 0x2753 && code <= 0x2755) ||
code == 0x2757 ||
code == 0x2763 ||
code == 0x2764 ||
(code >= 0x2795 && code <= 0x2797) ||
code == 0x27A1 ||
code == 0x27B0 ||
code == 0x27BF ||
// Supplemental Arrows-B
code == 0x2934 ||
code == 0x2935 ||
// Misc Symbols and Arrows
(code >= 0x2B05 && code <= 0x2B07) ||
code == 0x2B1B ||
code == 0x2B1C ||
code == 0x2B50 ||
code == 0x2B55 ||
// CJK Radicals through CJK Unified Ideographs Extension A
(code >= 0x2E80 && code <= 0x4DBF) ||
// CJK Unified Ideographs and Yi (skip Yijing Hexagram Symbols 0x4DC0-0x4DFF)
(code >= 0x4E00 && code <= 0xA4CF) ||
// Hangul Syllables
(code >= 0xAC00 && code <= 0xD7A3) ||
// CJK Compatibility Ideographs
(code >= 0xF900 && code <= 0xFAFF) ||
// Vertical Forms and CJK Compatibility Forms
(code >= 0xFE10 && code <= 0xFE19) ||
(code >= 0xFE30 && code <= 0xFE6F) ||
// Fullwidth Forms
(code >= 0xFF00 && code <= 0xFF60) ||
(code >= 0xFFE0 && code <= 0xFFE6) ||
// CJK-related wide symbols
code == 0x3030 ||
code == 0x303D ||
code == 0x3297 ||
code == 0x3299 ||
// Emoji and Supplementary Ideographic Plane
(code >= 0x1F300 && code <= 0x1FAFF) ||
// CJK Unified Ideographs Extension B and beyond
(code >= 0x20000 && code <= 0x3FFFD)
}
///|
fn char_display_width(ch : Char) -> Int {
if is_zero_width_char(ch) {
0
} else if is_wide_char(ch) {
2
} else {
1
}
}
///|
fn string_display_width(line : String) -> Int {
let mut width = 0
for ch in line {
width += char_display_width(ch)
}
width
}
///|
fn take_prefix_by_width(line : String, width : Int) -> String {
if width <= 0 {
return ""
}
let sb = StringBuilder::new()
let mut used = 0
for ch in line {
let ch_width = char_display_width(ch)
if used + ch_width > width {
break
}
sb.write_char(ch)
used += ch_width
if used >= width {
break
}
}
sb.to_string()
}
///|
fn chunk_line(
line : String,
width : Int,
placeholder : String,
) -> Array[String] {
if width <= 0 {
return [""]
}
if string_display_width(line) <= width {
return [line]
}
let parts = []
let mut current = StringBuilder::new()
let mut current_width = 0
for ch in line {
let ch_width = char_display_width(ch)
if current_width > 0 && current_width + ch_width > width {
parts.push(current.to_string())
current = StringBuilder::new()
current_width = 0
}
if current_width == 0 && ch_width > width {
parts.push(take_prefix_by_width(placeholder, width))
continue
}
current.write_char(ch)
current_width += ch_width
}
if current_width > 0 || parts.is_empty() {
parts.push(current.to_string())
}
parts
}
///|
pub fn apply_width(content : String, mode : WidthMode) -> Array[String] {
let lines = split_lines(content)
match mode {
None => lines
Wrap(width, placeholder) => {
let result = []
lines.each(line => {
chunk_line(line, width, placeholder).each(part => result.push(part))
})
if result.is_empty() {
[""]
} else {
result
}
}
Truncate(width, suffix) => {
let result = []
let suffix_width = string_display_width(suffix)
lines.each(line => {
if width <= 0 {
result.push("")
} else if string_display_width(line) <= width {
result.push(line)
} else if suffix_width >= width {
result.push(take_prefix_by_width(suffix, width))
} else {
let keep = width - suffix_width
result.push(take_prefix_by_width(line, keep) + suffix)
}
})
if result.is_empty() {
[""]
} else {
result
}
}
}
}
///|
pub fn apply_height(lines : Array[String], mode : HeightMode) -> Array[String] {
match mode {
None => lines
Increase(size) => {
let result = lines.copy()
while result.length() < size {
result.push("")
}
result
}
Limit(size) =>
if size <= 0 {
[]
} else if lines.length() <= size {
lines
} else {
lines[:size].to_array()
}
}
}
///|
pub fn processed_lines(
content : String,
width_mode : WidthMode,
height_mode : HeightMode,
) -> Array[String] {
apply_height(apply_width(content, width_mode), height_mode)
}