///| Faithful ports of a few `base/common/strings.ts` helpers from the
/// checked-in vscode source. Kept here, the
/// lowest-level shared package, so model/view code shares one definition.
///
/// These are UTF-16 code-unit algorithms, which is what JavaScript strings are
/// and what Monaco's offsets count. String indexing and `String::code_units`
/// both yield `UInt16` code units, so the scanners below stay in that type
/// instead of widening every read to `Int`. Only values that leave the
/// code-unit domain — a comparator result, a public `Int` predicate argument —
/// convert.
///|
/// The first code unit outside ASCII, above which case folding needs the full
/// lowercase table rather than the `±32` letter shift.
let code_ascii_limit : UInt16 = 128
///|
/// `0xD800 <= charCode <= 0xDBFF`. Mirrors `strings.isHighSurrogate`; the std
/// `Int::is_leading_surrogate` is the identical range check.
pub fn is_high_surrogate(char_code : Int32) -> Bool {
char_code.is_leading_surrogate()
}
///|
/// `0xDC00 <= charCode <= 0xDFFF`. Mirrors `strings.isLowSurrogate`; the std
/// `Int::is_trailing_surrogate` is the identical range check.
pub fn is_low_surrogate(char_code : Int32) -> Bool {
char_code.is_trailing_surrogate()
}
///|
/// Whether `text` contains any right-to-left character. Mirrors
/// `strings.containsRTL` over the common BMP RTL blocks (Hebrew/Arabic
/// `U+0590..U+08FF`, Arabic Presentation Forms-A `U+FB1D..U+FDFF` and Forms-B
/// `U+FE70..U+FEFF`); supplementary-plane RTL is not detected, matching the
/// viewer's existing simplified port.
pub fn contains_rtl(text : String) -> Bool {
for code in text.code_units() {
if (code >= 0x0590 && code <= 0x08ff) ||
(code >= 0xfb1d && code <= 0xfdff) ||
(code >= 0xfe70 && code <= 0xfeff) {
return true
}
}
false
}
///|
/// Index of the first non-whitespace (non-space, non-tab) character, or `-1`.
/// Mirrors `strings.firstNonWhitespaceIndex`.
pub fn first_non_whitespace_index(text : String) -> Int32 {
for i in 0.. Int32 {
for i in text.length()>..0 {
let code = text[i]
if code != ' ' && code != '\t' {
return i
}
}
-1
}
///|
/// Whether `char_code` is a full-width character (CJK and related blocks).
/// Mirrors `strings.isFullWidthCharacter`.
pub fn is_full_width_character(char_code : Int32) -> Bool {
(char_code >= 0x2E80 && char_code <= 0xD7AF) ||
(char_code >= 0xF900 && char_code <= 0xFAFF) ||
(char_code >= 0xFF01 && char_code <= 0xFF5E) ||
(char_code >= 0xFFE0 && char_code <= 0xFFE6)
}
///|
/// Maps `n` to a single letter (`a..z` then `A..Z`, wrapping). Mirrors
/// `strings.singleLetterHash`; Monaco uses it to prefix per-instance id
/// namespaces (model decoration ids, whitespace ids).
pub fn single_letter_hash(n : Int32) -> String {
let letters_cnt = 'Z'.to_int() - 'A'.to_int() + 1
let n = n % (2 * letters_cnt)
if n < letters_cnt {
Int32::unsafe_to_char('a'.to_int() + n).to_string()
} else {
Int32::unsafe_to_char('A'.to_int() + n - letters_cnt).to_string()
}
}
///|
/// Mirrors `strings.compare` (strings.ts:343): JS `<`/`>` on strings is
/// charwise UTF-16 code-unit lexicographic. Required because MoonBit's
/// built-in `String` compare is length-first (`"z" > "aa"` here, not there).
pub fn compare(a : String, b : String) -> Int32 {
let a_len = a.length()
let b_len = b.length()
for i in 0.. b_len {
1
} else {
0
}
}
///|
/// Mirrors `strings.compareSubstring` (strings.ts:353).
fn compare_substring_impl(
a : String,
b : String,
a_start~ : Int32 = 0,
a_end~ : Int32 = -1,
b_start~ : Int32 = 0,
b_end~ : Int32 = -1,
) -> Int32 {
let a_end = if a_end < 0 { a.length() } else { a_end }
let b_end = if b_end < 0 { b.length() } else { b_end }
let a_len = a_end - a_start
let b_len = b_end - b_start
for offset in 0.. b_len {
1
} else {
0
}
}
///|
/// Simple lowercase mapping for one UTF-16 code unit — the stand-in for JS
/// `String.prototype.toLowerCase` (MoonBit's `String::to_lower` is
/// ASCII-only). Covers ASCII, Latin-1, Latin Extended-A, Greek, Cyrillic and
/// fullwidth Latin; other scripts and the rare length-changing mappings
/// (e.g. `İ`) pass through unchanged — a documented deviation from the full
/// Unicode table JS applies.
fn to_lower_code_unit(code : UInt16) -> UInt16 {
if is_upper_ascii_letter_code_unit(code) {
return code + 32
}
// Latin-1 Supplement: À-Þ except ×
if (code >= 0x00C0 && code <= 0x00D6) || (code >= 0x00D8 && code <= 0x00DE) {
return code + 32
}
// Latin Extended-A: alternating upper/lower pairs
if (code >= 0x0100 && code <= 0x0137) || (code >= 0x014A && code <= 0x0177) {
return if code % 2 == 0 { code + 1 } else { code }
}
if (code >= 0x0139 && code <= 0x0148) || (code >= 0x0179 && code <= 0x017E) {
return if code % 2 == 1 { code + 1 } else { code }
}
if code == 0x0178 {
return 0x00FF // Ÿ → ÿ
}
// Greek
if (code >= 0x0391 && code <= 0x03A1) || (code >= 0x03A3 && code <= 0x03AB) {
return code + 32
}
// Cyrillic
if code >= 0x0410 && code <= 0x042F {
return code + 32
}
if code >= 0x0400 && code <= 0x040F {
return code + 80
}
// Fullwidth Latin
if code >= 0xFF21 && code <= 0xFF3A {
return code + 32
}
code
}
///|
/// Per-code-unit `to_lower_code_unit` over a string — the JS
/// `toLowerCase()` call sites of the URI stack route through this so
/// non-ASCII authorities/paths fold the way Monaco's do.
fn to_lower_js(s : String) -> String {
let len = s.length()
let changed = for i in 0.. Array[String] {
let lines = []
let len = text.length()
let line_start = for i = 0, line_start = 0; i < len; {
if text[i] == '\r' {
lines.push(text[line_start:i].to_owned())
if i + 1 < len && text[i + 1] == '\n' {
// Consume the `\n` of a `\r\n` pair.
continue i + 2, i + 2
}
continue i + 1, i + 1
} else if text[i] == '\n' {
lines.push(text[line_start:i].to_owned())
continue i + 1, i + 1
}
continue i + 1, line_start
} nobreak {
line_start
}
lines.push(text[line_start:].to_owned())
lines
}
///|
fn is_lower_ascii_letter_code_unit(code : UInt16) -> Bool {
code is ('a'..<='z')
}
///|
fn is_upper_ascii_letter_code_unit(code : UInt16) -> Bool {
code is ('A'..<='Z')
}
///|
/// Mirrors `strings.compareSubstringIgnoreCase` (strings.ts:377): per-char
/// ASCII case-fold compare, falling back to lower-cased `compareSubstring`
/// when a non-ASCII unit is hit.
fn compare_substring_ignore_case_impl(
a : String,
b : String,
a_start~ : Int32 = 0,
a_end~ : Int32 = -1,
b_start~ : Int32 = 0,
b_end~ : Int32 = -1,
) -> Int32 {
let a_end = if a_end < 0 { a.length() } else { a_end }
let b_end = if b_end < 0 { b.length() } else { b_end }
let a_len = a_end - a_start
let b_len = b_end - b_start
for offset in 0..= code_ascii_limit || raw_code_b >= code_ascii_limit {
// not ASCII letters -> fallback to lower-casing strings
return compare_substring_impl(
to_lower_js(a),
to_lower_js(b),
a_start=a_index,
a_end~,
b_start=b_index,
b_end~,
)
}
// map lower-case ascii letter onto upper-case variants
// [97-122] (lower ascii) --> [65-90] (upper ascii)
let code_a = if is_lower_ascii_letter_code_unit(raw_code_a) {
raw_code_a - 32
} else {
raw_code_a
}
let code_b = if is_lower_ascii_letter_code_unit(raw_code_b) {
raw_code_b - 32
} else {
raw_code_b
}
// Compare both code points. The difference must be taken in `Int`: a
// `UInt16` subtraction wraps, so `'A' - 'B'` would be 65535, not -1.
let diff = code_a.to_int() - code_b.to_int()
if diff == 0 {
continue
}
return diff
}
if a_len < b_len {
-1
} else if a_len > b_len {
1
} else {
0
}
}
///|
/// Mirrors `strings.equalsIgnoreCase` (strings.ts:436): length gate +
/// per-char case-fold compare.
fn equals_ignore_case_impl(a : String, b : String) -> Bool {
a.length() == b.length() && compare_substring_ignore_case_impl(a, b) == 0
}
///|
/// Mirrors `strings.startsWithIgnoreCase` (strings.ts:444): length gate +
/// prefix case-fold compare.
fn starts_with_ignore_case_impl(str : String, candidate : String) -> Bool {
let len = candidate.length()
len <= str.length() &&
compare_substring_ignore_case_impl(str, candidate, a_start=0, a_end=len) == 0
}
///|
/// HTML-escapes `&`, `<`, `>`. Mirrors `strings.escape`. It deliberately leaves
/// `"` alone, matching Monaco's `_tokenizeToString`; use `escape_html` for text
/// that can also land in an attribute value.
pub fn escape(text : String) -> String {
let sb = StringBuilder()
for ch in text {
match ch {
'&' => sb.write_string("&")
'<' => sb.write_string("<")
'>' => sb.write_string(">")
_ => sb.write_char(ch)
}
}
sb.to_string()
}
///|
/// HTML-escapes text for element content **or** attribute values: `escape`'s
/// three characters plus `"`. Callers that build markup by concatenation and
/// may quote the result need this variant.
pub fn escape_html(text : String) -> String {
let sb = StringBuilder()
for ch in text {
match ch {
'&' => sb.write_string("&")
'<' => sb.write_string("<")
'>' => sb.write_string(">")
'"' => sb.write_string(""")
_ => sb.write_char(ch)
}
}
sb.to_string()
}