use crate::SourceDataProvider;
use crate::properties::ucd_helpers::{self, UcdLine};
use icu::collections::codepointtrie::{CodePointTrie, TrieValue};
use icu::properties::props::EnumeratedProperty;
use icu::properties::provider::{names::*, *};
use icu_provider::prelude::*;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::Debug;
use zerotrie::ZeroTrieSimpleAscii;
use zerovec::ule::NichedOption;
impl SourceDataProvider {
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
pub(super) fn build_enumerated_prop<T: EnumeratedProperty + Debug>(
&self,
short_name_to_t: HashMap<&'static str, T>,
) -> Result<CodePointTrie<'static, T>, DataError> {
let name = str::from_utf8(T::NAME).unwrap();
let short_name = str::from_utf8(T::SHORT_NAME).unwrap();
self.validate_property_name(name, short_name)?;
let (names_to_short_names, _) = self.enumerated_prop_names(name, short_name)?;
let file = match name {
"Indic_Conjunct_Break" => "ucd/DerivedCoreProperties.txt".into(),
"Canonical_Combining_Class"
| "General_Category"
| "Bidi_Class"
| "Numeric_Type"
| "East_Asian_Width"
| "Joining_Type"
| "Joining_Group" => {
format!(
"ucd/extracted/Derived{}.txt",
name.replace('_', "").replace("Canonical", "")
)
}
"Grapheme_Cluster_Break" | "Word_Break" | "Sentence_Break" => {
format!(
"ucd/auxiliary/{}Property.txt",
name.replace('_', "").replace("Cluster", "")
)
}
_ => format!(
"ucd/{}.txt",
name.replace('_', "").replace("Script", "Scripts")
),
};
let mut builder = icu_codepointtrie_builder::CodePointTrieBuilder::new(
T::default(),
T::default(),
self.trie_type().into(),
);
let mut last_seen_cp = -1i32;
for line in self.rscd()?.parse_ucd_lines(&file)? {
match line {
UcdLine::Missing(fields) => {
let mut fields = fields.fields();
let cps = fields.next().unwrap();
if &file == "ucd/DerivedCoreProperties.txt" {
if fields.next().unwrap() != short_name {
continue;
}
}
let value = fields.next().unwrap();
let value = names_to_short_names
.get(value)
.expect("file should only use names from PropertyValueAliases.txt")
.0;
let Some(&value) = short_name_to_t.get(value) else {
continue;
};
let range = ucd_helpers::parse_range(cps);
if range == (0..=0x10FFFF) {
assert_eq!(value, T::default());
} else {
assert!(
*range.start() as i32 > last_seen_cp,
"Found @missing rule after data in its block in {file}, we don't currently handle it"
);
builder.set_range_value(range, value);
}
}
UcdLine::Fields(fields) => {
let mut fields = fields.fields();
let cp_range = fields.next().unwrap();
if &file == "ucd/DerivedCoreProperties.txt" {
if fields.next().unwrap() != short_name {
continue;
}
}
let value = fields.next().unwrap();
let value = names_to_short_names
.get(value)
.expect("file should only use names from PropertyValueAliases.txt")
.0;
let Some(&value) = short_name_to_t.get(value) else {
continue;
};
let range = ucd_helpers::parse_range(cp_range);
last_seen_cp = *range.end() as i32;
builder.set_range_value(range, value);
}
}
}
Ok(builder.build())
}
#[allow(clippy::type_complexity)] fn enumerated_prop_names<'a>(
&'a self,
name: &str,
short_name: &str,
) -> Result<(HashMap<&'a str, (&'a str, NameType)>, Option<&'a str>), DataError> {
let mut names = HashMap::new();
let mut default = None;
for line in self
.rscd()?
.parse_ucd_lines("ucd/PropertyValueAliases.txt")?
{
match line {
UcdLine::Missing(fields) => {
let mut fields = fields.fields();
assert_eq!(
fields.next().unwrap(),
"0000..10FFFF",
"We only expect full-range @missing values in PropertyValueAliases.txt"
);
if fields.next().unwrap() != name {
continue;
}
default = Some(fields.next().unwrap())
}
UcdLine::Fields(fields) => {
let mut fields = fields.fields();
if fields.next().unwrap() != short_name {
continue;
}
let numeric_name = (short_name.as_bytes()
== icu::properties::props::CanonicalCombiningClass::SHORT_NAME)
.then(|| fields.next().unwrap());
let short = fields.next().unwrap();
let long = fields.next().unwrap();
names.insert(short, (short, NameType::Short));
names.insert(long, (short, NameType::Long));
for alias in fields {
names.insert(alias, (short, NameType::Alias));
}
if let Some(numeric_name) = numeric_name {
names.insert(numeric_name, (short, NameType::Numeric));
}
}
}
}
for name in names.keys() {
if name.contains('-') || name.bytes().any(|b| b.is_ascii_whitespace()) {
return Err(
DataError::custom("Property name contains '-' or whitespace")
.with_display_context(name),
);
}
}
Ok((names, default))
}
}
#[derive(Debug)]
enum NameType {
Short,
Long,
Numeric,
Alias,
}
fn validate_dense<T: TrieValue + Debug, V: Debug + Copy>(
map: &HashMap<T, V>,
) -> Result<Vec<V>, DataError> {
let map = map
.iter()
.map(|(k, &v)| (k.to_u32() as usize, v))
.collect::<BTreeMap<_, _>>();
if !map.keys().copied().eq(0..map.len()) {
return Err(DataError::custom(
"Property has more than 0 gaps and cannot be stored in a dense map",
)
.with_debug_context(&map));
};
Ok(map.into_values().collect())
}
#[allow(clippy::unnecessary_wraps)] fn convert_sparse<T: TrieValue>(
map: HashMap<T, &str>,
) -> Result<PropertyEnumToValueNameSparseMap<'static>, DataError> {
Ok(PropertyEnumToValueNameSparseMap {
map: map
.into_iter()
.map(|(k, v)| (u16::try_from(k.to_u32()).unwrap(), v))
.collect(),
})
}
fn convert_linear<T: TrieValue + Debug>(
map: HashMap<T, &str>,
) -> Result<PropertyEnumToValueNameLinearMap<'static>, DataError> {
let dense = validate_dense(&map)?;
Ok(PropertyEnumToValueNameLinearMap {
map: (&dense).into(),
})
}
fn convert_script(
map: HashMap<icu::properties::props::Script, &str>,
) -> Result<PropertyScriptToIcuScriptMap<'static>, DataError> {
let dense = validate_dense(&map)?;
Ok(PropertyScriptToIcuScriptMap {
map: dense
.into_iter()
.map(|s| {
if s.is_empty() {
Ok(NichedOption(None))
} else {
icu::locale::subtags::Script::try_from_str(s)
.map(Some)
.map(NichedOption)
}
})
.collect::<Result<_, _>>()
.map_err(|_| DataError::custom("Found invalid script tag"))?,
})
}
macro_rules! expand {
($(
(
$prop:ty,
$marker:ident,
$parse_marker:ident,
$short_marker:ident[$short_convert:ident],
$long_marker:ident[$long_convert:ident]
)
),+,) => {
$(
impl DataProvider<$marker> for SourceDataProvider
{
fn load(&self, req: DataRequest) -> Result<DataResponse<$marker>, DataError> {
self.check_req::<$marker>(req)?;
#[cfg(not(any(feature = "use_wasm", feature = "use_icu4c")))]
return Err(DataError::custom(
"icu_provider_source must be built with use_icu4c or use_wasm to build properties data",
)
.with_req($marker::INFO, req));
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
{
let trie = if let Some(t) = self.rscd()?.cpt_cache.get(str::from_utf8(<$prop as EnumeratedProperty>::SHORT_NAME).unwrap()).
and_then(|t| t.downcast_ref::<CodePointTrie<'static, $prop>>().cloned()) {
t
} else {
let trie = self.build_enumerated_prop::<$prop>(<$prop>::names().collect())?;
self.rscd()?.cpt_cache
.insert(str::from_utf8(<$prop as EnumeratedProperty>::SHORT_NAME).unwrap(), Box::new(trie.clone()));
trie
};
Ok(DataResponse {
metadata: Default::default(),
payload: DataPayload::from_owned(PropertyCodePointMap::CodePointTrie(trie)),
})
}
}
}
impl DataProvider<$parse_marker> for SourceDataProvider
{
fn load(&self, req: DataRequest) -> Result<DataResponse<$parse_marker>, DataError> {
self.check_req::<$parse_marker>(req)?;
let short_name_to_t = <$prop>::names().collect::<HashMap<_, _>>();
let names = self.enumerated_prop_names(str::from_utf8(<$prop as EnumeratedProperty>::NAME).unwrap(), str::from_utf8(<$prop as EnumeratedProperty>::SHORT_NAME).unwrap())?.0;
for (name, _) in &short_name_to_t {
if !names.contains_key(name) && <$prop as EnumeratedProperty>::SHORT_NAME != icu::properties::props::Script::SHORT_NAME {
log::warn!(
"UCD does not contain {} {name:?}",
str::from_utf8(<$prop as EnumeratedProperty>::NAME).unwrap()
);
}
}
let trie = names
.into_iter()
.filter_map(|(name, (short_name, _))| Some((name, short_name_to_t.get(short_name).copied()?)))
.chain(short_name_to_t.clone().into_iter())
.map(|(n, v)| (n, v.to_u32() as usize))
.collect::<HashMap<_, _>>()
.into_iter()
.collect::<ZeroTrieSimpleAscii<_>>()
.convert_store();
Ok(DataResponse {
metadata: Default::default(),
payload: DataPayload::from_owned(PropertyValueNameToEnumMap { map: trie }),
})
}
}
impl DataProvider<$short_marker> for SourceDataProvider
{
fn load(&self, req: DataRequest) -> Result<DataResponse<$short_marker>, DataError> {
self.check_req::<$short_marker>(req)?;
let map = ($short_convert)(<$prop>::names().map(|(k, v)| (v, k)).collect())?;
Ok(DataResponse {
metadata: Default::default(),
payload: DataPayload::from_owned(map),
})
}
}
impl DataProvider<$long_marker> for SourceDataProvider
{
fn load(&self, req: DataRequest) -> Result<DataResponse<$long_marker>, DataError> {
self.check_req::<$long_marker>(req)?;
let short_name_to_t = <$prop>::names().collect::<HashMap<_, _>>();
let names = self.enumerated_prop_names(str::from_utf8(<$prop as EnumeratedProperty>::NAME).unwrap(), str::from_utf8(<$prop as EnumeratedProperty>::SHORT_NAME).unwrap())?.0;
let names = short_name_to_t.iter().map(|(&short_name, &t)| (t, short_name))
.chain(names
.iter()
.filter(|(_, (_, ty))| matches!(ty, NameType::Long))
.filter_map(|(&name, (short_name, _))| {
let Some(&t) = short_name_to_t.get(short_name) else {
if <$prop>::SHORT_NAME == icu::properties::props::GeneralCategory::SHORT_NAME {
return None;
}
log::error!(
"Missing Rust value for {} {name:?} {short_name:?}",
str::from_utf8(<$prop as EnumeratedProperty>::NAME).unwrap()
);
return None;
};
Some((t, name))
})
)
.collect();
let map = ($long_convert)(names)?;
Ok(DataResponse {
metadata: Default::default(),
payload: DataPayload::from_owned(map),
})
}
}
impl crate::IterableDataProviderCached<$marker> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
Ok(HashSet::from_iter([Default::default()]))
}
}
impl crate::IterableDataProviderCached<$parse_marker> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
Ok(HashSet::from_iter([Default::default()]))
}
}
impl crate::IterableDataProviderCached<$short_marker> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
Ok(HashSet::from_iter([Default::default()]))
}
}
impl crate::IterableDataProviderCached<$long_marker> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
Ok(HashSet::from_iter([Default::default()]))
}
}
)+
}
}
impl DataProvider<PropertyNameParseGeneralCategoryMaskV1> for SourceDataProvider {
fn load(
&self,
req: DataRequest,
) -> Result<DataResponse<PropertyNameParseGeneralCategoryMaskV1>, DataError> {
use icu::properties::props::GeneralCategoryGroup;
self.check_req::<PropertyNameParseGeneralCategoryMaskV1>(req)?;
let short_name_to_t = GeneralCategoryGroup::names().collect::<HashMap<_, _>>();
let trie = self
.enumerated_prop_names("General_Category", "gc")?
.0
.into_iter()
.filter(|(_, (_, ty))| matches!(ty, NameType::Short | NameType::Long | NameType::Alias))
.filter_map(|(name, (short_name, _))| {
let Some(&t) = short_name_to_t.get(short_name) else {
log::error!(
"Missing Rust value for GeneralCategoryGroup {name:?} {short_name:?}"
);
return None;
};
Some((name, t))
})
.map(|(n, v)| (n, v.to_u32() as usize))
.collect::<ZeroTrieSimpleAscii<_>>()
.convert_store();
Ok(DataResponse {
metadata: Default::default(),
payload: DataPayload::from_owned(PropertyValueNameToEnumMap { map: trie }),
})
}
}
impl crate::IterableDataProviderCached<PropertyNameParseGeneralCategoryMaskV1>
for SourceDataProvider
{
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
Ok(HashSet::from_iter([Default::default()]))
}
}
expand!(
(
icu::properties::props::CanonicalCombiningClass,
PropertyEnumCanonicalCombiningClassV1,
PropertyNameParseCanonicalCombiningClassV1,
PropertyNameShortCanonicalCombiningClassV1[convert_sparse],
PropertyNameLongCanonicalCombiningClassV1[convert_sparse]
),
(
icu::properties::props::GeneralCategory,
PropertyEnumGeneralCategoryV1,
PropertyNameParseGeneralCategoryV1,
PropertyNameShortGeneralCategoryV1[convert_linear],
PropertyNameLongGeneralCategoryV1[convert_linear]
),
(
icu::properties::props::BidiClass,
PropertyEnumBidiClassV1,
PropertyNameParseBidiClassV1,
PropertyNameShortBidiClassV1[convert_linear],
PropertyNameLongBidiClassV1[convert_linear]
),
(
icu::properties::props::NumericType,
PropertyEnumNumericTypeV1,
PropertyNameParseNumericTypeV1,
PropertyNameShortNumericTypeV1[convert_linear],
PropertyNameLongNumericTypeV1[convert_linear]
),
(
icu::properties::props::Script,
PropertyEnumScriptV1,
PropertyNameParseScriptV1,
PropertyNameShortScriptV1[convert_script],
PropertyNameLongScriptV1[convert_linear]
),
(
icu::properties::props::HangulSyllableType,
PropertyEnumHangulSyllableTypeV1,
PropertyNameParseHangulSyllableTypeV1,
PropertyNameShortHangulSyllableTypeV1[convert_linear],
PropertyNameLongHangulSyllableTypeV1[convert_linear]
),
(
icu::properties::props::EastAsianWidth,
PropertyEnumEastAsianWidthV1,
PropertyNameParseEastAsianWidthV1,
PropertyNameShortEastAsianWidthV1[convert_linear],
PropertyNameLongEastAsianWidthV1[convert_linear]
),
(
icu::properties::props::IndicSyllabicCategory,
PropertyEnumIndicSyllabicCategoryV1,
PropertyNameParseIndicSyllabicCategoryV1,
PropertyNameShortIndicSyllabicCategoryV1[convert_linear],
PropertyNameLongIndicSyllabicCategoryV1[convert_linear]
),
(
icu::properties::props::IndicConjunctBreak,
PropertyEnumIndicConjunctBreakV1,
PropertyNameParseIndicConjunctBreakV1,
PropertyNameShortIndicConjunctBreakV1[convert_linear],
PropertyNameLongIndicConjunctBreakV1[convert_linear]
),
(
icu::properties::props::LineBreak,
PropertyEnumLineBreakV1,
PropertyNameParseLineBreakV1,
PropertyNameShortLineBreakV1[convert_linear],
PropertyNameLongLineBreakV1[convert_linear]
),
(
icu::properties::props::GraphemeClusterBreak,
PropertyEnumGraphemeClusterBreakV1,
PropertyNameParseGraphemeClusterBreakV1,
PropertyNameShortGraphemeClusterBreakV1[convert_linear],
PropertyNameLongGraphemeClusterBreakV1[convert_linear]
),
(
icu::properties::props::WordBreak,
PropertyEnumWordBreakV1,
PropertyNameParseWordBreakV1,
PropertyNameShortWordBreakV1[convert_linear],
PropertyNameLongWordBreakV1[convert_linear]
),
(
icu::properties::props::SentenceBreak,
PropertyEnumSentenceBreakV1,
PropertyNameParseSentenceBreakV1,
PropertyNameShortSentenceBreakV1[convert_linear],
PropertyNameLongSentenceBreakV1[convert_linear]
),
(
icu::properties::props::JoiningType,
PropertyEnumJoiningTypeV1,
PropertyNameParseJoiningTypeV1,
PropertyNameShortJoiningTypeV1[convert_linear],
PropertyNameLongJoiningTypeV1[convert_linear]
),
(
icu::properties::props::JoiningGroup,
PropertyEnumJoiningGroupV1,
PropertyNameParseJoiningGroupV1,
PropertyNameShortJoiningGroupV1[convert_linear],
PropertyNameLongJoiningGroupV1[convert_linear]
),
(
icu::properties::props::VerticalOrientation,
PropertyEnumVerticalOrientationV1,
PropertyNameParseVerticalOrientationV1,
PropertyNameShortVerticalOrientationV1[convert_linear],
PropertyNameLongVerticalOrientationV1[convert_linear]
),
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_general_category() {
use icu::properties::{CodePointMapData, props::GeneralCategory};
let provider = SourceDataProvider::new_testing();
let trie = CodePointMapData::<GeneralCategory>::try_new_unstable(&provider).unwrap();
let trie = trie.as_code_point_trie().unwrap();
assert_eq!(trie.get32('꣓' as u32), GeneralCategory::DecimalNumber);
assert_eq!(trie.get32('≈' as u32), GeneralCategory::MathSymbol);
}
#[test]
fn test_script() {
use icu::properties::{CodePointMapData, props::Script};
let provider = SourceDataProvider::new_testing();
let trie = CodePointMapData::<Script>::try_new_unstable(&provider).unwrap();
let trie = trie.as_code_point_trie().unwrap();
assert_eq!(trie.get32('꣓' as u32), Script::Saurashtra);
assert_eq!(trie.get32('≈' as u32), Script::Common);
}
}