// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Unicode normalization helpers (compose/decompose).
///
/// Ported from `swash/src/text/compose.rs` (swash is dual-licensed Apache-2.0 OR MIT).
///|
pub type Decompose = Iter[Char]
///|
fn decompose_single(c : Char) -> Iter[Char] {
let mut done = false
Iter::new(fn() {
if done {
None
} else {
done = true
Some(c)
}
})
}
///|
const LBASE : Int = 0x1100
///|
const VBASE : Int = 0x1161
///|
const TBASE : Int = 0x11A7
///|
const LCOUNT : Int = 19
///|
const VCOUNT : Int = 21
///|
const TCOUNT : Int = 28
///|
const SBASE : Int = 0xAC00
///|
const NCOUNT : Int = VCOUNT * TCOUNT
///|
const SCOUNT : Int = LCOUNT * NCOUNT
///|
fn is_hangul_syllable(c : Int) -> Bool {
c >= SBASE && c < SBASE + SCOUNT
}
///|
fn compose_hangul(a : Int, b : Int) -> Int? {
if b < VBASE || b >= TBASE + TCOUNT {
return None
}
if !((a >= LBASE && a < LBASE + LCOUNT) || (a >= SBASE && a < SBASE + SCOUNT)) {
return None
}
if a >= SBASE {
if (a - SBASE) % TCOUNT == 0 {
Some(a + (b - TBASE))
} else {
None
}
} else {
let li = a - LBASE
let vi = b - VBASE
Some(SBASE + li * NCOUNT + vi * TCOUNT)
}
}
///|
fn pair_index(c : Int, table : ReadOnlyArray[(UInt, UInt, UInt)]) -> Int? {
for entry in table.iter() {
let (start_u, len_u, base_u) = entry
let start = start_u.reinterpret_as_int()
if start == 0 || c < start {
return None
}
let end = start + len_u.reinterpret_as_int()
if c <= end {
return Some(base_u.reinterpret_as_int() + (c - start))
}
}
None
}
///|
pub fn compose_pair(a : Char, b : Char) -> Char? {
let a = a.to_int()
let b = b.to_int()
match compose_hangul(a, b) {
Some(cp) => Some(cp.unsafe_to_char())
None => {
let l = match pair_index(a, compose0) {
None => return None
Some(v) => v
}
let r = match pair_index(b, compose1) {
None => return None
Some(v) => v
}
let c = compose_index(l * COMPOSE1_COUNT + r)
if c != 0 {
Some(c.unsafe_to_char())
} else {
None
}
}
}
}
///|
fn decompose_hangul(c : Int) -> Iter[Char] {
let si = c - SBASE
let li = si / NCOUNT
let vi = si % NCOUNT / TCOUNT
let ti = si % TCOUNT
let l = (LBASE + li).unsafe_to_char()
let v = (VBASE + vi).unsafe_to_char()
let t = (TBASE + ti).unsafe_to_char()
let mut cur = 0
let len = if ti > 0 { 3 } else { 2 }
Iter::new(fn() {
if cur >= len {
return None
}
let i = cur
cur = cur + 1
if i == 0 {
Some(l)
} else if i == 1 {
Some(v)
} else {
Some(t)
}
})
}
///|
pub fn decompose(c : Char) -> Iter[Char] {
let cp = c.to_int()
if cp <= 0x7F {
decompose_single(c)
} else if is_hangul_syllable(cp) {
decompose_hangul(cp)
} else {
let index = decompose_index(cp)
if index == 0 {
decompose_single(c)
} else {
let len = decompose_data[index].reinterpret_as_int()
let start = index + 1
let mut cur = 0
Iter::new(fn() {
if cur >= len {
return None
}
let c = decompose_data[start + cur]
.reinterpret_as_int()
.unsafe_to_char()
cur = cur + 1
Some(c)
})
}
}
}
///|
pub fn decompose_compat(c : Char) -> Iter[Char] {
let cp = c.to_int()
if cp <= 0x7F {
decompose_single(c)
} else if is_hangul_syllable(cp) {
decompose_hangul(cp)
} else {
let index = decompose_compat_index(cp)
if index == 0 {
decompose_single(c)
} else if index == 1 {
decompose(c)
} else {
let len = decompose_compat_data[index].reinterpret_as_int()
let start = index + 1
let mut cur = 0
Iter::new(fn() {
if cur >= len {
return None
}
let c = decompose_compat_data[start + cur]
.reinterpret_as_int()
.unsafe_to_char()
cur = cur + 1
Some(c)
})
}
}
}