1use std::collections::{HashMap, HashSet};
4
5use crate::duplicates::collapse;
6use crate::generated::enums::{Alias, Condition, WrittenUnit};
7use crate::generated::mng;
8use crate::generated::mng_normalize;
9use crate::normalize::NormalizeTable;
10use crate::redundant_zwj::drop_redundant_zwj;
11use crate::rules::{self, Rule};
12use crate::tables::{Fvs, Letter, Locale, LocaleData, ParticleSym, Position, Variant};
13use crate::token::{assign_positions, tokenize, Token, TokenKind};
14use crate::unicode::check_word_chars;
15use crate::Error;
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19#[non_exhaustive]
20pub struct TokenDetail {
21 pub cp: char,
23 pub alias: Option<Alias>,
25 pub position: Position,
27 pub fvs: Option<Fvs>,
29 pub condition: Option<Condition>,
31 pub written: Vec<WrittenUnit>,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37#[non_exhaustive]
38pub struct ConditionChange {
39 pub token: usize,
41 pub before: Option<Condition>,
43 pub after: Option<Condition>,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct RuleTransition {
51 pub rule: &'static str,
53 pub changes: Vec<ConditionChange>,
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
61#[non_exhaustive]
62pub struct ShapeTrace {
63 pub positions: Vec<Position>,
65 pub transitions: Vec<RuleTransition>,
67 pub final_conditions: Vec<Option<Condition>>,
69 pub written_by_token: Vec<Vec<WrittenUnit>>,
71 pub shape: Vec<WrittenUnit>,
73}
74
75pub struct Shaper {
81 locale: Locale,
82 letters: HashMap<u32, &'static Letter>,
83 variants: HashMap<(u32, Position, Option<Fvs>), &'static Variant>,
84 defaults: HashMap<(u32, Position), &'static Variant>,
85 vowels: HashSet<Alias>,
86 consonants: HashSet<Alias>,
87 masculine: HashSet<Alias>,
88 feminine: HashSet<Alias>,
89 neuter: HashSet<Alias>,
90 particles: HashMap<&'static [ParticleSym], &'static [usize]>,
91 rules: &'static [Rule],
92 pub(crate) normalize: NormalizeTable,
93}
94
95const _: () = {
98 fn assert_send_sync<T: Send + Sync>() {}
99 let _ = assert_send_sync::<Shaper>;
100};
101
102impl Default for Shaper {
103 fn default() -> Shaper {
104 Shaper::new(Locale::Mng)
105 }
106}
107
108impl std::fmt::Debug for Shaper {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.debug_struct("Shaper")
111 .field("locale", &self.locale)
112 .field("letters", &self.letters.len())
113 .field("variants", &self.variants.len())
114 .field("rules", &self.rules.len())
115 .finish_non_exhaustive()
116 }
117}
118
119impl Shaper {
120 pub fn new(locale: Locale) -> Shaper {
122 let (data, normalize): (&'static LocaleData, _) = match locale {
123 Locale::Mng => (&mng::DATA, &mng_normalize::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: NormalizeTable::new(normalize),
155 }
156 }
157
158 #[cfg(any(test, feature = "testing"))]
162 #[doc(hidden)]
163 pub fn with_empty_normalize_table(locale: Locale) -> Shaper {
164 let mut shaper = Shaper::new(locale);
165 shaper.normalize = NormalizeTable::empty(shaper.normalize.canonical_version);
166 shaper
167 }
168
169 pub fn locale(&self) -> Locale {
171 self.locale
172 }
173
174 pub fn rule_names(&self) -> Vec<&'static str> {
176 self.rules.iter().map(|rule| rule.name).collect()
177 }
178
179 pub(crate) fn alias_of(&self, cp: u32) -> Option<Alias> {
182 self.letters.get(&cp).map(|letter| letter.alias)
183 }
184
185 pub(crate) fn tokenize(&self, text: &str) -> Vec<Token> {
186 tokenize(text, |cp| self.alias_of(cp))
187 }
188
189 pub(crate) fn particle(&self, key: &[ParticleSym]) -> Option<&'static [usize]> {
191 self.particles.get(key).copied()
192 }
193
194 fn condition_fvs(
200 &self,
201 cp: u32,
202 position: Position,
203 condition: Condition,
204 ) -> Option<Option<Fvs>> {
205 self.letters
206 .get(&cp)?
207 .variants
208 .iter()
209 .find(|variant| variant.position == position && variant.conditions.contains(&condition))
210 .map(|variant| variant.fvs)
211 }
212
213 pub(crate) fn resolve_written(&self, token: &mut Token) {
227 if token.written.is_some() {
228 return;
229 }
230 if !token.is_letter() {
231 token.written = Some(&[]);
232 return;
233 }
234 let written = token
235 .first_fvs()
236 .and_then(|fvs| self.variants.get(&(token.cp, token.position, Some(fvs))))
237 .map(|variant| variant.written)
238 .unwrap_or_else(|| self.contextual_written(token));
239 token.written = Some(written);
240 }
241
242 pub(crate) fn contextual_written(&self, token: &Token) -> &'static [WrittenUnit] {
246 token
247 .condition
248 .and_then(|condition| self.condition_fvs(token.cp, token.position, condition))
249 .and_then(|fvs| self.variants.get(&(token.cp, token.position, fvs)))
250 .or_else(|| self.defaults.get(&(token.cp, token.position)))
251 .map_or(&[], |variant| variant.written)
252 }
253
254 pub(crate) fn is_vowel(&self, token: &Token) -> bool {
257 token.is_letter()
258 && token
259 .alias
260 .is_some_and(|alias| self.vowels.contains(&alias))
261 }
262
263 pub(crate) fn is_consonant(&self, token: &Token) -> bool {
264 token.is_letter()
265 && token
266 .alias
267 .is_some_and(|alias| self.consonants.contains(&alias))
268 }
269
270 pub(crate) fn has_valid_fvs(&self, token: &Token) -> bool {
275 token.is_letter()
276 && token.first_fvs().is_some_and(|fvs| {
277 self.variants
278 .contains_key(&(token.cp, token.position, Some(fvs)))
279 })
280 }
281
282 pub(crate) fn is_masc_vowel(&self, token: &Token) -> bool {
283 token
284 .alias
285 .is_some_and(|alias| self.masculine.contains(&alias))
286 }
287
288 pub(crate) fn is_fem_vowel(&self, token: &Token) -> bool {
289 token
290 .alias
291 .is_some_and(|alias| self.feminine.contains(&alias))
292 }
293
294 pub(crate) fn is_neut_vowel(&self, token: &Token) -> bool {
295 token
296 .alias
297 .is_some_and(|alias| self.neuter.contains(&alias))
298 }
299
300 pub(crate) fn masc_marker_reaches_g_h(&self, tokens: &[Token], idx: usize) -> bool {
305 let mut j = idx;
307 while j > 0 {
308 j -= 1;
309 let token = &tokens[j];
310 if !token.is_letter() {
311 if token.is_nirugu() {
312 continue;
313 }
314 break; }
316 if self.is_fem_vowel(token) {
317 break;
318 }
319 if self.is_masc_vowel(token)
320 && matches!(token.position, Position::Init | Position::Medi)
321 {
322 return true;
323 }
324 }
325
326 if !matches!(tokens[idx].position, Position::Init | Position::Medi) {
328 return false;
329 }
330 let mut j = idx;
332 while j > 0 {
333 j -= 1;
334 let token = &tokens[j];
335 if !token.is_letter() {
336 if token.is_nirugu() {
337 continue;
338 }
339 break;
340 }
341 if self.is_fem_vowel(token) {
342 return false;
343 }
344 }
345 let mut j = idx + 1;
348 while j < tokens.len() {
349 let next = &tokens[j];
350 if !next.is_letter() {
351 if next.is_nirugu() {
352 j += 1;
353 continue;
354 }
355 return false;
356 }
357 if self.is_masc_vowel(next) {
358 return true;
359 }
360 if self.is_fem_vowel(next) {
361 return false;
362 }
363 if matches!(next.position, Position::Init | Position::Medi) {
364 j += 1;
365 continue;
366 }
367 let mut k = j + 1;
368 while k < tokens.len() && tokens[k].is_nirugu() {
369 k += 1;
370 }
371 return next.position == Position::Fina
372 && k < tokens.len()
373 && tokens[k].is_mvs()
374 && k + 1 < tokens.len()
375 && tokens[k + 1].is_letter()
376 && tokens[k + 1].alias == Some(Alias::A)
377 && tokens[k + 1].position == Position::Isol;
378 }
379 false
380 }
381
382 fn prepare(&self, text: &str) -> Result<Vec<Token>, Error> {
387 check_word_chars(text)?;
388 let mut tokens = self.tokenize(text);
389 assign_positions(&mut tokens);
390 Ok(tokens)
391 }
392
393 fn resolve_all(&self, tokens: &mut [Token]) {
395 for token in tokens {
396 self.resolve_written(token);
397 }
398 }
399
400 fn run_pipeline(&self, text: &str) -> Result<Vec<Token>, Error> {
401 let mut tokens = self.prepare(text)?;
402 rules::run_rules(self.rules, &mut tokens, self);
403 self.resolve_all(&mut tokens);
404 Ok(tokens)
405 }
406
407 pub fn shape(&self, text: &str) -> Result<Vec<WrittenUnit>, Error> {
427 Ok(self.canonical_units(&self.shape_raw(text)?))
428 }
429
430 pub(crate) fn canonical_units(&self, raw: &[WrittenUnit]) -> Vec<WrittenUnit> {
436 collapse(&self.without_redundant_zwj(raw))
437 }
438
439 pub(crate) fn without_redundant_zwj(&self, units: &[WrittenUnit]) -> Vec<WrittenUnit> {
441 drop_redundant_zwj(units)
442 }
443
444 #[doc(hidden)]
453 pub fn shape_raw(&self, text: &str) -> Result<Vec<WrittenUnit>, Error> {
454 Ok(flatten(&self.run_pipeline(text)?))
455 }
456
457 pub fn shape_str(&self, text: &str) -> Result<String, Error> {
459 let units = self.shape(text)?;
460 let mut out = String::new();
461 for unit in units {
462 if !out.is_empty() {
463 out.push('+');
464 }
465 out.push_str(unit.as_str());
466 }
467 Ok(out)
468 }
469
470 pub fn same_shape(&self, a: &str, b: &str) -> Result<bool, Error> {
472 Ok(self.shape(a)? == self.shape(b)?)
473 }
474
475 pub fn shape_detailed(&self, text: &str) -> Result<Vec<TokenDetail>, Error> {
477 let tokens = self.run_pipeline(text)?;
478 Ok(tokens
479 .iter()
480 .map(|token| TokenDetail {
481 cp: char::from_u32(token.cp).expect("token code points are scalar values"),
482 alias: token.alias,
483 position: token.position,
484 fvs: token.first_fvs(),
485 condition: token.condition,
486 written: token
487 .written
488 .map(<[WrittenUnit]>::to_vec)
489 .unwrap_or_default(),
490 })
491 .collect())
492 }
493
494 pub fn trace(&self, text: &str) -> Result<ShapeTrace, Error> {
496 let mut tokens = self.prepare(text)?;
497 let mut transitions = Vec::new();
498 for rule in self.rules {
499 let before: Vec<Option<Condition>> =
500 tokens.iter().map(|token| token.condition).collect();
501 (rule.apply)(&mut tokens, self);
502 let changes: Vec<ConditionChange> = before
503 .iter()
504 .zip(&tokens)
505 .enumerate()
506 .filter(|(_, (old, token))| **old != token.condition)
507 .map(|(index, (old, token))| ConditionChange {
508 token: index,
509 before: *old,
510 after: token.condition,
511 })
512 .collect();
513 if !changes.is_empty() {
514 transitions.push(RuleTransition {
515 rule: rule.name,
516 changes,
517 });
518 }
519 }
520 self.resolve_all(&mut tokens);
521 Ok(ShapeTrace {
522 positions: tokens.iter().map(|token| token.position).collect(),
523 transitions,
524 final_conditions: tokens.iter().map(|token| token.condition).collect(),
525 written_by_token: tokens
526 .iter()
527 .map(|token| {
528 token
529 .written
530 .map(<[WrittenUnit]>::to_vec)
531 .unwrap_or_default()
532 })
533 .collect(),
534 shape: self.canonical_units(&flatten(&tokens)),
535 })
536 }
537}
538
539pub(crate) fn flatten(tokens: &[Token]) -> Vec<WrittenUnit> {
541 let mut shape = Vec::with_capacity(tokens.len() * 2);
542 for token in tokens {
543 match token.kind {
544 TokenKind::Mvs => shape.push(WrittenUnit::Mvs),
545 TokenKind::Nirugu => shape.push(WrittenUnit::Nirugu),
546 TokenKind::Zwj => shape.push(WrittenUnit::Zwj),
547 TokenKind::Letter => {
548 if let Some(written) = token.written {
549 shape.extend_from_slice(written);
550 }
551 }
552 }
553 }
554 shape
555}
556
557pub(crate) fn prev_tok(_tokens: &[Token], index: usize) -> Option<usize> {
562 if index > 0 {
563 Some(index - 1)
564 } else {
565 None
566 }
567}
568
569pub(crate) fn next_tok(tokens: &[Token], index: usize) -> Option<usize> {
571 if index + 1 < tokens.len() {
572 Some(index + 1)
573 } else {
574 None
575 }
576}
577
578pub(crate) fn prev_adjacent_letter(tokens: &[Token], index: usize) -> Option<usize> {
580 let mut j = index;
581 while j > 0 {
582 j -= 1;
583 let token = &tokens[j];
584 if token.is_mvs() {
585 return None;
586 }
587 if token.is_letter() {
588 return Some(j);
589 }
590 if token.is_nirugu() {
591 continue;
592 }
593 return None;
594 }
595 None
596}
597
598pub(crate) fn next_adjacent_letter(tokens: &[Token], index: usize) -> Option<usize> {
600 let mut j = index + 1;
601 while j < tokens.len() {
602 let token = &tokens[j];
603 if token.is_mvs() {
604 return None;
605 }
606 if token.is_letter() {
607 return Some(j);
608 }
609 if token.is_nirugu() {
610 j += 1;
611 continue;
612 }
613 return None;
614 }
615 None
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621
622 #[test]
623 fn default_forms_without_rules() {
624 let shaper = Shaper::new(Locale::Mng);
625 assert_eq!(
626 shaper.shape("\u{1820}").unwrap(),
627 vec![WrittenUnit::A, WrittenUnit::A]
628 );
629 assert_eq!(
630 shaper.shape("\u{1820}\u{180B}").unwrap(),
631 vec![WrittenUnit::A]
632 );
633 assert_eq!(shaper.shape("\u{180E}").unwrap(), vec![WrittenUnit::Mvs]);
634 assert_eq!(
635 shaper.shape("\u{180A}\u{1823}").unwrap(),
636 vec![WrittenUnit::Nirugu, WrittenUnit::U]
637 );
638 assert_eq!(
641 shaper.shape_raw("\u{200D}\u{1833}").unwrap(),
642 vec![WrittenUnit::Zwj, WrittenUnit::Dd]
643 );
644 assert_eq!(
645 shaper.shape("\u{200D}\u{1833}").unwrap(),
646 vec![WrittenUnit::O, WrittenUnit::A]
647 );
648 assert_eq!(shaper.shape("").unwrap(), Vec::<WrittenUnit>::new());
649 assert_eq!(shaper.shape_str("\u{1820}").unwrap(), "A+A");
650 assert!(shaper.same_shape("\u{1820}", "\u{1820}").unwrap());
651 assert_eq!(
652 shaper.shape("\u{1820} "),
653 Err(Error::NonMongolianChar { ch: ' ', index: 1 })
654 );
655 }
656
657 #[test]
658 fn detailed_and_trace_cover_every_token() {
659 let shaper = Shaper::new(Locale::Mng);
660 let details = shaper
661 .shape_detailed("\u{1832}\u{1820}\u{182F}\u{202F}\u{1820}")
662 .unwrap();
663 let positions: Vec<Position> = details.iter().map(|d| d.position).collect();
664 assert_eq!(
665 positions,
666 vec![
667 Position::Init,
668 Position::Medi,
669 Position::Fina,
670 Position::Isol,
671 Position::Isol
672 ]
673 );
674 assert_eq!(details[3].cp, '\u{180E}'); assert_eq!(details[3].written, Vec::<WrittenUnit>::new());
676 assert_eq!(details[0].alias, Some(Alias::T));
677 let trace = shaper.trace("\u{1820}").unwrap();
678 assert_eq!(trace.positions, vec![Position::Isol]);
679 assert!(trace.transitions.is_empty());
680 assert_eq!(trace.final_conditions, vec![None]);
681 assert_eq!(
682 trace.written_by_token,
683 vec![vec![WrittenUnit::A, WrittenUnit::A]]
684 );
685 assert_eq!(trace.shape, shaper.shape("\u{1820}").unwrap());
686 }
687
688 fn prepared(shaper: &Shaper, text: &str) -> Vec<Token> {
690 let mut tokens = shaper.tokenize(text);
691 assign_positions(&mut tokens);
692 tokens
693 }
694
695 fn resolved(
697 shaper: &Shaper,
698 cp: u32,
699 position: Position,
700 condition: Option<Condition>,
701 ) -> Token {
702 let text = char::from_u32(cp).expect("scalar value").to_string();
703 let mut token = shaper.tokenize(&text).remove(0);
704 token.position = position;
705 token.condition = condition;
706 shaper.resolve_written(&mut token);
707 token
708 }
709
710 #[test]
711 fn masc_marker_reaches_g_h_paths() {
712 let shaper = Shaper::new(Locale::Mng);
713 let tokens = prepared(&shaper, "\u{1820}\u{182F}\u{182D}");
719 assert!(shaper.masc_marker_reaches_g_h(&tokens, 2));
720
721 let tokens = prepared(
725 &shaper,
726 "\u{1830}\u{1822}\u{182D}\u{1830}\u{1822}\u{182D}\u{1820}",
727 );
728 assert!(shaper.masc_marker_reaches_g_h(&tokens, 2));
729
730 let tokens = prepared(&shaper, "\u{1821}\u{182F}\u{182D}"); assert!(!shaper.masc_marker_reaches_g_h(&tokens, 2));
733 let tokens = prepared(
735 &shaper,
736 "\u{1821}\u{182D}\u{1821}\u{1828}\u{1822}\u{182D}\u{1832}\u{1820}",
737 );
738 assert!(!shaper.masc_marker_reaches_g_h(&tokens, 5));
739
740 let tokens = prepared(&shaper, "\u{1822}\u{182D}\u{182F}\u{180E}\u{1820}");
743 assert!(shaper.masc_marker_reaches_g_h(&tokens, 1));
744 let tokens = prepared(&shaper, "\u{1822}\u{182D}\u{182F}\u{180E}\u{1821}");
746 assert!(!shaper.masc_marker_reaches_g_h(&tokens, 1));
747 }
748
749 #[test]
750 fn resolve_written_condition_branch() {
751 let shaper = Shaper::new(Locale::Mng);
752
753 assert_eq!(
756 shaper.condition_fvs(0x182C, Position::Fina, Condition::MasculineDevsger),
757 Some(None)
758 );
759 let token = resolved(
760 &shaper,
761 0x182C,
762 Position::Fina,
763 Some(Condition::MasculineDevsger),
764 );
765 assert_eq!(token.written, Some(&[WrittenUnit::H][..]));
766
767 let (letter, variant, condition) = mng::DATA
770 .letters
771 .iter()
772 .flat_map(|letter| letter.variants.iter().map(move |variant| (letter, variant)))
773 .filter_map(|(letter, variant)| {
774 let condition = *variant.conditions.first()?;
775 variant.fvs?;
776 let default = shaper.defaults.get(&(letter.cp, variant.position))?;
777 (default.written != variant.written
778 && shaper.condition_fvs(letter.cp, variant.position, condition)
779 == Some(variant.fvs))
780 .then_some((letter, variant, condition))
781 })
782 .next()
783 .expect("MNG has an FVS variant selected by a condition");
784 let default = shaper.defaults[&(letter.cp, variant.position)].written;
785 let token = resolved(&shaper, letter.cp, variant.position, Some(condition));
786 assert_eq!(
787 token.written,
788 Some(variant.written),
789 "U+{:04X} {} {}",
790 letter.cp,
791 variant.position,
792 condition.as_str()
793 );
794 assert_ne!(token.written, Some(default));
795
796 let mut token = shaper.tokenize("\u{182C}\u{180B}").remove(0);
799 token.position = Position::Init;
800 token.condition = Some(Condition::Feminine);
801 shaper.resolve_written(&mut token);
802 assert_eq!(token.written, Some(&[WrittenUnit::Hx][..]));
803
804 assert_eq!(
807 shaper.condition_fvs(0x182C, Position::Fina, Condition::Feminine),
808 None
809 );
810 let token = resolved(&shaper, 0x182C, Position::Fina, Some(Condition::Feminine));
811 assert_eq!(token.written, Some(&[WrittenUnit::H][..]));
812 }
813
814 #[test]
815 fn memoised_written_is_never_invalidated() {
816 let shaper = Shaper::new(Locale::Mng);
817 let mut token = shaper.tokenize("\u{1820}").remove(0);
820 token.position = Position::Fina;
821 shaper.resolve_written(&mut token);
822 assert_eq!(token.written, Some(&[WrittenUnit::A][..]));
823 token.condition = Some(Condition::PostBowed);
824 shaper.resolve_written(&mut token);
825 assert_eq!(token.written, Some(&[WrittenUnit::A][..]));
826
827 assert_eq!(
833 shaper
834 .shape("\u{182A}\u{1820}\u{180E}\u{180A}\u{1822}\u{1828}")
835 .unwrap(),
836 vec![
837 WrittenUnit::B,
838 WrittenUnit::Aa,
839 WrittenUnit::Mvs,
840 WrittenUnit::Nirugu,
841 WrittenUnit::I,
842 WrittenUnit::A,
843 ]
844 );
845 }
846
847 #[test]
848 fn debug_reports_the_index_sizes() {
849 let shaper = Shaper::new(Locale::Mng);
850 let text = format!("{shaper:?}");
851 assert!(text.starts_with("Shaper { locale: Mng,"), "{text}");
852 assert!(text.contains("letters: 35"), "{text}");
853 assert!(
854 text.contains(&format!("rules: {}", shaper.rule_names().len())),
855 "{text}"
856 );
857 assert!(text.ends_with(".. }"), "{text}");
858 }
859
860 #[test]
861 fn unknown_letters_shape_to_nothing() {
862 let shaper = Shaper::new(Locale::Mng);
864 assert_eq!(shaper.shape("\u{181A}").unwrap(), Vec::<WrittenUnit>::new());
865 let details = shaper.shape_detailed("\u{181A}").unwrap();
866 assert_eq!(details[0].alias, None);
867 }
868}