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