use crate::SourceDataProvider;
use crate::properties::ucd_helpers;
use icu::casemap::provider::data::{CaseMapData, CaseType, DotType};
use icu::casemap::provider::exceptions::{CaseMapExceptions, Exception};
use icu::casemap::provider::{CaseMap, CaseMapUnfold, CaseMapUnfoldV1, CaseMapV1};
use icu::locale::LanguageIdentifier;
use icu::properties::{CodePointMapData, CodePointSetData, props};
use icu_provider::prelude::*;
use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque};
use std::convert::TryFrom;
#[cfg(not(any(feature = "use_wasm", feature = "use_icu4c")))]
impl DataProvider<CaseMapV1> for SourceDataProvider {
fn load(&self, req: DataRequest) -> Result<DataResponse<CaseMapV1>, DataError> {
self.check_req::<CaseMapV1>(req)?;
Err(DataError::custom(
"The `use_wasm` or `use_icu4c` feature must be enabled to use CaseMapV1",
))
}
}
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
impl DataProvider<CaseMapV1> for SourceDataProvider {
fn load(&self, req: DataRequest) -> Result<DataResponse<CaseMapV1>, DataError> {
self.check_req::<CaseMapV1>(req)?;
let mut adjacency_list: BTreeMap<char, BTreeSet<char>> = BTreeMap::new();
fn add_edge(u: char, v: Option<char>, adjacency_list: &mut BTreeMap<char, BTreeSet<char>>) {
if let Some(v) = v {
adjacency_list.entry(u).or_default().insert(v);
adjacency_list.entry(v).or_default().insert(u);
}
}
fn add_edges(u: char, v: &str, adjacency_list: &mut BTreeMap<char, BTreeSet<char>>) {
if v.chars().count() == 1 {
add_edge(u, v.chars().next(), adjacency_list);
} else {
adjacency_list.entry(u).or_default();
for v in v.chars() {
adjacency_list.entry(v).or_default();
}
}
}
let mut simple = BTreeMap::new();
for line in self.rscd()?.parse_ucd_lines("ucd/UnicodeData.txt")? {
let Some(line) = line.skip_missing_rule() else {
continue;
};
let mut fields = line.fields();
let Ok(cp) = char::try_from(ucd_helpers::parse_cp(fields.next().unwrap())) else {
continue;
};
for _ in 1..=11 {
fields.next();
}
let upper = fields
.next()
.filter(|s| !s.is_empty())
.map(ucd_helpers::parse_cp)
.map(|cp| char::from_u32(cp).unwrap());
let lower = fields
.next()
.filter(|s| !s.is_empty())
.map(ucd_helpers::parse_cp)
.map(|cp| char::from_u32(cp).unwrap());
let title = fields
.next()
.filter(|s| !s.is_empty())
.map(ucd_helpers::parse_cp)
.map(|cp| char::from_u32(cp).unwrap())
.or(upper);
assert_eq!(fields.next(), None);
add_edge(cp, lower, &mut adjacency_list);
add_edge(cp, upper, &mut adjacency_list);
add_edge(cp, title, &mut adjacency_list);
simple.insert(cp, (upper, lower, title));
}
let mut special = BTreeMap::<char, (String, String, String, bool)>::new();
for line in self.rscd()?.parse_ucd_lines("ucd/SpecialCasing.txt")? {
let Some(line) = line.skip_missing_rule() else {
continue;
};
let mut fields = line.fields();
let cp = char::from_u32(ucd_helpers::parse_cp(fields.next().unwrap())).unwrap();
let lower = ucd_helpers::parse_cps(fields.next().unwrap());
let title = ucd_helpers::parse_cps(fields.next().unwrap());
let upper = ucd_helpers::parse_cps(fields.next().unwrap());
let condition = fields.next().filter(|s| !s.is_empty());
let entry = special.entry(cp).or_default();
if let Some(condition) = condition {
entry.3 = true;
let condition = if let Ok(langid) = condition
.split_ascii_whitespace()
.next()
.unwrap()
.parse::<LanguageIdentifier>()
{
const LT: LanguageIdentifier = icu::locale::langid!("lt");
const AZ: LanguageIdentifier = icu::locale::langid!("az");
const TR: LanguageIdentifier = icu::locale::langid!("tr");
if !matches!(langid, LT | AZ | TR) {
log::error!(
"Unhandled language condition in SpecialCasing.txt: {condition}"
);
}
condition.split_ascii_whitespace().nth(1)
} else {
condition.split_ascii_whitespace().next()
};
if let Some(condition) = condition
&& !matches!(
condition.strip_prefix("Not_").unwrap_or(condition),
"Final_Sigma"
| "After_Soft_Dotted"
| "More_Above"
| "After_I"
| "Before_Dot"
)
{
log::error!("Unhandled condition in SpecialCasing.txt: {condition}");
}
} else {
add_edges(cp, &lower, &mut adjacency_list);
add_edges(cp, &upper, &mut adjacency_list);
add_edges(cp, &title, &mut adjacency_list);
entry.0 = lower;
entry.1 = upper;
entry.2 = title;
}
}
special.entry('Ö‡').or_default().3 = true;
let mut case_folds = BTreeMap::<char, (Option<char>, Option<String>, bool)>::new();
for line in self.rscd()?.parse_ucd_lines("ucd/CaseFolding.txt")? {
let Some(line) = line.skip_missing_rule() else {
continue;
};
let mut fields = line.fields();
let cp = char::from_u32(ucd_helpers::parse_cp(fields.next().unwrap())).unwrap();
let status = fields.next().unwrap();
let full_fold = ucd_helpers::parse_cps(fields.next().unwrap());
let simple_fold = full_fold.chars().next().unwrap();
let entry = case_folds.entry(cp).or_default();
match status {
"C" => {
add_edge(cp, Some(simple_fold), &mut adjacency_list);
add_edges(cp, &full_fold, &mut adjacency_list);
entry.0 = Some(simple_fold);
entry.1 = Some(full_fold);
}
"F" => {
add_edges(cp, &full_fold, &mut adjacency_list);
entry.1 = Some(full_fold);
}
"S" => {
add_edge(cp, Some(simple_fold), &mut adjacency_list);
entry.0 = Some(simple_fold);
}
"T" => {
entry.2 = true;
}
_ => unreachable!("Invalid status in CaseFolding.txt: {}", status),
}
}
let case_ignorable = CodePointSetData::try_new_unstable::<props::CaseIgnorable>(self)?;
let case_ignorable = case_ignorable.as_borrowed();
let soft_dotted = CodePointSetData::try_new_unstable::<props::SoftDotted>(self)?;
let soft_dotted = soft_dotted.as_borrowed();
let ccc = CodePointMapData::<props::CanonicalCombiningClass>::try_new_unstable(self)?;
let ccc = ccc.as_borrowed();
let is_lowercase = CodePointSetData::try_new_unstable::<props::Lowercase>(self)?;
let is_lowercase = is_lowercase.as_borrowed();
let is_uppercase = CodePointSetData::try_new_unstable::<props::Uppercase>(self)?;
let is_uppercase = is_uppercase.as_borrowed();
let gc = CodePointMapData::<props::GeneralCategory>::try_new_unstable(self)?;
let gc = gc.as_borrowed();
let mut builder = icu_codepointtrie_builder::CodePointTrieBuilder::new(
CaseMapData::UNCASED_INSENSITIVE_NO_DOT,
CaseMapData::UNCASED_INSENSITIVE_NO_DOT,
self.trie_type().into(),
);
let mut exceptions = Vec::<Exception>::new();
for (&c, &(simple_upper, simple_lower, simple_title)) in &simple {
let (full_lower, full_upper, full_title, has_conditional_map) = special
.get(&c)
.map(|(lower, title, upper, has_conditional_special)| {
(
Some(lower.as_str()),
Some(title.as_str()),
Some(upper.as_str()),
*has_conditional_special,
)
})
.unwrap_or((None, None, None, false));
let (simple_fold, full_fold, has_conditional_fold) = case_folds
.get(&c)
.map(|(simple, full, turkic)| (*simple, full.as_deref(), *turkic))
.unwrap_or((None, None, false));
let mut full_closure = BTreeSet::from_iter([c]);
let mut queue = VecDeque::from_iter([c]);
while let Some(u) = queue.pop_front() {
if let Some(neighbors) = adjacency_list.get(&u) {
for &v in neighbors {
if full_closure.insert(v) {
queue.push_back(v);
}
}
}
}
full_closure.remove(&c);
let dot_type = if soft_dotted.contains(c) {
DotType::SoftDotted
} else {
match ccc.get(c) {
props::CanonicalCombiningClass::Above => DotType::Above,
props::CanonicalCombiningClass::NotReordered => DotType::NoDot,
_ => DotType::OtherAccent,
}
};
let is_ignoreable = case_ignorable.contains(c);
let is_sensitive = adjacency_list.contains_key(&c);
let case_type = if is_lowercase.contains(c) {
Some(CaseType::Lower)
} else if is_uppercase.contains(c) {
Some(CaseType::Upper)
} else if gc.get(c) == props::GeneralCategory::TitlecaseLetter {
Some(CaseType::Title)
} else {
None
};
builder.set_value(
c as u32,
CaseMapData::new(
&mut exceptions,
c,
is_ignoreable,
is_sensitive,
dot_type,
case_type,
simple_upper,
simple_lower,
simple_title,
full_lower,
full_upper,
full_title,
has_conditional_map,
simple_fold,
full_fold,
has_conditional_fold,
full_closure,
),
);
}
let case_mapping = CaseMap {
trie: builder.build(),
exceptions: CaseMapExceptions {
exceptions: exceptions.as_slice().into(),
},
};
Ok(DataResponse {
metadata: Default::default(),
payload: DataPayload::from_owned(case_mapping),
})
}
}
impl crate::IterableDataProviderCached<CaseMapV1> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
Ok(HashSet::from_iter([Default::default()]))
}
}
impl DataProvider<CaseMapUnfoldV1> for SourceDataProvider {
fn load(&self, req: DataRequest) -> Result<DataResponse<CaseMapUnfoldV1>, DataError> {
self.check_req::<CaseMapUnfoldV1>(req)?;
let mut unfold_map: BTreeMap<String, BTreeSet<char>> = BTreeMap::new();
for line in self.rscd()?.parse_ucd_lines("ucd/CaseFolding.txt")? {
let Some(line) = line.skip_missing_rule() else {
continue;
};
let mut fields = line.fields();
let cp = char::from_u32(ucd_helpers::parse_cp(fields.next().unwrap())).unwrap();
let status = fields.next().unwrap();
let full_mapping = ucd_helpers::parse_cps(fields.next().unwrap());
if matches!(status, "C" | "F") && full_mapping.chars().count() > 1 {
unfold_map.entry(full_mapping).or_default().insert(cp);
}
}
let map = unfold_map
.iter()
.map(|(full_mapping, cps)| {
(
potential_utf::PotentialUtf8::from_str(full_mapping),
cps.iter().copied().collect::<String>(),
)
})
.collect();
Ok(DataResponse {
metadata: Default::default(),
payload: DataPayload::from_owned(CaseMapUnfold { map }),
})
}
}
impl crate::IterableDataProviderCached<CaseMapUnfoldV1> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
Ok(HashSet::from_iter([Default::default()]))
}
}