use std::sync::OnceLock;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[allow(non_camel_case_types)]
pub enum Category {
Lu,
Ll,
Lt,
Lm,
Lo,
Mn,
Mc,
Me,
Nd,
Nl,
No,
Pc,
Pd,
Ps,
Pe,
Pi,
Pf,
Po,
Sm,
Sc,
Sk,
So,
Zs,
Zl,
Zp,
Cc,
Cf,
Cs,
Co,
Cn,
}
impl Category {
pub fn as_str(self) -> &'static str {
match self {
Category::Lu => "Lu",
Category::Ll => "Ll",
Category::Lt => "Lt",
Category::Lm => "Lm",
Category::Lo => "Lo",
Category::Mn => "Mn",
Category::Mc => "Mc",
Category::Me => "Me",
Category::Nd => "Nd",
Category::Nl => "Nl",
Category::No => "No",
Category::Pc => "Pc",
Category::Pd => "Pd",
Category::Ps => "Ps",
Category::Pe => "Pe",
Category::Pi => "Pi",
Category::Pf => "Pf",
Category::Po => "Po",
Category::Sm => "Sm",
Category::Sc => "Sc",
Category::Sk => "Sk",
Category::So => "So",
Category::Zs => "Zs",
Category::Zl => "Zl",
Category::Zp => "Zp",
Category::Cc => "Cc",
Category::Cf => "Cf",
Category::Cs => "Cs",
Category::Co => "Co",
Category::Cn => "Cn",
}
}
fn from_code(s: &str) -> Option<Category> {
Some(match s {
"Lu" => Category::Lu,
"Ll" => Category::Ll,
"Lt" => Category::Lt,
"Lm" => Category::Lm,
"Lo" => Category::Lo,
"Mn" => Category::Mn,
"Mc" => Category::Mc,
"Me" => Category::Me,
"Nd" => Category::Nd,
"Nl" => Category::Nl,
"No" => Category::No,
"Pc" => Category::Pc,
"Pd" => Category::Pd,
"Ps" => Category::Ps,
"Pe" => Category::Pe,
"Pi" => Category::Pi,
"Pf" => Category::Pf,
"Po" => Category::Po,
"Sm" => Category::Sm,
"Sc" => Category::Sc,
"Sk" => Category::Sk,
"So" => Category::So,
"Zs" => Category::Zs,
"Zl" => Category::Zl,
"Zp" => Category::Zp,
"Cc" => Category::Cc,
"Cf" => Category::Cf,
"Cs" => Category::Cs,
"Co" => Category::Co,
"Cn" => Category::Cn,
_ => return None,
})
}
}
const CATEGORIES_DATA: &str = include_str!("categories.txt");
const NFD_BASES_DATA: &str = include_str!("nfd_bases.txt");
fn ranges() -> &'static [(u32, Category)] {
static RANGES: OnceLock<Vec<(u32, Category)>> = OnceLock::new();
RANGES.get_or_init(|| {
CATEGORIES_DATA
.lines()
.map(|line| {
let (end, cat) = line
.split_once(' ')
.unwrap_or_else(|| panic!("malformed categories.txt line: {line:?}"));
let end = u32::from_str_radix(end, 16)
.unwrap_or_else(|_| panic!("bad hex in categories.txt: {end:?}"));
let cat = Category::from_code(cat)
.unwrap_or_else(|| panic!("unknown category in categories.txt: {cat:?}"));
(end, cat)
})
.collect()
})
}
fn nfd_bases() -> &'static [(u32, u32)] {
static BASES: OnceLock<Vec<(u32, u32)>> = OnceLock::new();
BASES.get_or_init(|| {
NFD_BASES_DATA
.lines()
.map(|line| {
let (cp, base) = line
.split_once(' ')
.unwrap_or_else(|| panic!("malformed nfd_bases.txt line: {line:?}"));
let cp = u32::from_str_radix(cp, 16)
.unwrap_or_else(|_| panic!("bad hex in nfd_bases.txt: {cp:?}"));
let base = u32::from_str_radix(base, 16)
.unwrap_or_else(|_| panic!("bad hex in nfd_bases.txt: {base:?}"));
(cp, base)
})
.collect()
})
}
pub fn general_category(cp: u32) -> Category {
assert!(cp <= 0x10FFFF, "codepoint {:#x} out of range", cp);
let table = ranges();
let idx = table
.binary_search_by(|&(end, _)| {
if end < cp {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
})
.unwrap_err();
table[idx].1
}
pub fn is_in_group(cp: u32, group: &str) -> bool {
let cat = general_category(cp).as_str();
match group.len() {
2 => cat == group,
1 => cat.starts_with(group) && MAJOR_CLASSES.contains(&group),
_ => false,
}
}
const MAJOR_CLASSES: &[&str] = &["L", "M", "N", "P", "S", "Z", "C"];
pub fn nfd_base(cp: u32) -> Option<u32> {
let table = nfd_bases();
table
.binary_search_by_key(&cp, |&(c, _)| c)
.ok()
.map(|idx| table[idx].1)
}
#[cfg(test)]
#[path = "../../tests/embedded/native/unicodedata_tests.rs"]
mod tests;