#[cfg(feature = "unstable")]
use crate::IterableDataProviderCached;
use crate::SourceDataProvider;
use crate::cldr_cache::CldrCache;
#[cfg(feature = "unstable")]
use crate::source::Cache;
use crate::source::{RscdCache, include_files};
#[cfg(feature = "unstable")]
use icu::collections::codepointinvlist::CodePointInversionList;
#[cfg(feature = "unstable")]
use icu::locale::extensions::unicode::key;
use icu::properties::{
CodePointMapData, CodePointMapDataBorrowed, CodePointSetData,
props::{
EastAsianWidth, GeneralCategory, GraphemeClusterBreak, IndicConjunctBreak, LineBreak,
Script, SentenceBreak, WordBreak,
},
};
use icu::segmenter::options::WordType;
use icu::segmenter::provider::*;
use icu_provider::prelude::*;
use std::collections::HashSet;
#[cfg(feature = "unstable")]
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Debug;
use std::sync::OnceLock;
mod dictionary;
mod lstm;
#[cfg(feature = "unstable")]
mod unihan;
#[derive(serde::Deserialize, Debug)]
struct SegmenterProperty {
name: String,
left: Option<String>,
right: Option<String>,
interm_break_state: Option<bool>,
as_simple_property: Option<bool>,
}
#[derive(serde::Deserialize, Debug)]
struct SegmenterState {
left: Vec<String>,
right: Vec<String>,
break_state: Option<bool>,
}
#[derive(serde::Deserialize, Debug)]
struct SegmenterRuleTable {
segmenter_type: String,
tables: Vec<SegmenterProperty>,
rules: Vec<SegmenterState>,
}
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
fn generate_rule_break_data(
provider: &SourceDataProvider,
rules_file: &str,
trie_type: crate::TrieType,
) -> Result<RuleBreakData<'static>, DataError> {
use icu::properties::{PropertyParser, props::ExtendedPictographic};
use icu_codepointtrie_builder::CodePointTrieBuilder;
let segmenter =
toml::from_str::<SegmenterRuleTable>(rules_file).expect("The data should be valid!");
fn set_break_state(
break_state_table: &mut [Option<BreakState>],
property_length: usize,
left_index: usize,
right_index: usize,
break_state: BreakState,
) {
let index = left_index * property_length + right_index;
if break_state_table[index].is_none()
|| break_state_table[index] == Some(BreakState::NoMatch)
{
break_state_table[index] = Some(break_state);
}
}
fn get_index_from_name(properties_names: &[String], s: &str) -> Option<usize> {
properties_names.iter().position(|n| n.eq(s))
}
fn is_east_asian(eaw: CodePointMapDataBorrowed<EastAsianWidth>, codepoint: u32) -> bool {
matches!(
eaw.get32(codepoint),
EastAsianWidth::Fullwidth | EastAsianWidth::Halfwidth | EastAsianWidth::Wide
)
}
fn is_cjk_fullwidth(eaw: CodePointMapDataBorrowed<EastAsianWidth>, codepoint: u32) -> bool {
matches!(
eaw.get32(codepoint),
EastAsianWidth::Ambiguous | EastAsianWidth::Fullwidth | EastAsianWidth::Wide
)
}
const CODEPOINT_TABLE_LEN: usize = 0xE1000;
let mut properties_trie = CodePointTrieBuilder::new(0u8, 0, trie_type.into());
let mut properties_names = Vec::<String>::new();
let mut simple_properties_count = 0;
properties_names.push("Unknown".to_string());
simple_properties_count += 1;
match &*segmenter.segmenter_type {
"word" => {
let wb = CodePointMapData::<WordBreak>::try_new_unstable(provider)?;
let wb = wb.as_borrowed();
let extended_pictographic =
CodePointSetData::try_new_unstable::<ExtendedPictographic>(provider)?;
let extended_pictographic = extended_pictographic.as_borrowed();
let script = CodePointMapData::<Script>::try_new_unstable(provider)?;
let script = script.as_borrowed();
let lb = CodePointMapData::<LineBreak>::try_new_unstable(provider)?;
let lb = lb.as_borrowed();
let wb_name_to_enum = PropertyParser::<WordBreak>::try_new_unstable(provider)?;
let wb_name_to_enum = wb_name_to_enum.as_borrowed();
for p in &segmenter.tables {
let property_index = if !properties_names.contains(&p.name) {
properties_names.push(p.name.clone());
(properties_names.len() - 1).try_into().unwrap()
} else {
continue;
};
if p.left.is_none() && p.right.is_none() {
simple_properties_count += 1;
if p.as_simple_property.is_some() {
continue;
}
if p.name == "Extended_Pictographic" {
for range in extended_pictographic.iter_ranges() {
for ch in range.filter(|ch| wb.get32(*ch) != WordBreak::ALetter) {
properties_trie.set_value(ch, property_index);
}
}
continue;
}
if p.name == "ALetter_Extended_Pictographic" {
for range in wb.iter_ranges_for_value(WordBreak::ALetter) {
for ch in range.filter(|ch| extended_pictographic.contains32(*ch)) {
properties_trie.set_value(ch, property_index);
}
}
continue;
}
if p.name == "SA" {
for range in script.iter_ranges_for_value(Script::Han) {
properties_trie.set_range_value(range, property_index);
}
for range in script.iter_ranges_for_value(Script::Hiragana) {
properties_trie.set_range_value(range, property_index);
}
for range in lb.iter_ranges_for_value(LineBreak::ComplexContext) {
for ch in range.filter(|ch| *ch != 0x19da) {
properties_trie.set_value(ch, property_index);
}
}
continue;
}
let prop = wb_name_to_enum
.get_loose(&p.name)
.expect("property name should be valid!");
for range in wb.iter_ranges_for_value(prop) {
if prop == WordBreak::Extend {
for ch in range.filter(|ch| {
script.get32(*ch) != Script::Han
&& lb.get32(*ch) != LineBreak::ComplexContext
}) {
properties_trie.set_value(ch, property_index);
}
} else if prop == WordBreak::ALetter {
for ch in range.filter(|ch| !extended_pictographic.contains32(*ch)) {
properties_trie.set_value(ch, property_index);
}
} else {
properties_trie.set_range_value(range, property_index);
}
}
continue;
}
}
}
"grapheme" => {
let extended_pictographic =
CodePointSetData::try_new_unstable::<ExtendedPictographic>(provider)?;
let extended_pictographic = extended_pictographic.as_borrowed();
let incb = CodePointMapData::<IndicConjunctBreak>::try_new_unstable(provider)?;
let incb = incb.as_borrowed();
let gcb_name_to_enum =
PropertyParser::<GraphemeClusterBreak>::try_new_unstable(provider)?;
let gcb_name_to_enum = gcb_name_to_enum.as_borrowed();
let gb = CodePointMapData::<GraphemeClusterBreak>::try_new_unstable(provider)?;
let gb = gb.as_borrowed();
for p in &segmenter.tables {
let property_index = if !properties_names.contains(&p.name) {
properties_names.push(p.name.clone());
(properties_names.len() - 1).try_into().unwrap()
} else {
continue;
};
if p.left.is_none() && p.right.is_none() {
simple_properties_count += 1;
if p.as_simple_property.is_some() {
continue;
}
if p.name == "Extended_Pictographic" {
for range in extended_pictographic.iter_ranges() {
properties_trie.set_range_value(range, property_index);
}
continue;
}
let relevant_incb = match &*p.name {
"InCBConsonant" => Some(IndicConjunctBreak::Consonant),
"InCBLinker" => Some(IndicConjunctBreak::Linker),
"InCBExtend" => Some(IndicConjunctBreak::Extend),
_ => None,
};
if let Some(relevant_incb) = relevant_incb {
for range in incb.iter_ranges_for_value(relevant_incb) {
if range.contains(&0x200D) {
for ch in range.filter(|ch| *ch != 0x200D) {
properties_trie.set_value(ch, property_index);
}
} else {
properties_trie.set_range_value(range, property_index);
}
}
continue;
}
let prop = gcb_name_to_enum
.get_loose(&p.name)
.expect("property name should be valid!");
for range in gb.iter_ranges_for_value(prop) {
properties_trie.set_range_value(range, property_index);
}
continue;
}
}
}
"sentence" => {
let sb = CodePointMapData::<SentenceBreak>::try_new_unstable(provider)?;
let sb = sb.as_borrowed();
let sb_name_to_enum = PropertyParser::<SentenceBreak>::try_new_unstable(provider)?;
let sb_name_to_enum = sb_name_to_enum.as_borrowed();
for p in &segmenter.tables {
let property_index = if !properties_names.contains(&p.name) {
properties_names.push(p.name.clone());
(properties_names.len() - 1).try_into().unwrap()
} else {
continue;
};
if p.left.is_none() && p.right.is_none() {
simple_properties_count += 1;
if p.as_simple_property.is_some() {
continue;
}
let prop = sb_name_to_enum
.get_loose(&p.name)
.expect("property name should be valid!");
for range in sb.iter_ranges_for_value(prop) {
properties_trie.set_range_value(range, property_index);
}
continue;
}
}
}
"line" => {
let lb = CodePointMapData::<LineBreak>::try_new_unstable(provider)?;
let lb = lb.as_borrowed();
let eaw = CodePointMapData::<EastAsianWidth>::try_new_unstable(provider)?;
let eaw = eaw.as_borrowed();
let gc = CodePointMapData::<GeneralCategory>::try_new_unstable(provider)?;
let gc = gc.as_borrowed();
let extended_pictographic =
CodePointSetData::try_new_unstable::<ExtendedPictographic>(provider)?;
let extended_pictographic = extended_pictographic.as_borrowed();
let lb_name_to_enum = PropertyParser::<LineBreak>::try_new_unstable(provider)?;
let lb_name_to_enum = lb_name_to_enum.as_borrowed();
let unicode_15_1_rules = segmenter.tables.iter().any(|p| p.name == "OP_OP30");
for p in &segmenter.tables {
let property_index = if !properties_names.contains(&p.name) {
properties_names.push(p.name.clone());
(properties_names.len() - 1).try_into().unwrap()
} else {
continue;
};
if p.left.is_none() && p.right.is_none() {
simple_properties_count += 1;
if p.as_simple_property.is_some() {
continue;
}
if p.name == "ID_CN"
|| p.name == "QU_PI"
|| p.name == "QU_PF"
|| p.name == "CP_EA"
|| p.name == "OP_OP30"
|| p.name == "OP_EA"
|| p.name == "PO_EAW"
|| p.name == "PR_EAW"
|| p.name == "AL_DOTTED_CIRCLE"
|| p.name == "SA_MC_MN"
|| p.name == "AI_EastAsian"
|| p.name == "AL_DottedCircle"
|| p.name == "AL_EastAsian"
|| p.name == "BA_EastAsian"
|| p.name == "CL_EastAsian"
|| p.name == "CM_EastAsian"
|| p.name == "EB_EastAsian"
|| p.name == "EX_EastAsian"
|| p.name == "GL_EastAsian"
|| p.name == "ID_EastAsian"
|| p.name == "IN_EastAsian"
|| p.name == "NS_EastAsian"
|| p.name == "OP_EastAsian"
|| p.name == "PO_EastAsian"
|| p.name == "PR_EastAsian"
|| p.name == "XX_ExtPict"
{
for cp in 0..(CODEPOINT_TABLE_LEN as u32) {
match lb.get32(cp) {
LineBreak::OpenPunctuation
if (p.name == "OP_EastAsian" && is_east_asian(eaw, cp))
|| (p.name == "OP_OP30" && !is_east_asian(eaw, cp))
|| (p.name == "OP_EA" && is_east_asian(eaw, cp)) => {
properties_trie.set_value(cp, property_index);
}
LineBreak::CloseParenthesis
if p.name == "CP_EA"
&& (eaw.get32(cp) == EastAsianWidth::Fullwidth
|| eaw.get32(cp) == EastAsianWidth::Halfwidth
|| eaw.get32(cp) == EastAsianWidth::Wide)
=> {
properties_trie.set_value(cp, property_index);
}
LineBreak::Ideographic => {
if p.name == "ID_CN"
&& gc.get32(cp) == GeneralCategory::Unassigned
{
if let Some(c) = char::from_u32(cp) {
if extended_pictographic.contains(c) {
properties_trie.set_value(cp, property_index);
} else if unicode_15_1_rules {
match cp {
0x1f774..=0x1f77f => properties_trie
.set_value(cp, property_index),
0x1f8ae..=0x1f8ff => properties_trie
.set_value(cp, property_index),
0x1f947..=0x1faff => properties_trie
.set_value(cp, property_index),
_ => {}
};
}
}
} else if p.name == "ID_EastAsian" && is_east_asian(eaw, cp) {
properties_trie.set_value(cp, property_index);
}
}
LineBreak::PostfixNumeric
if (p.name == "PO_EastAsian" && is_east_asian(eaw, cp))
|| (p.name == "PO_EAW" && is_cjk_fullwidth(eaw, cp)) => {
properties_trie.set_value(cp, property_index);
}
LineBreak::PrefixNumeric
if (p.name == "PR_EastAsian" && is_east_asian(eaw, cp))
|| (p.name == "PR_EAW" && is_cjk_fullwidth(eaw, cp)) => {
properties_trie.set_value(cp, property_index);
}
LineBreak::Ambiguous
if p.name == "AI_EastAsian" && is_east_asian(eaw, cp) => {
properties_trie.set_value(cp, property_index);
}
LineBreak::Alphabetic
if (p.name == "AL_EastAsian" && is_east_asian(eaw, cp))
|| (p.name == "AL_DottedCircle" && cp == 0x25CC)
|| (p.name == "AL_DOTTED_CIRCLE" && cp == 0x25CC) =>
{
properties_trie.set_value(cp, property_index);
}
LineBreak::Quotation => {
if p.name == "QU_PI"
&& gc.get32(cp) == GeneralCategory::InitialPunctuation
{
properties_trie.set_value(cp, property_index);
}
if p.name == "QU_PF"
&& gc.get32(cp) == GeneralCategory::FinalPunctuation
{
properties_trie.set_value(cp, property_index);
}
}
LineBreak::BreakAfter
if p.name == "BA_EastAsian" && is_east_asian(eaw, cp) => {
properties_trie.set_value(cp, property_index);
}
LineBreak::ClosePunctuation
if p.name == "CL_EastAsian" && is_east_asian(eaw, cp) => {
properties_trie.set_value(cp, property_index);
}
LineBreak::CombiningMark
if p.name == "CM_EastAsian" && is_east_asian(eaw, cp) => {
properties_trie.set_value(cp, property_index);
}
LineBreak::ComplexContext
if p.name == "SA_MC_MN"
&& (gc.get32(cp) == GeneralCategory::NonspacingMark
|| gc.get32(cp) == GeneralCategory::SpacingMark) =>
{
properties_trie.set_value(cp, property_index);
}
LineBreak::EBase
if p.name == "EB_EastAsian" && is_east_asian(eaw, cp) => {
properties_trie.set_value(cp, property_index);
}
LineBreak::Exclamation
if p.name == "EX_EastAsian" && is_east_asian(eaw, cp) => {
properties_trie.set_value(cp, property_index);
}
LineBreak::Glue
if p.name == "GL_EastAsian" && is_east_asian(eaw, cp) => {
properties_trie.set_value(cp, property_index);
}
LineBreak::Inseparable
if p.name == "IN_EastAsian" && is_east_asian(eaw, cp) => {
properties_trie.set_value(cp, property_index);
}
LineBreak::Nonstarter
if p.name == "NS_EastAsian" && is_east_asian(eaw, cp) => {
properties_trie.set_value(cp, property_index);
}
LineBreak::Unknown
if p.name == "XX_ExtPict"
&& extended_pictographic.contains32(cp)
&& gc.get32(cp) == GeneralCategory::Unassigned =>
{
properties_trie.set_value(cp, property_index);
}
_ => {}
}
}
continue;
}
let prop = lb_name_to_enum
.get_loose(&p.name)
.expect("property name should be valid!");
for range in lb.iter_ranges_for_value(prop) {
properties_trie.set_range_value(range, property_index);
}
if p.name == "AL" {
let prop = lb_name_to_enum
.get_loose("SG")
.expect("property name should be valid!");
for range in lb.iter_ranges_for_value(prop) {
properties_trie.set_range_value(range, property_index);
}
}
continue;
}
}
let expected_properties: &[(&str, u8)] = if unicode_15_1_rules {
&[
("AI", RuleBreakData::LINE_PROPERTY_AI),
("AK", RuleBreakData::LINE_PROPERTY_AK),
(
"AL_DOTTED_CIRCLE",
RuleBreakData::LINE_PROPERTY_AL_DOTTED_CIRCLE,
),
("AL", RuleBreakData::LINE_PROPERTY_AL),
("AP", RuleBreakData::LINE_PROPERTY_AP),
("AS", RuleBreakData::LINE_PROPERTY_AS),
("B2", RuleBreakData::LINE_PROPERTY_B2),
("BA", RuleBreakData::LINE_PROPERTY_BA),
("BB", RuleBreakData::LINE_PROPERTY_BB),
("BK", RuleBreakData::LINE_PROPERTY_BK),
("CB", RuleBreakData::LINE_PROPERTY_CB),
("CJ", RuleBreakData::LINE_PROPERTY_CJ),
("CL", RuleBreakData::LINE_PROPERTY_CL),
("CM", RuleBreakData::LINE_PROPERTY_CM),
("CP", RuleBreakData::LINE_PROPERTY_CP),
("CR", RuleBreakData::LINE_PROPERTY_CR),
("EB", RuleBreakData::LINE_PROPERTY_EB),
("EM", RuleBreakData::LINE_PROPERTY_EM),
("EX", RuleBreakData::LINE_PROPERTY_EX),
("GL", RuleBreakData::LINE_PROPERTY_GL),
("H2", RuleBreakData::LINE_PROPERTY_H2),
("H3", RuleBreakData::LINE_PROPERTY_H3),
("HL", RuleBreakData::LINE_PROPERTY_HL),
("HY", RuleBreakData::LINE_PROPERTY_HY),
("ID_CN", RuleBreakData::LINE_PROPERTY_ID_CN),
("ID", RuleBreakData::LINE_PROPERTY_ID),
("IN", RuleBreakData::LINE_PROPERTY_IN),
("IS", RuleBreakData::LINE_PROPERTY_IS),
("JL", RuleBreakData::LINE_PROPERTY_JL),
("JT", RuleBreakData::LINE_PROPERTY_JT),
("JV", RuleBreakData::LINE_PROPERTY_JV),
("LF", RuleBreakData::LINE_PROPERTY_LF),
("NL", RuleBreakData::LINE_PROPERTY_NL),
("NS", RuleBreakData::LINE_PROPERTY_NS),
("NU", RuleBreakData::LINE_PROPERTY_NU),
("OP_EA", RuleBreakData::LINE_PROPERTY_OP_EA),
("OP_OP30", RuleBreakData::LINE_PROPERTY_OP_OP30),
("PO_EAW", RuleBreakData::LINE_PROPERTY_PO_EAW),
("PO", RuleBreakData::LINE_PROPERTY_PO),
("PR_EAW", RuleBreakData::LINE_PROPERTY_PR_EAW),
("PR", RuleBreakData::LINE_PROPERTY_PR),
("QU_PF", RuleBreakData::LINE_PROPERTY_QU_PF),
("QU_PI", RuleBreakData::LINE_PROPERTY_QU_PI),
("QU", RuleBreakData::LINE_PROPERTY_QU),
("RI", RuleBreakData::LINE_PROPERTY_RI),
("SP", RuleBreakData::LINE_PROPERTY_SP),
("SY", RuleBreakData::LINE_PROPERTY_SY),
("VF", RuleBreakData::LINE_PROPERTY_VF),
("VI", RuleBreakData::LINE_PROPERTY_VI),
("WJ", RuleBreakData::LINE_PROPERTY_WJ),
("XX", RuleBreakData::LINE_PROPERTY_XX),
("ZW", RuleBreakData::LINE_PROPERTY_ZW),
("ZWJ", RuleBreakData::LINE_PROPERTY_ZWJ),
]
} else {
&[
("AI", RuleBreakData::LINE_V3_PROPERTY_AI),
("AK", RuleBreakData::LINE_V3_PROPERTY_AK),
(
"AL_DottedCircle",
RuleBreakData::LINE_V3_PROPERTY_AL_DOTTED_CIRCLE,
),
("AL", RuleBreakData::LINE_V3_PROPERTY_AL),
("AP", RuleBreakData::LINE_V3_PROPERTY_AP),
("AS", RuleBreakData::LINE_V3_PROPERTY_AS),
("B2", RuleBreakData::LINE_V3_PROPERTY_B2),
("BA", RuleBreakData::LINE_V3_PROPERTY_BA),
("BA_EastAsian", RuleBreakData::LINE_V3_PROPERTY_BA_EASTASIAN),
("BB", RuleBreakData::LINE_V3_PROPERTY_BB),
("BK", RuleBreakData::LINE_V3_PROPERTY_BK),
("CB", RuleBreakData::LINE_V3_PROPERTY_CB),
("CJ", RuleBreakData::LINE_V3_PROPERTY_CJ),
("CL", RuleBreakData::LINE_V3_PROPERTY_CL),
("CL_EastAsian", RuleBreakData::LINE_V3_PROPERTY_CL_EASTASIAN),
("CM", RuleBreakData::LINE_V3_PROPERTY_CM),
("CM_EastAsian", RuleBreakData::LINE_V3_PROPERTY_CM_EASTASIAN),
("CP", RuleBreakData::LINE_V3_PROPERTY_CP),
("CR", RuleBreakData::LINE_V3_PROPERTY_CR),
("EB", RuleBreakData::LINE_V3_PROPERTY_EB),
("EB_EastAsian", RuleBreakData::LINE_V3_PROPERTY_EB_EASTASIAN),
("EM", RuleBreakData::LINE_V3_PROPERTY_EM),
("EX", RuleBreakData::LINE_V3_PROPERTY_EX),
("EX_EastAsian", RuleBreakData::LINE_V3_PROPERTY_EX_EASTASIAN),
("GL", RuleBreakData::LINE_V3_PROPERTY_GL),
("GL_EastAsian", RuleBreakData::LINE_V3_PROPERTY_GL_EASTASIAN),
("H2", RuleBreakData::LINE_V3_PROPERTY_H2),
("H3", RuleBreakData::LINE_V3_PROPERTY_H3),
("HL", RuleBreakData::LINE_V3_PROPERTY_HL),
("HY", RuleBreakData::LINE_V3_PROPERTY_HY),
("ID", RuleBreakData::LINE_V3_PROPERTY_ID),
("ID_CN", RuleBreakData::LINE_V3_PROPERTY_ID_CN),
("ID_EastAsian", RuleBreakData::LINE_V3_PROPERTY_ID_EASTASIAN),
("IN", RuleBreakData::LINE_V3_PROPERTY_IN),
("IN_EastAsian", RuleBreakData::LINE_V3_PROPERTY_IN_EASTASIAN),
("IS", RuleBreakData::LINE_V3_PROPERTY_IS),
("JL", RuleBreakData::LINE_V3_PROPERTY_JL),
("JT", RuleBreakData::LINE_V3_PROPERTY_JT),
("JV", RuleBreakData::LINE_V3_PROPERTY_JV),
("LF", RuleBreakData::LINE_V3_PROPERTY_LF),
("NL", RuleBreakData::LINE_V3_PROPERTY_NL),
("NS", RuleBreakData::LINE_V3_PROPERTY_NS),
("NS_EastAsian", RuleBreakData::LINE_V3_PROPERTY_NS_EASTASIAN),
("NU", RuleBreakData::LINE_V3_PROPERTY_NU),
("OP", RuleBreakData::LINE_V3_PROPERTY_OP),
("OP_EastAsian", RuleBreakData::LINE_V3_PROPERTY_OP_EASTASIAN),
("PO", RuleBreakData::LINE_V3_PROPERTY_PO),
("PO_EastAsian", RuleBreakData::LINE_V3_PROPERTY_PO_EASTASIAN),
("PR", RuleBreakData::LINE_V3_PROPERTY_PR),
("PR_EastAsian", RuleBreakData::LINE_V3_PROPERTY_PR_EASTASIAN),
("QU_PF", RuleBreakData::LINE_V3_PROPERTY_QU_PF),
("QU_PI", RuleBreakData::LINE_V3_PROPERTY_QU_PI),
("QU", RuleBreakData::LINE_V3_PROPERTY_QU),
("RI", RuleBreakData::LINE_V3_PROPERTY_RI),
("SP", RuleBreakData::LINE_V3_PROPERTY_SP),
("SY", RuleBreakData::LINE_V3_PROPERTY_SY),
("VF", RuleBreakData::LINE_V3_PROPERTY_VF),
("VI", RuleBreakData::LINE_V3_PROPERTY_VI),
("WJ", RuleBreakData::LINE_V3_PROPERTY_WJ),
("XX", RuleBreakData::LINE_V3_PROPERTY_XX),
("XX_ExtPict", RuleBreakData::LINE_V3_PROPERTY_XX_EXTPICT),
("ZW", RuleBreakData::LINE_V3_PROPERTY_ZW),
("ZWJ", RuleBreakData::LINE_V3_PROPERTY_ZWJ),
]
};
for &(name, value) in expected_properties {
assert_eq!(
get_index_from_name(&properties_names, name),
Some(value as usize),
"{name} {properties_names:?}"
);
}
}
_ => {
panic!("unknown built-in segmenter type");
}
}
properties_names.push("sot".to_string());
properties_names.push("eot".to_string());
let rule_size = properties_names.len() * properties_names.len();
let mut break_state_table = vec![None; rule_size];
for rule in &segmenter.rules {
let break_state = if let Some(state) = rule.break_state {
if state {
BreakState::Break
} else {
BreakState::Keep
}
} else {
BreakState::NoMatch
};
for l in &rule.left {
if l == "Any" {
for r in &rule.right {
if r == "Any" {
for item in break_state_table.iter_mut().take(rule_size) {
if item.is_none() {
*item = Some(break_state);
}
}
} else {
let right_index = get_index_from_name(&properties_names, r).unwrap();
for i in 0..simple_properties_count {
set_break_state(
&mut break_state_table,
properties_names.len(),
i,
right_index,
break_state,
);
}
}
}
continue;
}
let left_index =
get_index_from_name(&properties_names, l).expect("left property should be valid!");
for r in &rule.right {
if r == "Any" {
for i in 0..properties_names.len() {
set_break_state(
&mut break_state_table,
properties_names.len(),
left_index,
i,
break_state,
);
}
continue;
}
let right_index = get_index_from_name(&properties_names, r).unwrap();
if r != "eot"
&& break_state_table[left_index * properties_names.len() + right_index]
== Some(BreakState::NoMatch)
{
break_state_table[left_index * properties_names.len() + right_index] = None;
}
set_break_state(
&mut break_state_table,
properties_names.len(),
left_index,
right_index,
break_state,
);
for i in 0..properties_names.len() {
if left_index >= simple_properties_count {
set_break_state(
&mut break_state_table,
properties_names.len(),
left_index,
i,
BreakState::NoMatch,
);
}
}
}
}
}
for p in &segmenter.tables {
if let Some(left) = &p.left
&& let Some(right) = &p.right
{
let right_index = get_index_from_name(&properties_names, right).unwrap();
let left_index = get_index_from_name(&properties_names, left).unwrap();
let index = properties_names.iter().position(|n| n.eq(&p.name)).unwrap();
break_state_table[left_index * properties_names.len() + right_index] =
Some(if p.interm_break_state.is_some() {
BreakState::Intermediate(index.try_into().unwrap())
} else {
BreakState::Index(index.try_into().unwrap())
})
}
}
Ok(RuleBreakData {
property_table: properties_trie.build(),
break_state_table: break_state_table
.into_iter()
.map(|o| o.unwrap())
.collect(),
rule_status_table: if segmenter.segmenter_type == "word" {
segmenter
.tables
.iter()
.map(|p| match &*p.name {
"Numeric" => WordType::Number,
"ALetter" | "Hebrew_Letter" | "ExtendNumLet" | "Katakana" | "SA" => {
WordType::Letter
}
_ => WordType::None,
} as u8)
.collect()
} else {
Default::default()
},
property_count: properties_names.len().try_into().unwrap(),
last_codepoint_property: (simple_properties_count - 1).try_into().unwrap(),
sot_property: (properties_names.len() - 2).try_into().unwrap(),
eot_property: (properties_names.len() - 1).try_into().unwrap(),
complex_property: get_index_from_name(&properties_names, "SA")
.unwrap_or(127)
.try_into()
.unwrap(),
})
}
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
fn generate_rule_break_data_override(
_provider: &SourceDataProvider,
rules_file: &str,
trie_type: crate::TrieType,
) -> RuleBreakDataOverride<'static> {
use icu_codepointtrie_builder::CodePointTrieBuilder;
let segmenter =
toml::from_str::<SegmenterRuleTable>(rules_file).expect("The data should be valid!");
let mut properties_trie = CodePointTrieBuilder::new(0u8, 0, trie_type.into());
let mut properties_names = Vec::<String>::new();
properties_names.push("Unknown".to_string());
for p in &segmenter.tables {
let property_index = if !properties_names.contains(&p.name) {
properties_names.push(p.name.clone());
(properties_names.len() - 1).try_into().unwrap()
} else {
continue;
};
if p.left.is_none() && p.right.is_none() {
match &*segmenter.segmenter_type {
"sentence" if p.name == "STerm" => {
properties_trie.set_value(0x003b, property_index);
properties_trie.set_value(0x037e, property_index);
}
_ => {}
}
}
}
RuleBreakDataOverride {
property_table_override: properties_trie.build(),
}
}
macro_rules! implement {
($marker:ident, $rules:literal, $provider:expr) => {
impl DataProvider<$marker> for SourceDataProvider {
fn load(&self, req: DataRequest) -> Result<DataResponse<$marker>, DataError> {
#[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 segmentation rules",
)
.with_req($marker::INFO, req));
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
return {
self.check_req::<$marker>(req)?;
let data = generate_rule_break_data(
($provider)(self),
include_str!(concat!("../../data/segmenter/", $rules)),
self.trie_type(),
)?;
Ok(DataResponse {
metadata: Default::default(),
payload: DataPayload::from_owned(data),
})
};
}
}
impl crate::IterableDataProviderCached<$marker> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
Ok(HashSet::from_iter([Default::default()]))
}
}
}
}
macro_rules! implement_override {
($marker:ident, $rules:literal, [$($supported:expr),*]) => {
impl DataProvider<$marker> for SourceDataProvider {
fn load(&self, req: DataRequest) -> Result<DataResponse<$marker>, DataError> {
#[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 segmentation rules",
)
.with_req($marker::INFO, req));
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
return {
self.check_req::<$marker>(req)?;
let data = generate_rule_break_data_override(
self,
include_str!(concat!("../../data/segmenter/", $rules)),
self.trie_type(),
);
Ok(DataResponse {
metadata: Default::default(),
payload: DataPayload::from_owned(data),
})
};
}
}
impl crate::IterableDataProviderCached<$marker> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
const SUPPORTED: &[&str] = &[$($supported),*];
Ok(SUPPORTED
.iter()
.map(|l|DataIdentifierCow::from_locale(DataLocale::try_from_str(l).unwrap()))
.collect())
}
}
}
}
fn rscd_15_1() -> &'static SourceDataProvider {
static SINGLETON: OnceLock<SourceDataProvider> = OnceLock::new();
SINGLETON.get_or_init(|| {
let mut provider = SourceDataProvider::new_custom();
provider.rscd_paths = Some(std::sync::Arc::new(RscdCache::new_local(include_files!(
"../../data/segmenter/rscd15/";
"ucd/DerivedCoreProperties.txt",
"ucd/emoji/emoji-data.txt",
"ucd/extracted/DerivedEastAsianWidth.txt",
"ucd/extracted/DerivedGeneralCategory.txt",
"ucd/LineBreak.txt",
"ucd/PropertyAliases.txt",
"ucd/PropertyValueAliases.txt",
"ucd/PropList.txt",
))));
provider
})
}
implement!(SegmenterBreakLineV1, "line15.toml", |_| rscd_15_1());
#[cfg(feature = "unstable")]
implement!(SegmenterBreakLineV3, "line.toml", |s| s);
implement!(SegmenterBreakGraphemeClusterV1, "grapheme.toml", |s| s);
implement!(SegmenterBreakWordV1, "word.toml", |s| s);
implement!(SegmenterBreakSentenceV1, "sentence.toml", |s| s);
implement_override!(SegmenterBreakWordOverrideV1, "word.toml", []);
implement_override!(SegmenterBreakSentenceOverrideV1, "sentence.toml", ["el"]);
#[cfg(feature = "unstable")]
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
fn pri_555_sources() -> crate::source::AbstractFs {
include_files!(
"../../data/segmenter/pri555/";
"GraphemeClusterBreakStates.txt",
"GraphemeClusterBreakSymbols.txt",
"GraphemeClusterBreakTransitions.txt",
"LineBreakStates.txt",
"LineBreakSymbols.txt",
"LineBreakTransitions.txt",
"SentenceBreakStates.txt",
"SentenceBreakSymbols.txt",
"SentenceBreakTransitions.txt",
"WordBreakStates.txt",
"WordBreakSymbols.txt",
"WordBreakTransitions.txt",
)
}
#[cfg(feature = "unstable")]
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
fn pri555_cldr_json() -> &'static CldrCache {
static SINGLETON: OnceLock<CldrCache> = OnceLock::new();
SINGLETON.get_or_init(|| {
CldrCache::new(include_files!(
"../../data/segmenter/cldr-json/";
"cldr-segments-full/segments/el/tailorings.json",
"cldr-segments-full/segments/ja/tailorings.json",
"cldr-segments-full/segments/und/tailorings.json",
))
})
}
#[test]
#[ignore]
#[cfg(all(feature = "unstable", feature = "networking"))]
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
fn download() {
use std::fs::File;
use std::io::Write;
let data_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("data/segmenter/pri555");
for file in pri_555_sources().list("").unwrap() {
let target = data_root.join(&file);
std::fs::create_dir_all(target.parent().unwrap()).unwrap();
crlify::BufWriterWithLineEndingFix::new(File::create(&target).unwrap())
.write_all(
&crate::source::AbstractFs::new_from_url(format!(
"https://unicode.org/review/pri555/{}",
SourceDataProvider::TESTED_UNICODE_TAG
))
.read_to_buf(&file)
.unwrap(),
)
.unwrap();
}
}
#[cfg(feature = "unstable")]
type TailoredSegmenter = (
SegmenterStateMachine<'static>,
BTreeMap<DataIdentifierCow<'static>, SegmenterStateMachineOverride<'static>>,
u64,
);
#[cfg(feature = "unstable")]
#[derive(Debug, Default)]
pub(crate) struct NeoSegmenters {
line: Cache<TailoredSegmenter>,
word: Cache<TailoredSegmenter>,
sentence: Cache<TailoredSegmenter>,
grapheme_cluster: Cache<TailoredSegmenter>,
}
#[cfg(feature = "unstable")]
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
impl SourceDataProvider {
fn line_segmenter(&self) -> Result<&TailoredSegmenter, DataError> {
self.rscd()?
.segmenter_cache
.line
.get_or_init(|| {
self.build_segmenter(&pri_555_sources(), "LineBreak", |s| {
if s == "Mandatory" { 1 } else { 0 }
})
})
.as_ref()
.map_err(|&e| e)
}
fn word_segmenter(&self) -> Result<&TailoredSegmenter, DataError> {
self.rscd()?
.segmenter_cache
.word
.get_or_init(|| {
self.build_segmenter(&pri_555_sources(), "WordBreak", |s| match s {
"Letter" => WordType::Letter,
"Number" => WordType::Number,
_ => WordType::None,
} as u8)
})
.as_ref()
.map_err(|&e| e)
}
fn sentence_segmenter(&self) -> Result<&TailoredSegmenter, DataError> {
self.rscd()?
.segmenter_cache
.sentence
.get_or_init(|| {
self.build_segmenter(&pri_555_sources(), "SentenceBreak", |s| {
if s == "Nonterminated" { 1 } else { 0 }
})
})
.as_ref()
.map_err(|&e| e)
}
fn grapheme_cluster_segmenter(&self) -> Result<&TailoredSegmenter, DataError> {
self.rscd()?
.segmenter_cache
.grapheme_cluster
.get_or_init(|| {
self.build_segmenter(&pri_555_sources(), "GraphemeClusterBreak", |s| match s {
"" => 0,
s => unreachable!("{s}"),
})
})
.as_ref()
.map_err(|&e| e)
}
fn build_segmenter(
&self,
sources: &crate::source::AbstractFs,
prefix: &str,
status_lookup: fn(&str) -> u8,
) -> Result<TailoredSegmenter, DataError> {
let mut magic_symbols = BTreeMap::new();
let mut complex_symbols = BTreeMap::new();
let symbols = sources.read_to_string(&format!("{prefix}Symbols.txt"))?;
let symbols = symbols
.lines()
.map(|l| l.split('#').next().unwrap().trim())
.filter(|l| !l.is_empty())
.map(|line| {
let mut iter = line.split(';').map(str::trim);
let symbol = iter.next().unwrap();
let unicode_set = iter.next().unwrap();
if let Some(non_complex_equivalent) = iter.next()
&& !non_complex_equivalent.is_empty()
{
complex_symbols.insert(symbol, non_complex_equivalent);
}
let set = icu::properties::unicodeset_parse::parse_unstable(unicode_set, self)
.map_err(|e| {
DataError::custom("unicodeset parse")
.with_display_context(&e.fmt_with_source(unicode_set))
})?
.0;
for string in set.strings().iter() {
assert_eq!(magic_symbols.insert(String::from(string), symbol), None);
}
let set = set.code_points().clone();
Ok((symbol.to_owned(), set))
})
.collect::<Result<BTreeMap<_, _>, DataError>>()?;
let eot_symbol = magic_symbols.remove("eot").unwrap_or("eot").to_string();
let magic_symbols = magic_symbols;
let complex_symbols = complex_symbols;
let states = sources.read_to_string(&format!("{prefix}States.txt"))?;
let states = states
.lines()
.map(|l| l.split('#').next().unwrap().trim())
.filter(|l| !l.is_empty())
.map(|line| {
let mut iter = line.split(';');
let state = iter.next().unwrap().trim();
let accepting = iter.next().unwrap().trim();
let lookahead = iter.next().unwrap().trim();
let status = iter.next().unwrap().trim();
(
state,
(accepting, Some(lookahead).filter(|s| !s.is_empty()), status),
)
})
.collect::<BTreeMap<_, _>>();
let transitions = sources.read_to_string(&format!("{prefix}Transitions.txt"))?;
let transitions = transitions
.lines()
.map(|l| l.split('#').next().unwrap().trim())
.filter(|l| !l.is_empty())
.map(|line| {
let mut iter = line.split(';');
let state = iter.next().unwrap().trim();
let symbol = iter.next().unwrap().trim();
let next_state = iter.next().unwrap().trim();
((state, symbol), next_state)
})
.collect::<BTreeMap<_, _>>();
let lookaheads = states
.iter()
.flat_map(|(_, &(_, lookahead, _))| lookahead)
.collect::<BTreeSet<_>>();
let complex_languages = match prefix {
"LineBreak" => [
(ComplexScript::Myanmar, "[:sc=Myanmar:]&[:lb=SA:]"),
(ComplexScript::Khmer, "[:sc=Khmer:]&[:lb=SA:]"),
(ComplexScript::Lao, "[:sc=Lao:]&[:lb=SA:]"),
(ComplexScript::Thai, "[:sc=Thai:]&[:lb=SA:]"),
]
.as_slice(),
"WordBreak" => [
(ComplexScript::Myanmar, "[:sc=Myanmar:]&[:lb=SA:]"),
(
ComplexScript::ChineseOrJapanese,
"[[[:sc=Han:] [:sc=Hiragana:] [:wb=Katakana:] 가-힣] - [:lb=SA:]]",
),
(ComplexScript::Khmer, "[:sc=Khmer:]&[:lb=SA:]"),
(ComplexScript::Lao, "[:sc=Lao:]&[:lb=SA:]"),
(ComplexScript::Thai, "[:sc=Thai:]&[:lb=SA:]"),
]
.as_slice(),
_ => &[],
}
.iter()
.map(|&(l, set)| {
(
l,
icu::properties::unicodeset_parse::parse_unstable(set, self)
.unwrap()
.0
.code_points()
.clone(),
)
})
.collect::<Vec<_>>();
let mut tailorings = BTreeMap::new();
for locale in pri555_cldr_json().segments().list_locales()? {
let Some(ts) = pri555_cldr_json()
.segments()
.read_and_parse::<crate::cldr_serde::segmentation::Resource>(
&locale,
"tailorings.json",
)?
.segments
.segmentations
.0
.get(prefix)
else {
continue;
};
for (keywords, lines) in ts.iter().map(|(k, v)| (&k.extensions.unicode.keywords, v)) {
let mut overrides = BTreeMap::<String, BTreeSet<char>>::new();
for line in lines {
let mut iter = line.split(';');
let unicode_set = iter.next().unwrap().trim();
let target = iter.next().unwrap().trim();
let set = icu::properties::unicodeset_parse::parse_unstable(unicode_set, self)
.map_err(|e| {
DataError::custom("unicodeset parse")
.with_display_context(&e.fmt_with_source(unicode_set))
})?
.0;
let target = icu::properties::unicodeset_parse::parse_unstable(target, self)
.map_err(|e| {
DataError::custom("unicodeset parse")
.with_display_context(&e.fmt_with_source(unicode_set))
})?
.0;
let target_symbol = if target.has_strings() {
magic_symbols[target.strings().iter().next().unwrap()]
} else {
let target = target.code_points().iter_chars().next().unwrap();
symbols
.iter()
.find(|(_, set)| set.contains(target))
.unwrap()
.0
.as_str()
};
for c in set.code_points().iter_chars() {
overrides
.entry(target_symbol.to_owned())
.or_default()
.insert(c);
}
}
let id = if prefix == "LineBreak" {
let x;
DataIdentifierCow::from_marker_attributes_owned(
DataMarkerAttributes::try_from_string(format!(
"{}{}{}",
if locale.is_unknown() {
""
} else {
x = locale.to_string();
&x
},
if locale.is_unknown() || keywords.is_empty() {
""
} else {
"-"
},
keywords
.get(&key!("lb"))
.or_else(|| keywords.get(&key!("lw")))
.map(|v| v.to_string())
.unwrap_or_default()
))
.unwrap(),
)
} else {
DataIdentifierCow::from_owned(
DataMarkerAttributes::try_from_string(
keywords
.get(&key!("lb"))
.or_else(|| keywords.get(&key!("lw")))
.map(|v| v.to_string())
.unwrap_or_default(),
)
.unwrap(),
locale,
)
};
tailorings.insert(
id,
overrides
.into_iter()
.map(|(k, v)| {
let mut builder = CodePointInversionListBuilder::new();
v.into_iter().for_each(|c| builder.add_char(c));
(k, builder.build())
})
.collect::<BTreeMap<_, CodePointInversionList>>(),
);
}
}
let mut symbols = symbols;
let mut transitions = transitions;
let mut pseudo_symbol_map = BTreeMap::<String, (String, ComplexScript)>::new();
for (&symbol, &non_complex_symbol) in &complex_symbols {
let set = symbols.get(symbol).unwrap().clone();
let mut set_builder = CodePointInversionListBuilder::new();
set_builder.add_set(&set);
for &(language, ref language_set) in &complex_languages {
if language_set
.iter_ranges()
.all(|mut range| range.all(|c| !set.contains32(c)))
{
continue;
}
set_builder.remove_set(language_set);
let mut intersection = CodePointInversionListBuilder::new();
intersection.add_set(language_set);
for r in set.iter_ranges_complemented() {
intersection.remove_range32(r);
}
let intersection_symbol = format!("{symbol}_{language:?}");
pseudo_symbol_map.insert(
intersection_symbol.clone(),
(non_complex_symbol.into(), language),
);
symbols.insert(intersection_symbol, intersection.build());
}
if symbol != non_complex_symbol {
let symbol_transitions = transitions
.iter()
.filter(|&(&(_, s), _)| s == symbol)
.map(|(&(before, _), &after)| (before, after))
.collect::<BTreeSet<_>>();
let non_complex_symbol_transitions = transitions
.iter()
.filter(|&(&(_, s), _)| s == non_complex_symbol)
.map(|(&(before, _), &after)| (before, after))
.collect::<BTreeSet<_>>();
if symbol_transitions == non_complex_symbol_transitions {
let non_complex_set = symbols.get_mut(non_complex_symbol).unwrap();
let mut non_complex_set_builder = CodePointInversionListBuilder::new();
non_complex_set_builder.add_set(non_complex_set);
non_complex_set_builder.add_set(&set_builder.build());
*non_complex_set = non_complex_set_builder.build();
symbols.remove(symbol);
transitions.retain(|&(_, s), _| s != symbol);
} else {
log::warn!(
"{symbol}/{non_complex_symbol}: {:?} != {:?}",
symbol_transitions
.difference(&non_complex_symbol_transitions)
.collect::<Vec<_>>(),
non_complex_symbol_transitions
.difference(&symbol_transitions)
.collect::<Vec<_>>()
);
}
}
}
for (tailoring, overrides) in tailorings.clone() {
for (rule, set) in overrides {
for (symbol, set2) in symbols.clone().into_iter().collect::<Vec<_>>() {
if set.iter_chars().any(|c| set2.contains(c)) {
let pseudo_symbol = format!("{symbol}_{tailoring}_{rule}");
symbols.insert(pseudo_symbol.clone(), {
let mut builder = CodePointInversionListBuilder::new();
builder.add_set(&set);
for r in set2.iter_ranges_complemented() {
builder.remove_range32(r);
}
builder.build()
});
pseudo_symbol_map.insert(pseudo_symbol, {
let mut s = &*symbol;
let mut l = ComplexScript::None;
while let Some(&(ref x, y)) = pseudo_symbol_map.get(s) {
s = x.as_str();
l = y;
}
(s.to_string(), l)
});
symbols.insert(symbol, {
let mut builder = CodePointInversionListBuilder::new();
builder.add_set(&set2);
builder.remove_set(&set);
builder.build()
});
}
}
}
}
let mut unused_pseudo_symbols = pseudo_symbol_map.keys().cloned().collect::<BTreeSet<_>>();
let tailorings = tailorings
.into_iter()
.map(|(tailoring, overrides)| {
let mut tailored_pseudo_symbol_map = BTreeMap::new();
for (target_symbol, set) in overrides {
let target_language = ComplexScript::None;
for c in set.iter_chars() {
let pseudo_symbol =
symbols.iter().find(|(_, set)| set.contains(c)).unwrap().0;
unused_pseudo_symbols.remove(pseudo_symbol);
tailored_pseudo_symbol_map.insert(
pseudo_symbol.to_owned(),
(target_symbol.clone(), target_language),
);
}
}
(tailoring, tailored_pseudo_symbol_map)
})
.collect::<BTreeMap<_, _>>();
for unused in unused_pseudo_symbols {
if pseudo_symbol_map.get(&unused).unwrap().1 != ComplexScript::None {
continue;
}
let resolved = pseudo_symbol_map.remove(&unused).unwrap().0;
let set = symbols.remove(&unused).unwrap();
let resolved_set = symbols.get_mut(&resolved).unwrap();
let mut builder = CodePointInversionListBuilder::new();
builder.add_set(resolved_set);
builder.add_set(&set);
*resolved_set = builder.build();
}
symbols.retain(|n, set| {
if pseudo_symbol_map.contains_key(n) {
return true;
}
if !set.is_empty() {
return true;
}
if pseudo_symbol_map
.values()
.any(|(root_symbol, _)| root_symbol == n)
|| tailorings.values().any(|tailored_pseudo_symbol_map| {
tailored_pseudo_symbol_map
.values()
.any(|(target_symbol, _)| target_symbol == n)
})
{
return true;
}
transitions.retain(|&(_, m), _| m != n);
false
});
let symbols = symbols;
let pseudo_symbol_map = pseudo_symbol_map;
let hash = {
use core::hash::{Hash, Hasher};
let mut hash = twox_hash::XxHash64::with_seed(0);
symbols.hash(&mut hash);
pseudo_symbol_map.hash(&mut hash);
states.hash(&mut hash);
transitions.hash(&mut hash);
hash.finish()
};
let symbol_lookup = symbols
.keys()
.filter(|&s| s != &eot_symbol && !pseudo_symbol_map.contains_key(s))
.enumerate()
.map(|(i, symbol)| (symbol.as_str(), Symbol::try_from(i + 1).unwrap()))
.chain([(eot_symbol.as_str(), SegmenterStateMachine::EOT_SYMBOL)])
.collect::<BTreeMap<_, _>>();
let pseudo_symbol_shift = symbol_lookup.values().copied().max().unwrap() + 1;
let pseudo_symbol_lookup = pseudo_symbol_map
.keys()
.enumerate()
.map(|(i, k)| {
(
k.as_str(),
Symbol::try_from(i + usize::from(pseudo_symbol_shift)).unwrap(),
)
})
.collect::<BTreeMap<_, _>>();
assert!(states.len() < usize::from(State::MAX) - 2);
let state_lookup = core::iter::once("START")
.chain(states.keys().filter(|&&s| s != "START").copied())
.enumerate()
.map(|(i, state)| (state, State::try_from(i).unwrap()))
.collect::<BTreeMap<_, _>>();
assert!(lookaheads.len() < 0b11111);
let lookahead_lookup = lookaheads
.iter()
.enumerate()
.map(|(i, lookahead)| (*lookahead, Lookahead::try_from(i).unwrap()))
.collect::<BTreeMap<_, _>>();
use icu::collections::codepointinvlist::CodePointInversionListBuilder;
use icu::collections::codepointtrie::TrieType;
use icu_codepointtrie_builder::CodePointTrieBuilder;
let mut builder = CodePointTrieBuilder::new(0, 0, TrieType::Fast);
let mut missing_codepoints = CodePointInversionListBuilder::new();
missing_codepoints.add_set(&CodePointInversionList::all());
for (symbol, set) in &symbols {
for range in set.iter_ranges() {
missing_codepoints.remove_range32(range.clone());
builder.set_range_value(
range.clone(),
symbol_lookup
.get(symbol.as_str())
.or_else(|| pseudo_symbol_lookup.get(symbol.as_str()))
.copied()
.unwrap(),
);
}
}
let missing_codepoints = missing_codepoints.build();
assert!(missing_codepoints.is_empty(), "{missing_codepoints:?}");
let symbols = builder.build();
let states = states
.iter()
.map(|(&state, &(accepting, lookahead, status))| {
let status = status_lookup(status);
assert!(status < 0b111);
let acceptance = match accepting {
"Yes" => Acceptance::Accept(status),
"No" => Acceptance::Continue,
l => Acceptance::Conditional(lookahead_lookup[l], status),
};
(
state_lookup[state],
(acceptance, lookahead.as_ref().map(|l| lookahead_lookup[l])),
)
})
.collect::<BTreeMap<_, _>>()
.into_values()
.collect();
let transitions = transitions
.iter()
.map(|(&(state, symbol), &next_state)| {
(
usize::from(state_lookup[state])
+ state_lookup.len() * usize::from(symbol_lookup[symbol]),
*state_lookup.get(next_state).expect(next_state),
)
})
.collect::<BTreeMap<_, _>>();
let transitions = (0..=*transitions.last_key_value().unwrap().0)
.map(|i| {
transitions
.get(&i)
.copied()
.unwrap_or(SegmenterStateMachine::TRASH_STATE)
})
.collect();
let build_pseudo_map = |map: &BTreeMap<String, (String, ComplexScript)>| {
map.iter()
.map(|(pseudo_symbol, &(ref symbol, complex_script))| {
(
pseudo_symbol_lookup[pseudo_symbol.as_str()],
(symbol_lookup[symbol.as_str()], complex_script),
)
})
.collect::<BTreeMap<_, _>>()
.into_values()
.collect()
};
let tailorings = tailorings
.into_iter()
.map(|(tailoring, tailored_pseudo_symbol_map)| {
(
tailoring,
SegmenterStateMachineOverride {
pseudo_symbol_map: build_pseudo_map(
&pseudo_symbol_map
.clone()
.into_iter()
.chain(tailored_pseudo_symbol_map)
.collect(),
),
},
)
})
.collect();
Ok((
SegmenterStateMachine {
transitions,
symbols,
states,
num_lookaheads: lookahead_lookup.len(),
pseudo_symbol_shift,
pseudo_symbol_map: build_pseudo_map(&pseudo_symbol_map),
},
tailorings,
hash,
))
}
}
#[cfg(feature = "unstable")]
impl DataProvider<SegmenterBreakLineV2> for SourceDataProvider {
fn load(&self, req: DataRequest) -> Result<DataResponse<SegmenterBreakLineV2>, DataError> {
self.check_req::<SegmenterBreakLineV2>(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 segmentation rules",
)
.with_req(SegmenterBreakLineV2::INFO, req));
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
Ok(DataResponse {
metadata: DataResponseMetadata::default().with_checksum(self.line_segmenter()?.2),
payload: DataPayload::from_owned(self.line_segmenter()?.0.clone()),
})
}
}
#[cfg(feature = "unstable")]
impl DataProvider<SegmenterBreakWordV2> for SourceDataProvider {
fn load(&self, req: DataRequest) -> Result<DataResponse<SegmenterBreakWordV2>, DataError> {
self.check_req::<SegmenterBreakWordV2>(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 segmentation rules",
)
.with_req(SegmenterBreakWordV2::INFO, req));
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
Ok(DataResponse {
metadata: DataResponseMetadata::default().with_checksum(self.word_segmenter()?.2),
payload: DataPayload::from_owned(self.word_segmenter()?.0.clone()),
})
}
}
#[cfg(feature = "unstable")]
impl DataProvider<SegmenterBreakSentenceV2> for SourceDataProvider {
fn load(&self, req: DataRequest) -> Result<DataResponse<SegmenterBreakSentenceV2>, DataError> {
self.check_req::<SegmenterBreakSentenceV2>(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 segmentation rules",
)
.with_req(SegmenterBreakSentenceV2::INFO, req));
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
Ok(DataResponse {
metadata: DataResponseMetadata::default().with_checksum(self.sentence_segmenter()?.2),
payload: DataPayload::from_owned(self.sentence_segmenter()?.0.clone()),
})
}
}
#[cfg(feature = "unstable")]
impl DataProvider<SegmenterBreakGraphemeClusterV2> for SourceDataProvider {
fn load(
&self,
req: DataRequest,
) -> Result<DataResponse<SegmenterBreakGraphemeClusterV2>, DataError> {
self.check_req::<SegmenterBreakGraphemeClusterV2>(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 segmentation rules",
)
.with_req(SegmenterBreakGraphemeClusterV2::INFO, req));
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
Ok(DataResponse {
metadata: DataResponseMetadata::default()
.with_checksum(self.grapheme_cluster_segmenter()?.2),
payload: DataPayload::from_owned(self.grapheme_cluster_segmenter()?.0.clone()),
})
}
}
#[cfg(feature = "unstable")]
impl IterableDataProviderCached<SegmenterBreakLineV2> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
Ok([Default::default()].into_iter().collect())
}
}
#[cfg(feature = "unstable")]
impl IterableDataProviderCached<SegmenterBreakSentenceV2> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
Ok([Default::default()].into_iter().collect())
}
}
#[cfg(feature = "unstable")]
impl IterableDataProviderCached<SegmenterBreakWordV2> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
Ok([Default::default()].into_iter().collect())
}
}
#[cfg(feature = "unstable")]
impl IterableDataProviderCached<SegmenterBreakGraphemeClusterV2> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
Ok([Default::default()].into_iter().collect())
}
}
#[cfg(feature = "unstable")]
impl DataProvider<SegmenterBreakLineOverrideV2> for SourceDataProvider {
fn load(
&self,
req: DataRequest,
) -> Result<DataResponse<SegmenterBreakLineOverrideV2>, DataError> {
self.check_req::<SegmenterBreakLineOverrideV2>(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 segmentation rules",
)
.with_req(SegmenterBreakLineOverrideV2::INFO, req));
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
Ok(DataResponse {
metadata: DataResponseMetadata::default().with_checksum(self.line_segmenter()?.2),
payload: DataPayload::from_owned(
self.line_segmenter()?
.1
.get(&req.id.as_cow())
.ok_or_else(|| {
DataErrorKind::IdentifierNotFound
.with_req(SegmenterBreakLineOverrideV2::INFO, req)
})?
.clone(),
),
})
}
}
#[cfg(feature = "unstable")]
impl IterableDataProviderCached<SegmenterBreakLineOverrideV2> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
#[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 segmentation rules",
)
.with_marker(SegmenterBreakLineOverrideV2::INFO));
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
Ok(self.line_segmenter()?.1.keys().cloned().collect())
}
}
#[cfg(feature = "unstable")]
impl DataProvider<SegmenterBreakSentenceOverrideV2> for SourceDataProvider {
fn load(
&self,
req: DataRequest,
) -> Result<DataResponse<SegmenterBreakSentenceOverrideV2>, DataError> {
self.check_req::<SegmenterBreakSentenceOverrideV2>(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 segmentation rules",
)
.with_req(SegmenterBreakSentenceOverrideV2::INFO, req));
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
Ok(DataResponse {
metadata: DataResponseMetadata::default().with_checksum(self.sentence_segmenter()?.2),
payload: DataPayload::from_owned(
self.sentence_segmenter()?
.1
.get(&req.id.as_cow())
.ok_or_else(|| {
DataErrorKind::IdentifierNotFound
.with_req(SegmenterBreakSentenceOverrideV2::INFO, req)
})?
.clone(),
),
})
}
}
#[cfg(feature = "unstable")]
impl IterableDataProviderCached<SegmenterBreakSentenceOverrideV2> for SourceDataProvider {
fn iter_ids_cached(&self) -> Result<HashSet<DataIdentifierCow<'static>>, DataError> {
#[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 segmentation rules",
)
.with_marker(SegmenterBreakSentenceOverrideV2::INFO));
#[cfg(any(feature = "use_wasm", feature = "use_icu4c"))]
Ok(self.sentence_segmenter()?.1.keys().cloned().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_grapheme_cluster_data() {
let provider = SourceDataProvider::new_testing();
let response: DataResponse<SegmenterBreakGraphemeClusterV1> = provider
.load(Default::default())
.expect("Loading should succeed!");
assert_eq!(
response.payload.get().complex_property,
127,
"Grapheme cluster data doesn't handle SA"
);
}
#[test]
fn load_line_data_v1() {
let provider = SourceDataProvider::new_testing();
let response: DataResponse<SegmenterBreakLineV1> = provider
.load(Default::default())
.expect("Loading should succeed!");
let data = response.payload.get();
const CM: u8 = 14;
const XX: u8 = 52;
const ID: u8 = 25;
assert_eq!(data.property_table.get32(0x20000), ID);
assert_eq!(data.property_table.get32(0x3fffd), ID);
assert_eq!(data.property_table.get32(0xd0000), XX);
assert_eq!(data.property_table.get32(0xe0001), CM);
assert_eq!(data.property_table.get32(0xe0020), CM);
}
#[test]
fn load_line_data() {
let provider = SourceDataProvider::new_testing();
let response: DataResponse<SegmenterBreakLineV3> = provider
.load(Default::default())
.expect("Loading should succeed!");
let data = response.payload.get();
const CM: u8 = 18;
const XX: u8 = 65;
const ID: u8 = 36;
assert_eq!(data.property_table.get32(0x20000), ID);
assert_eq!(data.property_table.get32(0x3fffd), ID);
assert_eq!(data.property_table.get32(0xd0000), XX);
assert_eq!(data.property_table.get32(0xe0001), CM);
assert_eq!(data.property_table.get32(0xe0020), CM);
}
#[test]
#[should_panic]
fn missing_locale_data() {
let provider = SourceDataProvider::new_testing();
let response: DataResponse<SegmenterBreakSentenceOverrideV1> = provider
.load(Default::default())
.expect("Loading should succeed!");
response.payload.get();
}
}