// 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.
///|
/// Language tags (minimal subset of swash `text` module).
///
/// Ported from `swash/src/text/lang.rs` + `lang_data.rs`.
///|
/// Chinese, Japanese and Korean languages.
pub(all) enum Cjk {
None
Traditional
Simplified
Japanese
Korean
}
///|
/// Representation of a language and its associated script and region.
pub struct Language {
language : Array[Byte]
script : Array[Byte]
region : Array[Byte]
lang_len : Int
script_len : Int
region_len : Int
cjk : Cjk
name_index : Int
tag : Tag?
}
///|
fn text_lang_ascii_lower(u : Int) -> Byte? {
if u < 0 || u > 255 {
return None
}
let b = u.reinterpret_as_uint()
let lo = if b >= 65 && b <= 90 { b + 32 } else { b }
Some(lo.reinterpret_as_int().to_byte())
}
///|
fn text_lang_ascii_upper(u : Int) -> Byte? {
if u < 0 || u > 255 {
return None
}
let b = u.reinterpret_as_uint()
let up = if b >= 97 && b <= 122 { b - 32 } else { b }
Some(up.reinterpret_as_int().to_byte())
}
///|
fn text_lang_tag2(a : Byte, b : Byte) -> UInt {
(a.to_int().reinterpret_as_uint() << 8) | b.to_int().reinterpret_as_uint()
}
///|
fn text_lang_tag3(a : Byte, b : Byte, c : Byte) -> UInt {
(a.to_int().reinterpret_as_uint() << 16) |
(b.to_int().reinterpret_as_uint() << 8) |
c.to_int().reinterpret_as_uint()
}
///|
fn text_lang_find_dash(s : String, start : Int) -> Int {
let n = s.length()
let mut i = start
while i < n {
if s.code_unit_at(i) == '-' {
return i
}
i = i + 1
}
n
}
///|
fn text_lang_lookup_tag(tag : Tag) -> Int? {
let mut l = 0
let mut h = text_lang_tags.length()
while l < h {
let mid = (l + h) / 2
let k = text_lang_tags[mid]
if k < tag {
l = mid + 1
} else if k > tag {
h = mid
} else {
return Some(mid)
}
}
None
}
///|
fn text_lang_lookup_by_tag2(key : UInt) -> Int? {
let mut l = 0
let mut h = text_lang_by_tag2.length()
while l < h {
let mid = (l + h) / 2
let (k, v) = text_lang_by_tag2[mid]
if k < key {
l = mid + 1
} else if k > key {
h = mid
} else {
return Some(v)
}
}
None
}
///|
fn text_lang_lookup_by_tag3(key : UInt) -> Int? {
let mut l = 0
let mut h = text_lang_by_tag3.length()
while l < h {
let mid = (l + h) / 2
let (k, v) = text_lang_by_tag3[mid]
if k < key {
l = mid + 1
} else if k > key {
h = mid
} else {
return Some(v)
}
}
None
}
///|
fn text_lang_bytes_prefix_to_string(bytes : Array[Byte], len : Int) -> String {
let len = if len < 0 {
0
} else if len > bytes.length() {
bytes.length()
} else {
len
}
let chars = Array::makei(len, i => bytes[i].to_int().unsafe_to_char())
String::from_array(chars.op_as_view())
}
///|
/// Parses a language tag.
pub fn Language::parse(tag : String) -> Language? {
let language = [b'\x00', b'\x00', b'\x00']
let region = [b'\x00', b'\x00']
let script = [b'\x00', b'\x00', b'\x00', b'\x00']
let mut lang_len = 0
let mut region_len = 0
let mut script_len = 0
let mut cjk = Cjk::None
let mut name_index = -1
let mut tag_opt : Tag? = None
let n = tag.length()
let p0_end = text_lang_find_dash(tag, 0)
let p1_start = if p0_end < n { p0_end + 1 } else { n }
let p1_end = if p1_start < n { text_lang_find_dash(tag, p1_start) } else { n }
let p2_start = if p1_end < n { p1_end + 1 } else { n }
let p2_end = if p2_start < n { text_lang_find_dash(tag, p2_start) } else { n }
// Part 0: language (2 or 3).
let mut has_region = false
let mut zh = false
let mut lang_index = -1
let p0_len = p0_end
if p0_len == 2 {
let a_opt = text_lang_ascii_lower(tag.code_unit_at(0).to_int())
let b_opt = text_lang_ascii_lower(tag.code_unit_at(1).to_int())
let (a, b) = match (a_opt, b_opt) {
(Some(a), Some(b)) => (a, b)
_ => return None
}
if a == b'z' && b == b'h' {
zh = true
} else if a == b'j' && b == b'a' {
cjk = Cjk::Japanese
} else if a == b'k' && b == b'o' {
cjk = Cjk::Korean
}
language[0] = a
language[1] = b
lang_len = 2
let key = text_lang_tag2(a, b)
match text_lang_lookup_by_tag2(key) {
None => ()
Some(ix) => lang_index = ix
}
} else if p0_len == 3 {
let (a, b, c) = match
(
text_lang_ascii_lower(tag.code_unit_at(0).to_int()),
text_lang_ascii_lower(tag.code_unit_at(1).to_int()),
text_lang_ascii_lower(tag.code_unit_at(2).to_int()),
) {
(Some(a), Some(b), Some(c)) => (a, b, c)
_ => return None
}
zh = a == b'z' && b == b'h' && c == b'o'
language[0] = a
language[1] = b
language[2] = c
lang_len = 3
let key = text_lang_tag3(a, b, c)
match text_lang_lookup_by_tag3(key) {
None => ()
Some(ix) => lang_index = ix
}
} else {
return None
}
// Part 1: region(2) or script(4).
let p1_len = p1_end - p1_start
if p1_start < n {
if p1_len == 2 {
let (a, b) = match
(
text_lang_ascii_upper(tag.code_unit_at(p1_start).to_int()),
text_lang_ascii_upper(tag.code_unit_at(p1_start + 1).to_int()),
) {
(Some(a), Some(b)) => (a, b)
_ => return None
}
region[0] = a
region[1] = b
region_len = 2
has_region = true
} else if p1_len == 4 {
let (a0, a1, a2, a3) = match
(
text_lang_ascii_upper(tag.code_unit_at(p1_start).to_int()),
text_lang_ascii_lower(tag.code_unit_at(p1_start + 1).to_int()),
text_lang_ascii_lower(tag.code_unit_at(p1_start + 2).to_int()),
text_lang_ascii_lower(tag.code_unit_at(p1_start + 3).to_int()),
) {
(Some(a0), Some(a1), Some(a2), Some(a3)) => (a0, a1, a2, a3)
_ => return None
}
script[0] = a0
script[1] = a1
script[2] = a2
script[3] = a3
script_len = 4
} else {
// Ignore other subtags.
()
}
}
// Part 2: region(2) if part 1 was script.
let p2_len = p2_end - p2_start
if p2_start < n && !has_region && p2_len == 2 {
let (a, b) = match
(
text_lang_ascii_upper(tag.code_unit_at(p2_start).to_int()),
text_lang_ascii_upper(tag.code_unit_at(p2_start + 1).to_int()),
) {
(Some(a), Some(b)) => (a, b)
_ => return None
}
region[0] = a
region[1] = b
region_len = 2
has_region = true
}
// Resolve OpenType tag / name index.
if lang_index >= 0 && lang_index < text_lang_tags.length() {
name_index = lang_index
tag_opt = Some(text_lang_tags[lang_index])
} else if zh {
let sc_opt = if script_len == 4 {
Some(text_lang_bytes_prefix_to_string(script, 4))
} else {
None
}
let (tag, zh_cjk) = match sc_opt {
Some(sc) =>
if sc == "Hans" {
(TEXT_LANG_ZHS_TAG, Cjk::Simplified)
} else if sc == "Hant" {
(TEXT_LANG_ZHT_TAG, Cjk::Traditional)
} else {
(TEXT_LANG_ZHT_TAG, Cjk::Traditional)
}
None => (TEXT_LANG_ZHT_TAG, Cjk::Traditional)
}
tag_opt = Some(tag)
cjk = zh_cjk
name_index = text_lang_lookup_tag(tag).unwrap_or(-1)
}
Some(Language::{
language,
script,
region,
lang_len,
script_len,
region_len,
cjk,
name_index,
tag: tag_opt,
})
}
///|
/// Returns the language associated with the specified OpenType language tag.
pub fn Language::from_opentype(tag : Tag) -> Language? {
if tag == TEXT_LANG_ZHT_TAG {
return Language::parse("zh-Hant")
} else if tag == TEXT_LANG_ZHS_TAG {
return Language::parse("zh-Hans")
}
let name_index = match text_lang_lookup_tag(tag) {
None => return None
Some(ix) => ix
}
let (_, bcp47) = text_lang_entries[name_index]
Language::parse(bcp47)
}
///|
/// Returns the language component.
pub fn Language::language(self : Language) -> String {
text_lang_bytes_prefix_to_string(self.language, self.lang_len)
}
///|
/// Returns the script component.
pub fn Language::script(self : Language) -> String? {
if self.script_len == 4 {
Some(text_lang_bytes_prefix_to_string(self.script, 4))
} else {
None
}
}
///|
/// Returns the region component.
pub fn Language::region(self : Language) -> String? {
if self.region_len == 2 {
Some(text_lang_bytes_prefix_to_string(self.region, 2))
} else {
None
}
}
///|
/// Returns the CJK language.
pub fn Language::cjk(self : Language) -> Cjk {
self.cjk
}
///|
/// Returns the name of the language.
pub fn Language::name(self : Language) -> String? {
if self.name_index < 0 || self.name_index >= text_lang_entries.length() {
None
} else {
let (name, _) = text_lang_entries[self.name_index]
Some(name)
}
}
///|
/// Returns the associated OpenType language tag.
pub fn Language::to_opentype(self : Language) -> Tag? {
self.tag
}
///|
pub fn Language::to_string(self : Language) -> String {
let mut s = self.language()
match self.script() {
None => ()
Some(sc) => s = s + "-" + sc
}
match self.region() {
None => ()
Some(rg) => s = s + "-" + rg
}
match self.name() {
None => ()
Some(nm) => s = s + " (" + nm + ")"
}
s
}
///|
pub impl Show for Language with fn output(self, logger) {
logger.write_string(self.to_string())
}