1use std::collections::{HashMap, HashSet};
4
5use crate::generated::enums::{Alias, Condition, WrittenUnit};
6use crate::generated::mng_normalize;
7use crate::generated::{mch, mng, sib, tod};
8use crate::normalize::NormalizeTable;
9use crate::rules::{self, Rule};
10use crate::tables::{Fvs, Letter, Locale, LocaleData, ParticleSym, Position, Variant};
11use crate::token::{assign_positions, tokenize, Token, TokenKind};
12use crate::unicode::check_word_chars;
13use crate::Error;
14
15#[derive(Clone, Debug, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct TokenDetail {
19 pub cp: char,
21 pub alias: Option<Alias>,
23 pub position: Position,
25 pub fvs: Option<Fvs>,
27 pub condition: Option<Condition>,
29 pub written: Vec<WrittenUnit>,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35#[non_exhaustive]
36pub struct ConditionChange {
37 pub token: usize,
39 pub before: Option<Condition>,
41 pub after: Option<Condition>,
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
47#[non_exhaustive]
48pub struct RuleTransition {
49 pub rule: &'static str,
51 pub changes: Vec<ConditionChange>,
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
59#[non_exhaustive]
60pub struct ShapeTrace {
61 pub positions: Vec<Position>,
63 pub transitions: Vec<RuleTransition>,
65 pub final_conditions: Vec<Option<Condition>>,
67 pub written_by_token: Vec<Vec<WrittenUnit>>,
69 pub shape: Vec<WrittenUnit>,
71}
72
73pub struct Shaper {
78 locale: Locale,
79 letters: HashMap<u32, &'static Letter>,
80 variants: HashMap<(u32, Position, Option<Fvs>), &'static Variant>,
81 defaults: HashMap<(u32, Position), &'static Variant>,
82 vowels: HashSet<Alias>,
83 consonants: HashSet<Alias>,
84 masculine: HashSet<Alias>,
85 feminine: HashSet<Alias>,
86 neuter: HashSet<Alias>,
87 particles: HashMap<&'static [ParticleSym], &'static [usize]>,
88 rules: &'static [Rule],
89 pub(crate) normalize: Option<NormalizeTable>,
90}
91
92const _: () = {
95 fn assert_send_sync<T: Send + Sync>() {}
96 let _ = assert_send_sync::<Shaper>;
97};
98
99impl Default for Shaper {
100 fn default() -> Shaper {
101 Shaper::new(Locale::Mng)
102 }
103}
104
105impl std::fmt::Debug for Shaper {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 f.debug_struct("Shaper")
108 .field("locale", &self.locale)
109 .field("letters", &self.letters.len())
110 .field("variants", &self.variants.len())
111 .field("rules", &self.rules.len())
112 .finish_non_exhaustive()
113 }
114}
115
116impl Shaper {
117 pub fn new(locale: Locale) -> Shaper {
119 let data: &'static LocaleData = match locale {
120 Locale::Mng => &mng::DATA,
121 Locale::Tod => &tod::DATA,
122 Locale::Sib => &sib::DATA,
123 Locale::Mch => &mch::DATA,
124 };
125 let mut letters = HashMap::new();
126 let mut variants = HashMap::new();
127 let mut defaults = HashMap::new();
128 for letter in data.letters {
129 letters.insert(letter.cp, letter);
130 for variant in letter.variants {
131 variants.insert((letter.cp, variant.position, variant.fvs), variant);
132 if variant.default {
133 defaults.insert((letter.cp, variant.position), variant);
134 }
135 }
136 }
137 let set = |aliases: &'static [Alias]| aliases.iter().copied().collect::<HashSet<Alias>>();
138 Shaper {
139 locale,
140 letters,
141 variants,
142 defaults,
143 vowels: set(data.categories.vowel),
144 consonants: set(data.categories.consonant),
145 masculine: set(data.categories.vowel_masculine),
146 feminine: set(data.categories.vowel_feminine),
147 neuter: set(data.categories.vowel_neuter),
148 particles: data
149 .particles
150 .iter()
151 .map(|particle| (particle.key, particle.indices))
152 .collect(),
153 rules: rules::rules_for(locale),
154 normalize: match locale {
155 Locale::Mng => Some(NormalizeTable::new(&mng_normalize::DATA)),
156 Locale::Tod | Locale::Sib | Locale::Mch => None,
157 },
158 }
159 }
160
161 #[cfg(any(test, feature = "testing"))]
165 #[doc(hidden)]
166 pub fn with_empty_normalize_table(locale: Locale) -> Shaper {
167 let mut shaper = Shaper::new(locale);
168 let version = shaper
169 .normalize
170 .as_ref()
171 .map_or("mng-canonical/1", |table| table.canonical_version);
172 shaper.normalize = Some(NormalizeTable::empty(version));
173 shaper
174 }
175
176 pub fn locale(&self) -> Locale {
178 self.locale
179 }
180
181 pub fn rule_names(&self) -> Vec<&'static str> {
183 self.rules.iter().map(|rule| rule.name).collect()
184 }
185
186 pub(crate) fn alias_of(&self, cp: u32) -> Option<Alias> {
189 self.letters.get(&cp).map(|letter| letter.alias)
190 }
191
192 pub(crate) fn tokenize(&self, text: &str) -> Vec<Token> {
193 tokenize(text, |cp| self.alias_of(cp))
194 }
195
196 pub(crate) fn particle(&self, key: &[ParticleSym]) -> Option<&'static [usize]> {
198 self.particles.get(key).copied()
199 }
200
201 fn condition_fvs(
207 &self,
208 cp: u32,
209 position: Position,
210 condition: Condition,
211 ) -> Option<Option<Fvs>> {
212 self.letters
213 .get(&cp)?
214 .variants
215 .iter()
216 .find(|variant| variant.position == position && variant.conditions.contains(&condition))
217 .map(|variant| variant.fvs)
218 }
219
220 pub(crate) fn resolve_written(&self, token: &mut Token) {
229 if token.written.is_some() {
230 return;
231 }
232 if !token.is_letter() {
233 token.written = Some(&[]);
234 return;
235 }
236 let mut written: Option<&'static [WrittenUnit]> = None;
237 for &fvs in &token.fvs {
238 if let Some(variant) = self.variants.get(&(token.cp, token.position, Some(fvs))) {
239 written = Some(variant.written);
240 break;
241 }
242 }
243 if written.is_none() {
244 if let Some(condition) = token.condition {
245 if let Some(fvs) = self.condition_fvs(token.cp, token.position, condition) {
246 written = self
247 .variants
248 .get(&(token.cp, token.position, fvs))
249 .map(|variant| variant.written);
250 }
251 }
252 }
253 if written.is_none() {
254 written = self
255 .defaults
256 .get(&(token.cp, token.position))
257 .map(|variant| variant.written);
258 }
259 token.written = Some(written.unwrap_or(&[]));
260 }
261
262 pub(crate) fn is_vowel(&self, token: &Token) -> bool {
265 token.is_letter()
266 && token
267 .alias
268 .is_some_and(|alias| self.vowels.contains(&alias))
269 }
270
271 pub(crate) fn is_consonant(&self, token: &Token) -> bool {
272 token.is_letter()
273 && token
274 .alias
275 .is_some_and(|alias| self.consonants.contains(&alias))
276 }
277
278 pub(crate) fn is_masc_vowel(&self, token: &Token) -> bool {
279 token
280 .alias
281 .is_some_and(|alias| self.masculine.contains(&alias))
282 }
283
284 pub(crate) fn is_fem_vowel(&self, token: &Token) -> bool {
285 token
286 .alias
287 .is_some_and(|alias| self.feminine.contains(&alias))
288 }
289
290 pub(crate) fn is_neut_vowel(&self, token: &Token) -> bool {
291 token
292 .alias
293 .is_some_and(|alias| self.neuter.contains(&alias))
294 }
295
296 pub(crate) fn masc_marker_reaches_g_h(&self, tokens: &[Token], idx: usize) -> bool {
301 let mut j = idx;
303 while j > 0 {
304 j -= 1;
305 let token = &tokens[j];
306 if !token.is_letter() {
307 if token.is_nirugu() {
308 continue;
309 }
310 break; }
312 if self.is_fem_vowel(token) {
313 break;
314 }
315 if self.is_masc_vowel(token)
316 && matches!(token.position, Position::Init | Position::Medi)
317 {
318 return true;
319 }
320 }
321
322 if !matches!(tokens[idx].position, Position::Init | Position::Medi) {
324 return false;
325 }
326 let mut j = idx;
328 while j > 0 {
329 j -= 1;
330 let token = &tokens[j];
331 if !token.is_letter() {
332 if token.is_nirugu() {
333 continue;
334 }
335 break;
336 }
337 if self.is_fem_vowel(token) {
338 return false;
339 }
340 }
341 let mut j = idx + 1;
344 while j < tokens.len() {
345 let next = &tokens[j];
346 if !next.is_letter() {
347 if next.is_nirugu() {
348 j += 1;
349 continue;
350 }
351 return false;
352 }
353 if self.is_masc_vowel(next) {
354 return true;
355 }
356 if self.is_fem_vowel(next) {
357 return false;
358 }
359 if matches!(next.position, Position::Init | Position::Medi) {
360 j += 1;
361 continue;
362 }
363 let mut k = j + 1;
364 while k < tokens.len() && tokens[k].is_nirugu() {
365 k += 1;
366 }
367 return next.position == Position::Fina
368 && k < tokens.len()
369 && tokens[k].is_mvs()
370 && k + 1 < tokens.len()
371 && tokens[k + 1].is_letter()
372 && tokens[k + 1].alias == Some(Alias::A)
373 && tokens[k + 1].position == Position::Isol;
374 }
375 false
376 }
377
378 fn prepare(&self, text: &str) -> Result<Vec<Token>, Error> {
383 check_word_chars(text)?;
384 let mut tokens = self.tokenize(text);
385 assign_positions(&mut tokens);
386 Ok(tokens)
387 }
388
389 fn resolve_all(&self, tokens: &mut [Token]) {
391 for token in tokens {
392 self.resolve_written(token);
393 }
394 }
395
396 fn run_pipeline(&self, text: &str) -> Result<Vec<Token>, Error> {
397 let mut tokens = self.prepare(text)?;
398 rules::run_rules(self.rules, &mut tokens, self);
399 self.resolve_all(&mut tokens);
400 Ok(tokens)
401 }
402
403 pub fn shape(&self, text: &str) -> Result<Vec<WrittenUnit>, Error> {
409 Ok(flatten(&self.run_pipeline(text)?))
410 }
411
412 pub fn shape_str(&self, text: &str) -> Result<String, Error> {
414 let units = self.shape(text)?;
415 let mut out = String::new();
416 for unit in units {
417 if !out.is_empty() {
418 out.push('+');
419 }
420 out.push_str(unit.as_str());
421 }
422 Ok(out)
423 }
424
425 pub fn same_shape(&self, a: &str, b: &str) -> Result<bool, Error> {
427 Ok(self.shape(a)? == self.shape(b)?)
428 }
429
430 pub fn shape_detailed(&self, text: &str) -> Result<Vec<TokenDetail>, Error> {
432 let tokens = self.run_pipeline(text)?;
433 Ok(tokens
434 .iter()
435 .map(|token| TokenDetail {
436 cp: char::from_u32(token.cp).expect("token code points are scalar values"),
437 alias: token.alias,
438 position: token.position,
439 fvs: token.first_fvs(),
440 condition: token.condition,
441 written: token
442 .written
443 .map(<[WrittenUnit]>::to_vec)
444 .unwrap_or_default(),
445 })
446 .collect())
447 }
448
449 pub fn trace(&self, text: &str) -> Result<ShapeTrace, Error> {
451 let mut tokens = self.prepare(text)?;
452 let mut transitions = Vec::new();
453 for rule in self.rules {
454 let before: Vec<Option<Condition>> =
455 tokens.iter().map(|token| token.condition).collect();
456 (rule.apply)(&mut tokens, self);
457 let changes: Vec<ConditionChange> = before
458 .iter()
459 .zip(&tokens)
460 .enumerate()
461 .filter(|(_, (old, token))| **old != token.condition)
462 .map(|(index, (old, token))| ConditionChange {
463 token: index,
464 before: *old,
465 after: token.condition,
466 })
467 .collect();
468 if !changes.is_empty() {
469 transitions.push(RuleTransition {
470 rule: rule.name,
471 changes,
472 });
473 }
474 }
475 self.resolve_all(&mut tokens);
476 Ok(ShapeTrace {
477 positions: tokens.iter().map(|token| token.position).collect(),
478 transitions,
479 final_conditions: tokens.iter().map(|token| token.condition).collect(),
480 written_by_token: tokens
481 .iter()
482 .map(|token| {
483 token
484 .written
485 .map(<[WrittenUnit]>::to_vec)
486 .unwrap_or_default()
487 })
488 .collect(),
489 shape: flatten(&tokens),
490 })
491 }
492}
493
494pub(crate) fn flatten(tokens: &[Token]) -> Vec<WrittenUnit> {
496 let mut shape = Vec::with_capacity(tokens.len() * 2);
497 for token in tokens {
498 match token.kind {
499 TokenKind::Mvs => shape.push(WrittenUnit::Mvs),
500 TokenKind::Nirugu => shape.push(WrittenUnit::Nirugu),
501 TokenKind::Zwj => shape.push(WrittenUnit::Zwj),
502 TokenKind::Letter => {
503 if let Some(written) = token.written {
504 shape.extend_from_slice(written);
505 }
506 }
507 }
508 }
509 shape
510}
511
512pub(crate) fn prev_letter(tokens: &[Token], index: usize) -> Option<usize> {
516 (0..index).rev().find(|&j| tokens[j].is_letter())
517}
518
519pub(crate) fn next_letter(tokens: &[Token], index: usize) -> Option<usize> {
521 (index + 1..tokens.len()).find(|&j| tokens[j].is_letter())
522}
523
524pub(crate) fn prev_tok(_tokens: &[Token], index: usize) -> Option<usize> {
527 if index > 0 {
528 Some(index - 1)
529 } else {
530 None
531 }
532}
533
534pub(crate) fn next_tok(tokens: &[Token], index: usize) -> Option<usize> {
536 if index + 1 < tokens.len() {
537 Some(index + 1)
538 } else {
539 None
540 }
541}
542
543pub(crate) fn prev_adjacent_letter(tokens: &[Token], index: usize) -> Option<usize> {
545 let mut j = index;
546 while j > 0 {
547 j -= 1;
548 let token = &tokens[j];
549 if token.is_mvs() {
550 return None;
551 }
552 if token.is_letter() {
553 return Some(j);
554 }
555 if token.is_nirugu() {
556 continue;
557 }
558 return None;
559 }
560 None
561}
562
563pub(crate) fn next_adjacent_letter(tokens: &[Token], index: usize) -> Option<usize> {
565 let mut j = index + 1;
566 while j < tokens.len() {
567 let token = &tokens[j];
568 if token.is_mvs() {
569 return None;
570 }
571 if token.is_letter() {
572 return Some(j);
573 }
574 if token.is_nirugu() {
575 j += 1;
576 continue;
577 }
578 return None;
579 }
580 None
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586
587 #[test]
588 fn default_forms_without_rules() {
589 let shaper = Shaper::new(Locale::Mng);
590 assert_eq!(
591 shaper.shape("\u{1820}").unwrap(),
592 vec![WrittenUnit::A, WrittenUnit::A]
593 );
594 assert_eq!(
595 shaper.shape("\u{1820}\u{180B}").unwrap(),
596 vec![WrittenUnit::A]
597 );
598 assert_eq!(shaper.shape("\u{180E}").unwrap(), vec![WrittenUnit::Mvs]);
599 assert_eq!(
600 shaper.shape("\u{180A}\u{1823}").unwrap(),
601 vec![WrittenUnit::Nirugu, WrittenUnit::U]
602 );
603 assert_eq!(
604 shaper.shape("\u{200D}\u{1833}").unwrap(),
605 vec![WrittenUnit::Zwj, WrittenUnit::Dd]
606 );
607 assert_eq!(shaper.shape("").unwrap(), Vec::<WrittenUnit>::new());
608 assert_eq!(shaper.shape_str("\u{1820}").unwrap(), "A+A");
609 assert!(shaper.same_shape("\u{1820}", "\u{1820}").unwrap());
610 assert_eq!(
611 shaper.shape("\u{1820} "),
612 Err(Error::NonMongolianChar { ch: ' ', index: 1 })
613 );
614 for locale in [Locale::Tod, Locale::Sib, Locale::Mch] {
615 assert!(!Shaper::new(locale).shape("\u{1820}").unwrap().is_empty());
616 assert!(Shaper::new(locale).rule_names().is_empty());
617 }
618 }
619
620 #[test]
621 fn detailed_and_trace_cover_every_token() {
622 let shaper = Shaper::new(Locale::Mng);
623 let details = shaper
624 .shape_detailed("\u{1832}\u{1820}\u{182F}\u{202F}\u{1820}")
625 .unwrap();
626 let positions: Vec<Position> = details.iter().map(|d| d.position).collect();
627 assert_eq!(
628 positions,
629 vec![
630 Position::Init,
631 Position::Medi,
632 Position::Fina,
633 Position::Isol,
634 Position::Isol
635 ]
636 );
637 assert_eq!(details[3].cp, '\u{180E}'); assert_eq!(details[3].written, Vec::<WrittenUnit>::new());
639 assert_eq!(details[0].alias, Some(Alias::T));
640 let trace = shaper.trace("\u{1820}").unwrap();
641 assert_eq!(trace.positions, vec![Position::Isol]);
642 assert!(trace.transitions.is_empty());
643 assert_eq!(trace.final_conditions, vec![None]);
644 assert_eq!(
645 trace.written_by_token,
646 vec![vec![WrittenUnit::A, WrittenUnit::A]]
647 );
648 assert_eq!(trace.shape, shaper.shape("\u{1820}").unwrap());
649 }
650
651 fn prepared(shaper: &Shaper, text: &str) -> Vec<Token> {
653 let mut tokens = shaper.tokenize(text);
654 assign_positions(&mut tokens);
655 tokens
656 }
657
658 fn resolved(
660 shaper: &Shaper,
661 cp: u32,
662 position: Position,
663 condition: Option<Condition>,
664 ) -> Token {
665 let text = char::from_u32(cp).expect("scalar value").to_string();
666 let mut token = shaper.tokenize(&text).remove(0);
667 token.position = position;
668 token.condition = condition;
669 shaper.resolve_written(&mut token);
670 token
671 }
672
673 #[test]
674 fn masc_marker_reaches_g_h_paths() {
675 let shaper = Shaper::new(Locale::Mng);
676 let tokens = prepared(&shaper, "\u{1820}\u{182F}\u{182D}");
682 assert!(shaper.masc_marker_reaches_g_h(&tokens, 2));
683
684 let tokens = prepared(
688 &shaper,
689 "\u{1830}\u{1822}\u{182D}\u{1830}\u{1822}\u{182D}\u{1820}",
690 );
691 assert!(shaper.masc_marker_reaches_g_h(&tokens, 2));
692
693 let tokens = prepared(&shaper, "\u{1821}\u{182F}\u{182D}"); assert!(!shaper.masc_marker_reaches_g_h(&tokens, 2));
696 let tokens = prepared(
698 &shaper,
699 "\u{1821}\u{182D}\u{1821}\u{1828}\u{1822}\u{182D}\u{1832}\u{1820}",
700 );
701 assert!(!shaper.masc_marker_reaches_g_h(&tokens, 5));
702
703 let tokens = prepared(&shaper, "\u{1822}\u{182D}\u{182F}\u{180E}\u{1820}");
706 assert!(shaper.masc_marker_reaches_g_h(&tokens, 1));
707 let tokens = prepared(&shaper, "\u{1822}\u{182D}\u{182F}\u{180E}\u{1821}");
709 assert!(!shaper.masc_marker_reaches_g_h(&tokens, 1));
710 }
711
712 #[test]
713 fn resolve_written_condition_branch() {
714 let shaper = Shaper::new(Locale::Mng);
715
716 assert_eq!(
719 shaper.condition_fvs(0x182C, Position::Fina, Condition::MasculineDevsger),
720 Some(None)
721 );
722 let token = resolved(
723 &shaper,
724 0x182C,
725 Position::Fina,
726 Some(Condition::MasculineDevsger),
727 );
728 assert_eq!(token.written, Some(&[WrittenUnit::H][..]));
729
730 let (letter, variant, condition) = mng::DATA
733 .letters
734 .iter()
735 .flat_map(|letter| letter.variants.iter().map(move |variant| (letter, variant)))
736 .filter_map(|(letter, variant)| {
737 let condition = *variant.conditions.first()?;
738 variant.fvs?;
739 let default = shaper.defaults.get(&(letter.cp, variant.position))?;
740 (default.written != variant.written
741 && shaper.condition_fvs(letter.cp, variant.position, condition)
742 == Some(variant.fvs))
743 .then_some((letter, variant, condition))
744 })
745 .next()
746 .expect("MNG has an FVS variant selected by a condition");
747 let default = shaper.defaults[&(letter.cp, variant.position)].written;
748 let token = resolved(&shaper, letter.cp, variant.position, Some(condition));
749 assert_eq!(
750 token.written,
751 Some(variant.written),
752 "U+{:04X} {} {}",
753 letter.cp,
754 variant.position,
755 condition.as_str()
756 );
757 assert_ne!(token.written, Some(default));
758
759 let mut token = shaper.tokenize("\u{182C}\u{180B}").remove(0);
762 token.position = Position::Init;
763 token.condition = Some(Condition::Feminine);
764 shaper.resolve_written(&mut token);
765 assert_eq!(token.written, Some(&[WrittenUnit::Hx][..]));
766
767 assert_eq!(
770 shaper.condition_fvs(0x182C, Position::Fina, Condition::Feminine),
771 None
772 );
773 let token = resolved(&shaper, 0x182C, Position::Fina, Some(Condition::Feminine));
774 assert_eq!(token.written, Some(&[WrittenUnit::H][..]));
775 }
776
777 #[test]
778 fn memoised_written_is_never_invalidated() {
779 let shaper = Shaper::new(Locale::Mng);
780 let text = "\u{182A}\u{1820}\u{180E}\u{180A}\u{1822}\u{1828}";
785 let details = shaper.shape_detailed(text).unwrap();
786 assert_eq!(details[1].alias, Some(Alias::A));
787 assert_eq!(details[1].condition, Some(Condition::PostBowed));
788 assert_eq!(details[1].written, vec![WrittenUnit::A]);
789 assert_eq!(
790 shaper.shape(text).unwrap(),
791 vec![
792 WrittenUnit::B,
793 WrittenUnit::A,
794 WrittenUnit::Mvs,
795 WrittenUnit::Nirugu,
796 WrittenUnit::I,
797 WrittenUnit::I,
798 WrittenUnit::A,
799 ]
800 );
801 assert_eq!(
803 shaper.shape("\u{182A}\u{1820}").unwrap(),
804 vec![WrittenUnit::B, WrittenUnit::Aa]
805 );
806 }
807
808 #[test]
809 fn debug_reports_the_index_sizes() {
810 let shaper = Shaper::new(Locale::Mng);
811 let text = format!("{shaper:?}");
812 assert!(text.starts_with("Shaper { locale: Mng,"), "{text}");
813 assert!(text.contains("letters: 35"), "{text}");
814 assert!(
815 text.contains(&format!("rules: {}", shaper.rule_names().len())),
816 "{text}"
817 );
818 assert!(text.ends_with(".. }"), "{text}");
819 }
820
821 #[test]
822 fn unknown_letters_shape_to_nothing() {
823 let shaper = Shaper::new(Locale::Mng);
825 assert_eq!(shaper.shape("\u{181A}").unwrap(), Vec::<WrittenUnit>::new());
826 let details = shaper.shape_detailed("\u{181A}").unwrap();
827 assert_eq!(details[0].alias, None);
828 }
829}