///|
// Unlike the standard char_length, also accepts unpaired UTF-16 surrogates.
fn ecma_code_point_at(value : String, index : Int32) -> (Int32, Int32) {
let first = value[index].to_int()
if first >= 0xd800 && first <= 0xdbff && index + 1 < value.length() {
let second = value[index + 1].to_int()
if second >= 0xdc00 && second <= 0xdfff {
return (0x10000 + (first - 0xd800) * 0x400 + second - 0xdc00, 2)
}
}
(first, 1)
}
///|
fn ecma_code_point_length(value : String) -> Int32 {
let mut index = 0
let mut count = 0
while index < value.length() {
let (_, width) = ecma_code_point_at(value, index)
index = index + width
count = count + 1
}
count
}
///|
fn unicode_property_contains(
point : Int32,
ranges : Array[(Int32, Int32)],
) -> Bool {
let mut low = 0
let mut high = ranges.length()
while low < high {
let mid = low + (high - low) / 2
let (first, last) = ranges[mid]
if point < first {
high = mid
} else if point > last {
low = mid + 1
} else {
return true
}
}
false
}
///|
fn unicode_simple_case(
point : Int32,
ranges : Array[(Int32, Int32, Int32, Int32)],
) -> Int32 {
let mut low = 0
let mut high = ranges.length()
while low < high {
let mid = low + (high - low) / 2
let (first, last, stride, delta) = ranges[mid]
if point < first {
high = mid
} else if point > last {
low = mid + 1
} else {
return if (point - first) % stride == 0 { point + delta } else { point }
}
}
point
}
///|
fn unicode_expanded_case(
point : Int32,
expansions : Array[(Int32, String)],
) -> String? {
let mut low = 0
let mut high = expansions.length()
while low < high {
let mid = low + (high - low) / 2
let (key, mapped) = expansions[mid]
if point < key {
high = mid
} else if point > key {
low = mid + 1
} else {
return Some(mapped)
}
}
None
}
///|
// Unicode Final_Sigma ignores Case_Ignorable before testing Cased, including
// code points with both properties. Context is read from the original input.
fn unicode_is_final_sigma(value : String, index : Int32) -> Bool {
let mut before = index
let mut preceded_by_cased = false
while before > 0 {
let mut start = before - 1
let last = value[start].to_int()
if last >= 0xdc00 && last <= 0xdfff && start > 0 {
let previous = value[start - 1].to_int()
if previous >= 0xd800 && previous <= 0xdbff {
start = start - 1
}
}
let (point, _) = ecma_code_point_at(value, start)
before = start
if unicode_property_contains(point, unicode_ignorable_ranges) {
continue
}
preceded_by_cased = unicode_property_contains(point, unicode_cased_ranges)
break
}
if !preceded_by_cased {
return false
}
let mut after = index + 1
while after < value.length() {
let (point, width) = ecma_code_point_at(value, after)
after = after + width
if unicode_property_contains(point, unicode_ignorable_ranges) {
continue
}
return !unicode_property_contains(point, unicode_cased_ranges)
}
true
}
///|
fn ecma_case_convert(value : String, upper : Bool) -> String {
let ranges = if upper { unicode_upper_ranges } else { unicode_lower_ranges }
let expansions = if upper {
unicode_upper_expansions
} else {
unicode_lower_expansions
}
let result = StringBuilder(size_hint=value.length())
let mut index = 0
while index < value.length() {
let (point, width) = ecma_code_point_at(value, index)
if !upper && point == 0x03a3 && unicode_is_final_sigma(value, index) {
result.write_char('ς')
} else if unicode_expanded_case(point, expansions) is Some(mapped) {
result.write_string(mapped)
} else {
let mapped = unicode_simple_case(point, ranges)
if mapped == point {
// Copy code units so lone surrogates are never coerced into Char.
result.write_view(value[index:index + width])
} else {
// Generated mappings contain only Unicode scalar values.
result.write_char(mapped.unsafe_to_char())
}
}
index = index + width
}
result.to_string()
}
///|
fn ecma_is_whitespace(unit : UInt16) -> Bool {
match unit.to_int() {
0x0009
| 0x000a
| 0x000b
| 0x000c
| 0x000d
| 0x0020
| 0x00a0
| 0x1680
| 0x2000..<=0x200a
| 0x2028
| 0x2029
| 0x202f
| 0x205f
| 0x3000
| 0xfeff => true
_ => false
}
}
///|
fn ecma_trim(value : String) -> String {
let mut start = 0
let mut end = value.length()
while start < end && ecma_is_whitespace(value[start]) {
start = start + 1
}
while end > start && ecma_is_whitespace(value[end - 1]) {
end = end - 1
}
// Both offsets are within bounds; avoid StringView's surrogate normalization.
value.unsafe_substring(start~, end~)
}