use std::collections::{HashMap, HashSet};
use crate::generated::enums::{Alias, Condition, WrittenUnit};
use crate::generated::mng_normalize;
use crate::generated::{mch, mng, sib, tod};
use crate::normalize::NormalizeTable;
use crate::rules::{self, Rule};
use crate::tables::{Fvs, Letter, Locale, LocaleData, ParticleSym, Position, Variant};
use crate::token::{assign_positions, tokenize, Token, TokenKind};
use crate::unicode::check_word_chars;
use crate::Error;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct TokenDetail {
pub cp: char,
pub alias: Option<Alias>,
pub position: Position,
pub fvs: Option<Fvs>,
pub condition: Option<Condition>,
pub written: Vec<WrittenUnit>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ConditionChange {
pub token: usize,
pub before: Option<Condition>,
pub after: Option<Condition>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct RuleTransition {
pub rule: &'static str,
pub changes: Vec<ConditionChange>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ShapeTrace {
pub positions: Vec<Position>,
pub transitions: Vec<RuleTransition>,
pub final_conditions: Vec<Option<Condition>>,
pub written_by_token: Vec<Vec<WrittenUnit>>,
pub shape: Vec<WrittenUnit>,
}
pub struct Shaper {
locale: Locale,
letters: HashMap<u32, &'static Letter>,
variants: HashMap<(u32, Position, Option<Fvs>), &'static Variant>,
defaults: HashMap<(u32, Position), &'static Variant>,
vowels: HashSet<Alias>,
consonants: HashSet<Alias>,
masculine: HashSet<Alias>,
feminine: HashSet<Alias>,
neuter: HashSet<Alias>,
particles: HashMap<&'static [ParticleSym], &'static [usize]>,
rules: &'static [Rule],
pub(crate) normalize: Option<NormalizeTable>,
}
const _: () = {
fn assert_send_sync<T: Send + Sync>() {}
let _ = assert_send_sync::<Shaper>;
};
impl Default for Shaper {
fn default() -> Shaper {
Shaper::new(Locale::Mng)
}
}
impl std::fmt::Debug for Shaper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Shaper")
.field("locale", &self.locale)
.field("letters", &self.letters.len())
.field("variants", &self.variants.len())
.field("rules", &self.rules.len())
.finish_non_exhaustive()
}
}
impl Shaper {
pub fn new(locale: Locale) -> Shaper {
let data: &'static LocaleData = match locale {
Locale::Mng => &mng::DATA,
Locale::Tod => &tod::DATA,
Locale::Sib => &sib::DATA,
Locale::Mch => &mch::DATA,
};
let mut letters = HashMap::new();
let mut variants = HashMap::new();
let mut defaults = HashMap::new();
for letter in data.letters {
letters.insert(letter.cp, letter);
for variant in letter.variants {
variants.insert((letter.cp, variant.position, variant.fvs), variant);
if variant.default {
defaults.insert((letter.cp, variant.position), variant);
}
}
}
let set = |aliases: &'static [Alias]| aliases.iter().copied().collect::<HashSet<Alias>>();
Shaper {
locale,
letters,
variants,
defaults,
vowels: set(data.categories.vowel),
consonants: set(data.categories.consonant),
masculine: set(data.categories.vowel_masculine),
feminine: set(data.categories.vowel_feminine),
neuter: set(data.categories.vowel_neuter),
particles: data
.particles
.iter()
.map(|particle| (particle.key, particle.indices))
.collect(),
rules: rules::rules_for(locale),
normalize: match locale {
Locale::Mng => Some(NormalizeTable::new(&mng_normalize::DATA)),
Locale::Tod | Locale::Sib | Locale::Mch => None,
},
}
}
#[cfg(any(test, feature = "testing"))]
#[doc(hidden)]
pub fn with_empty_normalize_table(locale: Locale) -> Shaper {
let mut shaper = Shaper::new(locale);
let version = shaper
.normalize
.as_ref()
.map_or("mng-canonical/1", |table| table.canonical_version);
shaper.normalize = Some(NormalizeTable::empty(version));
shaper
}
pub fn locale(&self) -> Locale {
self.locale
}
pub fn rule_names(&self) -> Vec<&'static str> {
self.rules.iter().map(|rule| rule.name).collect()
}
pub(crate) fn alias_of(&self, cp: u32) -> Option<Alias> {
self.letters.get(&cp).map(|letter| letter.alias)
}
pub(crate) fn tokenize(&self, text: &str) -> Vec<Token> {
tokenize(text, |cp| self.alias_of(cp))
}
pub(crate) fn particle(&self, key: &[ParticleSym]) -> Option<&'static [usize]> {
self.particles.get(key).copied()
}
fn condition_fvs(
&self,
cp: u32,
position: Position,
condition: Condition,
) -> Option<Option<Fvs>> {
self.letters
.get(&cp)?
.variants
.iter()
.find(|variant| variant.position == position && variant.conditions.contains(&condition))
.map(|variant| variant.fvs)
}
pub(crate) fn resolve_written(&self, token: &mut Token) {
if token.written.is_some() {
return;
}
if !token.is_letter() {
token.written = Some(&[]);
return;
}
let mut written: Option<&'static [WrittenUnit]> = None;
for &fvs in &token.fvs {
if let Some(variant) = self.variants.get(&(token.cp, token.position, Some(fvs))) {
written = Some(variant.written);
break;
}
}
if written.is_none() {
if let Some(condition) = token.condition {
if let Some(fvs) = self.condition_fvs(token.cp, token.position, condition) {
written = self
.variants
.get(&(token.cp, token.position, fvs))
.map(|variant| variant.written);
}
}
}
if written.is_none() {
written = self
.defaults
.get(&(token.cp, token.position))
.map(|variant| variant.written);
}
token.written = Some(written.unwrap_or(&[]));
}
pub(crate) fn is_vowel(&self, token: &Token) -> bool {
token.is_letter()
&& token
.alias
.is_some_and(|alias| self.vowels.contains(&alias))
}
pub(crate) fn is_consonant(&self, token: &Token) -> bool {
token.is_letter()
&& token
.alias
.is_some_and(|alias| self.consonants.contains(&alias))
}
pub(crate) fn is_masc_vowel(&self, token: &Token) -> bool {
token
.alias
.is_some_and(|alias| self.masculine.contains(&alias))
}
pub(crate) fn is_fem_vowel(&self, token: &Token) -> bool {
token
.alias
.is_some_and(|alias| self.feminine.contains(&alias))
}
pub(crate) fn is_neut_vowel(&self, token: &Token) -> bool {
token
.alias
.is_some_and(|alias| self.neuter.contains(&alias))
}
pub(crate) fn masc_marker_reaches_g_h(&self, tokens: &[Token], idx: usize) -> bool {
let mut j = idx;
while j > 0 {
j -= 1;
let token = &tokens[j];
if !token.is_letter() {
if token.is_nirugu() {
continue;
}
break; }
if self.is_fem_vowel(token) {
break;
}
if self.is_masc_vowel(token)
&& matches!(token.position, Position::Init | Position::Medi)
{
return true;
}
}
if !matches!(tokens[idx].position, Position::Init | Position::Medi) {
return false;
}
let mut j = idx;
while j > 0 {
j -= 1;
let token = &tokens[j];
if !token.is_letter() {
if token.is_nirugu() {
continue;
}
break;
}
if self.is_fem_vowel(token) {
return false;
}
}
let mut j = idx + 1;
while j < tokens.len() {
let next = &tokens[j];
if !next.is_letter() {
if next.is_nirugu() {
j += 1;
continue;
}
return false;
}
if self.is_masc_vowel(next) {
return true;
}
if self.is_fem_vowel(next) {
return false;
}
if matches!(next.position, Position::Init | Position::Medi) {
j += 1;
continue;
}
let mut k = j + 1;
while k < tokens.len() && tokens[k].is_nirugu() {
k += 1;
}
return next.position == Position::Fina
&& k < tokens.len()
&& tokens[k].is_mvs()
&& k + 1 < tokens.len()
&& tokens[k + 1].is_letter()
&& tokens[k + 1].alias == Some(Alias::A)
&& tokens[k + 1].position == Position::Isol;
}
false
}
fn prepare(&self, text: &str) -> Result<Vec<Token>, Error> {
check_word_chars(text)?;
let mut tokens = self.tokenize(text);
assign_positions(&mut tokens);
Ok(tokens)
}
fn resolve_all(&self, tokens: &mut [Token]) {
for token in tokens {
self.resolve_written(token);
}
}
fn run_pipeline(&self, text: &str) -> Result<Vec<Token>, Error> {
let mut tokens = self.prepare(text)?;
rules::run_rules(self.rules, &mut tokens, self);
self.resolve_all(&mut tokens);
Ok(tokens)
}
pub fn shape(&self, text: &str) -> Result<Vec<WrittenUnit>, Error> {
Ok(flatten(&self.run_pipeline(text)?))
}
pub fn shape_str(&self, text: &str) -> Result<String, Error> {
let units = self.shape(text)?;
let mut out = String::new();
for unit in units {
if !out.is_empty() {
out.push('+');
}
out.push_str(unit.as_str());
}
Ok(out)
}
pub fn same_shape(&self, a: &str, b: &str) -> Result<bool, Error> {
Ok(self.shape(a)? == self.shape(b)?)
}
pub fn shape_detailed(&self, text: &str) -> Result<Vec<TokenDetail>, Error> {
let tokens = self.run_pipeline(text)?;
Ok(tokens
.iter()
.map(|token| TokenDetail {
cp: char::from_u32(token.cp).expect("token code points are scalar values"),
alias: token.alias,
position: token.position,
fvs: token.first_fvs(),
condition: token.condition,
written: token
.written
.map(<[WrittenUnit]>::to_vec)
.unwrap_or_default(),
})
.collect())
}
pub fn trace(&self, text: &str) -> Result<ShapeTrace, Error> {
let mut tokens = self.prepare(text)?;
let mut transitions = Vec::new();
for rule in self.rules {
let before: Vec<Option<Condition>> =
tokens.iter().map(|token| token.condition).collect();
(rule.apply)(&mut tokens, self);
let changes: Vec<ConditionChange> = before
.iter()
.zip(&tokens)
.enumerate()
.filter(|(_, (old, token))| **old != token.condition)
.map(|(index, (old, token))| ConditionChange {
token: index,
before: *old,
after: token.condition,
})
.collect();
if !changes.is_empty() {
transitions.push(RuleTransition {
rule: rule.name,
changes,
});
}
}
self.resolve_all(&mut tokens);
Ok(ShapeTrace {
positions: tokens.iter().map(|token| token.position).collect(),
transitions,
final_conditions: tokens.iter().map(|token| token.condition).collect(),
written_by_token: tokens
.iter()
.map(|token| {
token
.written
.map(<[WrittenUnit]>::to_vec)
.unwrap_or_default()
})
.collect(),
shape: flatten(&tokens),
})
}
}
pub(crate) fn flatten(tokens: &[Token]) -> Vec<WrittenUnit> {
let mut shape = Vec::with_capacity(tokens.len() * 2);
for token in tokens {
match token.kind {
TokenKind::Mvs => shape.push(WrittenUnit::Mvs),
TokenKind::Nirugu => shape.push(WrittenUnit::Nirugu),
TokenKind::Zwj => shape.push(WrittenUnit::Zwj),
TokenKind::Letter => {
if let Some(written) = token.written {
shape.extend_from_slice(written);
}
}
}
}
shape
}
pub(crate) fn prev_letter(tokens: &[Token], index: usize) -> Option<usize> {
(0..index).rev().find(|&j| tokens[j].is_letter())
}
pub(crate) fn next_letter(tokens: &[Token], index: usize) -> Option<usize> {
(index + 1..tokens.len()).find(|&j| tokens[j].is_letter())
}
pub(crate) fn prev_tok(_tokens: &[Token], index: usize) -> Option<usize> {
if index > 0 {
Some(index - 1)
} else {
None
}
}
pub(crate) fn next_tok(tokens: &[Token], index: usize) -> Option<usize> {
if index + 1 < tokens.len() {
Some(index + 1)
} else {
None
}
}
pub(crate) fn prev_adjacent_letter(tokens: &[Token], index: usize) -> Option<usize> {
let mut j = index;
while j > 0 {
j -= 1;
let token = &tokens[j];
if token.is_mvs() {
return None;
}
if token.is_letter() {
return Some(j);
}
if token.is_nirugu() {
continue;
}
return None;
}
None
}
pub(crate) fn next_adjacent_letter(tokens: &[Token], index: usize) -> Option<usize> {
let mut j = index + 1;
while j < tokens.len() {
let token = &tokens[j];
if token.is_mvs() {
return None;
}
if token.is_letter() {
return Some(j);
}
if token.is_nirugu() {
j += 1;
continue;
}
return None;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_forms_without_rules() {
let shaper = Shaper::new(Locale::Mng);
assert_eq!(
shaper.shape("\u{1820}").unwrap(),
vec![WrittenUnit::A, WrittenUnit::A]
);
assert_eq!(
shaper.shape("\u{1820}\u{180B}").unwrap(),
vec![WrittenUnit::A]
);
assert_eq!(shaper.shape("\u{180E}").unwrap(), vec![WrittenUnit::Mvs]);
assert_eq!(
shaper.shape("\u{180A}\u{1823}").unwrap(),
vec![WrittenUnit::Nirugu, WrittenUnit::U]
);
assert_eq!(
shaper.shape("\u{200D}\u{1833}").unwrap(),
vec![WrittenUnit::Zwj, WrittenUnit::Dd]
);
assert_eq!(shaper.shape("").unwrap(), Vec::<WrittenUnit>::new());
assert_eq!(shaper.shape_str("\u{1820}").unwrap(), "A+A");
assert!(shaper.same_shape("\u{1820}", "\u{1820}").unwrap());
assert_eq!(
shaper.shape("\u{1820} "),
Err(Error::NonMongolianChar { ch: ' ', index: 1 })
);
for locale in [Locale::Tod, Locale::Sib, Locale::Mch] {
assert!(!Shaper::new(locale).shape("\u{1820}").unwrap().is_empty());
assert!(Shaper::new(locale).rule_names().is_empty());
}
}
#[test]
fn detailed_and_trace_cover_every_token() {
let shaper = Shaper::new(Locale::Mng);
let details = shaper
.shape_detailed("\u{1832}\u{1820}\u{182F}\u{202F}\u{1820}")
.unwrap();
let positions: Vec<Position> = details.iter().map(|d| d.position).collect();
assert_eq!(
positions,
vec![
Position::Init,
Position::Medi,
Position::Fina,
Position::Isol,
Position::Isol
]
);
assert_eq!(details[3].cp, '\u{180E}'); assert_eq!(details[3].written, Vec::<WrittenUnit>::new());
assert_eq!(details[0].alias, Some(Alias::T));
let trace = shaper.trace("\u{1820}").unwrap();
assert_eq!(trace.positions, vec![Position::Isol]);
assert!(trace.transitions.is_empty());
assert_eq!(trace.final_conditions, vec![None]);
assert_eq!(
trace.written_by_token,
vec![vec![WrittenUnit::A, WrittenUnit::A]]
);
assert_eq!(trace.shape, shaper.shape("\u{1820}").unwrap());
}
fn prepared(shaper: &Shaper, text: &str) -> Vec<Token> {
let mut tokens = shaper.tokenize(text);
assign_positions(&mut tokens);
tokens
}
fn resolved(
shaper: &Shaper,
cp: u32,
position: Position,
condition: Option<Condition>,
) -> Token {
let text = char::from_u32(cp).expect("scalar value").to_string();
let mut token = shaper.tokenize(&text).remove(0);
token.position = position;
token.condition = condition;
shaper.resolve_written(&mut token);
token
}
#[test]
fn masc_marker_reaches_g_h_paths() {
let shaper = Shaper::new(Locale::Mng);
let tokens = prepared(&shaper, "\u{1820}\u{182F}\u{182D}");
assert!(shaper.masc_marker_reaches_g_h(&tokens, 2));
let tokens = prepared(
&shaper,
"\u{1830}\u{1822}\u{182D}\u{1830}\u{1822}\u{182D}\u{1820}",
);
assert!(shaper.masc_marker_reaches_g_h(&tokens, 2));
let tokens = prepared(&shaper, "\u{1821}\u{182F}\u{182D}"); assert!(!shaper.masc_marker_reaches_g_h(&tokens, 2));
let tokens = prepared(
&shaper,
"\u{1821}\u{182D}\u{1821}\u{1828}\u{1822}\u{182D}\u{1832}\u{1820}",
);
assert!(!shaper.masc_marker_reaches_g_h(&tokens, 5));
let tokens = prepared(&shaper, "\u{1822}\u{182D}\u{182F}\u{180E}\u{1820}");
assert!(shaper.masc_marker_reaches_g_h(&tokens, 1));
let tokens = prepared(&shaper, "\u{1822}\u{182D}\u{182F}\u{180E}\u{1821}");
assert!(!shaper.masc_marker_reaches_g_h(&tokens, 1));
}
#[test]
fn resolve_written_condition_branch() {
let shaper = Shaper::new(Locale::Mng);
assert_eq!(
shaper.condition_fvs(0x182C, Position::Fina, Condition::MasculineDevsger),
Some(None)
);
let token = resolved(
&shaper,
0x182C,
Position::Fina,
Some(Condition::MasculineDevsger),
);
assert_eq!(token.written, Some(&[WrittenUnit::H][..]));
let (letter, variant, condition) = mng::DATA
.letters
.iter()
.flat_map(|letter| letter.variants.iter().map(move |variant| (letter, variant)))
.filter_map(|(letter, variant)| {
let condition = *variant.conditions.first()?;
variant.fvs?;
let default = shaper.defaults.get(&(letter.cp, variant.position))?;
(default.written != variant.written
&& shaper.condition_fvs(letter.cp, variant.position, condition)
== Some(variant.fvs))
.then_some((letter, variant, condition))
})
.next()
.expect("MNG has an FVS variant selected by a condition");
let default = shaper.defaults[&(letter.cp, variant.position)].written;
let token = resolved(&shaper, letter.cp, variant.position, Some(condition));
assert_eq!(
token.written,
Some(variant.written),
"U+{:04X} {} {}",
letter.cp,
variant.position,
condition.as_str()
);
assert_ne!(token.written, Some(default));
let mut token = shaper.tokenize("\u{182C}\u{180B}").remove(0);
token.position = Position::Init;
token.condition = Some(Condition::Feminine);
shaper.resolve_written(&mut token);
assert_eq!(token.written, Some(&[WrittenUnit::Hx][..]));
assert_eq!(
shaper.condition_fvs(0x182C, Position::Fina, Condition::Feminine),
None
);
let token = resolved(&shaper, 0x182C, Position::Fina, Some(Condition::Feminine));
assert_eq!(token.written, Some(&[WrittenUnit::H][..]));
}
#[test]
fn memoised_written_is_never_invalidated() {
let shaper = Shaper::new(Locale::Mng);
let text = "\u{182A}\u{1820}\u{180E}\u{180A}\u{1822}\u{1828}";
let details = shaper.shape_detailed(text).unwrap();
assert_eq!(details[1].alias, Some(Alias::A));
assert_eq!(details[1].condition, Some(Condition::PostBowed));
assert_eq!(details[1].written, vec![WrittenUnit::A]);
assert_eq!(
shaper.shape(text).unwrap(),
vec![
WrittenUnit::B,
WrittenUnit::A,
WrittenUnit::Mvs,
WrittenUnit::Nirugu,
WrittenUnit::I,
WrittenUnit::I,
WrittenUnit::A,
]
);
assert_eq!(
shaper.shape("\u{182A}\u{1820}").unwrap(),
vec![WrittenUnit::B, WrittenUnit::Aa]
);
}
#[test]
fn debug_reports_the_index_sizes() {
let shaper = Shaper::new(Locale::Mng);
let text = format!("{shaper:?}");
assert!(text.starts_with("Shaper { locale: Mng,"), "{text}");
assert!(text.contains("letters: 35"), "{text}");
assert!(
text.contains(&format!("rules: {}", shaper.rule_names().len())),
"{text}"
);
assert!(text.ends_with(".. }"), "{text}");
}
#[test]
fn unknown_letters_shape_to_nothing() {
let shaper = Shaper::new(Locale::Mng);
assert_eq!(shaper.shape("\u{181A}").unwrap(), Vec::<WrittenUnit>::new());
let details = shaper.shape_detailed("\u{181A}").unwrap();
assert_eq!(details[0].alias, None);
}
}