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(test)]
165 pub(crate) fn with_empty_normalize_table(locale: Locale) -> Shaper {
166 let mut shaper = Shaper::new(locale);
167 let version = shaper
168 .normalize
169 .as_ref()
170 .map_or("mng-canonical/1", |table| table.canonical_version);
171 shaper.normalize = Some(NormalizeTable::empty(version));
172 shaper
173 }
174
175 pub fn locale(&self) -> Locale {
177 self.locale
178 }
179
180 pub fn rule_names(&self) -> Vec<&'static str> {
182 self.rules.iter().map(|rule| rule.name).collect()
183 }
184
185 pub(crate) fn alias_of(&self, cp: u32) -> Option<Alias> {
188 self.letters.get(&cp).map(|letter| letter.alias)
189 }
190
191 pub(crate) fn tokenize(&self, text: &str) -> Vec<Token> {
192 tokenize(text, |cp| self.alias_of(cp))
193 }
194
195 pub(crate) fn particle(&self, key: &[ParticleSym]) -> Option<&'static [usize]> {
197 self.particles.get(key).copied()
198 }
199
200 fn condition_fvs(
206 &self,
207 cp: u32,
208 position: Position,
209 condition: Condition,
210 ) -> Option<Option<Fvs>> {
211 self.letters
212 .get(&cp)?
213 .variants
214 .iter()
215 .find(|variant| variant.position == position && variant.conditions.contains(&condition))
216 .map(|variant| variant.fvs)
217 }
218
219 pub(crate) fn resolve_written(&self, token: &mut Token) {
228 if token.written.is_some() {
229 return;
230 }
231 if !token.is_letter() {
232 token.written = Some(&[]);
233 return;
234 }
235 let mut written: Option<&'static [WrittenUnit]> = None;
236 for &fvs in &token.fvs {
237 if let Some(variant) = self.variants.get(&(token.cp, token.position, Some(fvs))) {
238 written = Some(variant.written);
239 break;
240 }
241 }
242 if written.is_none() {
243 if let Some(condition) = token.condition {
244 if let Some(fvs) = self.condition_fvs(token.cp, token.position, condition) {
245 written = self
246 .variants
247 .get(&(token.cp, token.position, fvs))
248 .map(|variant| variant.written);
249 }
250 }
251 }
252 if written.is_none() {
253 written = self
254 .defaults
255 .get(&(token.cp, token.position))
256 .map(|variant| variant.written);
257 }
258 token.written = Some(written.unwrap_or(&[]));
259 }
260
261 pub(crate) fn is_vowel(&self, token: &Token) -> bool {
264 token.is_letter()
265 && token
266 .alias
267 .is_some_and(|alias| self.vowels.contains(&alias))
268 }
269
270 pub(crate) fn is_consonant(&self, token: &Token) -> bool {
271 token.is_letter()
272 && token
273 .alias
274 .is_some_and(|alias| self.consonants.contains(&alias))
275 }
276
277 pub(crate) fn is_masc_vowel(&self, token: &Token) -> bool {
278 token
279 .alias
280 .is_some_and(|alias| self.masculine.contains(&alias))
281 }
282
283 pub(crate) fn is_fem_vowel(&self, token: &Token) -> bool {
284 token
285 .alias
286 .is_some_and(|alias| self.feminine.contains(&alias))
287 }
288
289 pub(crate) fn is_neut_vowel(&self, token: &Token) -> bool {
290 token
291 .alias
292 .is_some_and(|alias| self.neuter.contains(&alias))
293 }
294
295 pub(crate) fn masc_marker_reaches_g_h(&self, tokens: &[Token], idx: usize) -> bool {
300 let mut j = idx;
302 while j > 0 {
303 j -= 1;
304 let token = &tokens[j];
305 if !token.is_letter() {
306 if token.is_nirugu() {
307 continue;
308 }
309 break; }
311 if self.is_fem_vowel(token) {
312 break;
313 }
314 if self.is_masc_vowel(token)
315 && matches!(token.position, Position::Init | Position::Medi)
316 {
317 return true;
318 }
319 }
320
321 if !matches!(tokens[idx].position, Position::Init | Position::Medi) {
323 return false;
324 }
325 let mut j = idx;
327 while j > 0 {
328 j -= 1;
329 let token = &tokens[j];
330 if !token.is_letter() {
331 if token.is_nirugu() {
332 continue;
333 }
334 break;
335 }
336 if self.is_fem_vowel(token) {
337 return false;
338 }
339 }
340 let mut j = idx + 1;
343 while j < tokens.len() {
344 let next = &tokens[j];
345 if !next.is_letter() {
346 if next.is_nirugu() {
347 j += 1;
348 continue;
349 }
350 return false;
351 }
352 if self.is_masc_vowel(next) {
353 return true;
354 }
355 if self.is_fem_vowel(next) {
356 return false;
357 }
358 if matches!(next.position, Position::Init | Position::Medi) {
359 j += 1;
360 continue;
361 }
362 let mut k = j + 1;
363 while k < tokens.len() && tokens[k].is_nirugu() {
364 k += 1;
365 }
366 return next.position == Position::Fina
367 && k < tokens.len()
368 && tokens[k].is_mvs()
369 && k + 1 < tokens.len()
370 && tokens[k + 1].is_letter()
371 && tokens[k + 1].alias == Some(Alias::A)
372 && tokens[k + 1].position == Position::Isol;
373 }
374 false
375 }
376
377 fn prepare(&self, text: &str) -> Result<Vec<Token>, Error> {
382 check_word_chars(text)?;
383 let mut tokens = self.tokenize(text);
384 assign_positions(&mut tokens);
385 Ok(tokens)
386 }
387
388 fn resolve_all(&self, tokens: &mut [Token]) {
390 for token in tokens {
391 self.resolve_written(token);
392 }
393 }
394
395 fn run_pipeline(&self, text: &str) -> Result<Vec<Token>, Error> {
396 let mut tokens = self.prepare(text)?;
397 rules::run_rules(self.rules, &mut tokens, self);
398 self.resolve_all(&mut tokens);
399 Ok(tokens)
400 }
401
402 pub fn shape(&self, text: &str) -> Result<Vec<WrittenUnit>, Error> {
408 Ok(flatten(&self.run_pipeline(text)?))
409 }
410
411 pub fn shape_str(&self, text: &str) -> Result<String, Error> {
413 let units = self.shape(text)?;
414 let mut out = String::new();
415 for unit in units {
416 if !out.is_empty() {
417 out.push('+');
418 }
419 out.push_str(unit.as_str());
420 }
421 Ok(out)
422 }
423
424 pub fn same_shape(&self, a: &str, b: &str) -> Result<bool, Error> {
426 Ok(self.shape(a)? == self.shape(b)?)
427 }
428
429 pub fn shape_detailed(&self, text: &str) -> Result<Vec<TokenDetail>, Error> {
431 let tokens = self.run_pipeline(text)?;
432 Ok(tokens
433 .iter()
434 .map(|token| TokenDetail {
435 cp: char::from_u32(token.cp).expect("token code points are scalar values"),
436 alias: token.alias,
437 position: token.position,
438 fvs: token.first_fvs(),
439 condition: token.condition,
440 written: token
441 .written
442 .map(<[WrittenUnit]>::to_vec)
443 .unwrap_or_default(),
444 })
445 .collect())
446 }
447
448 pub fn trace(&self, text: &str) -> Result<ShapeTrace, Error> {
450 let mut tokens = self.prepare(text)?;
451 let mut transitions = Vec::new();
452 for rule in self.rules {
453 let before: Vec<Option<Condition>> =
454 tokens.iter().map(|token| token.condition).collect();
455 (rule.apply)(&mut tokens, self);
456 let changes: Vec<ConditionChange> = before
457 .iter()
458 .zip(&tokens)
459 .enumerate()
460 .filter(|(_, (old, token))| **old != token.condition)
461 .map(|(index, (old, token))| ConditionChange {
462 token: index,
463 before: *old,
464 after: token.condition,
465 })
466 .collect();
467 if !changes.is_empty() {
468 transitions.push(RuleTransition {
469 rule: rule.name,
470 changes,
471 });
472 }
473 }
474 self.resolve_all(&mut tokens);
475 Ok(ShapeTrace {
476 positions: tokens.iter().map(|token| token.position).collect(),
477 transitions,
478 final_conditions: tokens.iter().map(|token| token.condition).collect(),
479 written_by_token: tokens
480 .iter()
481 .map(|token| {
482 token
483 .written
484 .map(<[WrittenUnit]>::to_vec)
485 .unwrap_or_default()
486 })
487 .collect(),
488 shape: flatten(&tokens),
489 })
490 }
491}
492
493pub(crate) fn flatten(tokens: &[Token]) -> Vec<WrittenUnit> {
495 let mut shape = Vec::with_capacity(tokens.len() * 2);
496 for token in tokens {
497 match token.kind {
498 TokenKind::Mvs => shape.push(WrittenUnit::Mvs),
499 TokenKind::Nirugu => shape.push(WrittenUnit::Nirugu),
500 TokenKind::Zwj => shape.push(WrittenUnit::Zwj),
501 TokenKind::Letter => {
502 if let Some(written) = token.written {
503 shape.extend_from_slice(written);
504 }
505 }
506 }
507 }
508 shape
509}
510
511pub(crate) fn prev_letter(tokens: &[Token], index: usize) -> Option<usize> {
515 (0..index).rev().find(|&j| tokens[j].is_letter())
516}
517
518pub(crate) fn next_letter(tokens: &[Token], index: usize) -> Option<usize> {
520 (index + 1..tokens.len()).find(|&j| tokens[j].is_letter())
521}
522
523pub(crate) fn prev_tok(_tokens: &[Token], index: usize) -> Option<usize> {
526 if index > 0 {
527 Some(index - 1)
528 } else {
529 None
530 }
531}
532
533pub(crate) fn next_tok(tokens: &[Token], index: usize) -> Option<usize> {
535 if index + 1 < tokens.len() {
536 Some(index + 1)
537 } else {
538 None
539 }
540}
541
542pub(crate) fn prev_adjacent_letter(tokens: &[Token], index: usize) -> Option<usize> {
544 let mut j = index;
545 while j > 0 {
546 j -= 1;
547 let token = &tokens[j];
548 if token.is_mvs() {
549 return None;
550 }
551 if token.is_letter() {
552 return Some(j);
553 }
554 if token.is_nirugu() {
555 continue;
556 }
557 return None;
558 }
559 None
560}
561
562pub(crate) fn next_adjacent_letter(tokens: &[Token], index: usize) -> Option<usize> {
564 let mut j = index + 1;
565 while j < tokens.len() {
566 let token = &tokens[j];
567 if token.is_mvs() {
568 return None;
569 }
570 if token.is_letter() {
571 return Some(j);
572 }
573 if token.is_nirugu() {
574 j += 1;
575 continue;
576 }
577 return None;
578 }
579 None
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
587 fn default_forms_without_rules() {
588 let shaper = Shaper::new(Locale::Mng);
589 assert_eq!(
590 shaper.shape("\u{1820}").unwrap(),
591 vec![WrittenUnit::A, WrittenUnit::A]
592 );
593 assert_eq!(
594 shaper.shape("\u{1820}\u{180B}").unwrap(),
595 vec![WrittenUnit::A]
596 );
597 assert_eq!(shaper.shape("\u{180E}").unwrap(), vec![WrittenUnit::Mvs]);
598 assert_eq!(
599 shaper.shape("\u{180A}\u{1823}").unwrap(),
600 vec![WrittenUnit::Nirugu, WrittenUnit::U]
601 );
602 assert_eq!(
603 shaper.shape("\u{200D}\u{1833}").unwrap(),
604 vec![WrittenUnit::Zwj, WrittenUnit::Dd]
605 );
606 assert_eq!(shaper.shape("").unwrap(), Vec::<WrittenUnit>::new());
607 assert_eq!(shaper.shape_str("\u{1820}").unwrap(), "A+A");
608 assert!(shaper.same_shape("\u{1820}", "\u{1820}").unwrap());
609 assert_eq!(
610 shaper.shape("\u{1820} "),
611 Err(Error::NonMongolianChar { ch: ' ', index: 1 })
612 );
613 for locale in [Locale::Tod, Locale::Sib, Locale::Mch] {
614 assert!(!Shaper::new(locale).shape("\u{1820}").unwrap().is_empty());
615 assert!(Shaper::new(locale).rule_names().is_empty());
616 }
617 }
618
619 #[test]
620 fn detailed_and_trace_cover_every_token() {
621 let shaper = Shaper::new(Locale::Mng);
622 let details = shaper
623 .shape_detailed("\u{1832}\u{1820}\u{182F}\u{202F}\u{1820}")
624 .unwrap();
625 let positions: Vec<Position> = details.iter().map(|d| d.position).collect();
626 assert_eq!(
627 positions,
628 vec![
629 Position::Init,
630 Position::Medi,
631 Position::Fina,
632 Position::Isol,
633 Position::Isol
634 ]
635 );
636 assert_eq!(details[3].cp, '\u{180E}'); assert_eq!(details[3].written, Vec::<WrittenUnit>::new());
638 assert_eq!(details[0].alias, Some(Alias::T));
639 let trace = shaper.trace("\u{1820}").unwrap();
640 assert_eq!(trace.positions, vec![Position::Isol]);
641 assert!(trace.transitions.is_empty());
642 assert_eq!(trace.final_conditions, vec![None]);
643 assert_eq!(
644 trace.written_by_token,
645 vec![vec![WrittenUnit::A, WrittenUnit::A]]
646 );
647 assert_eq!(trace.shape, shaper.shape("\u{1820}").unwrap());
648 }
649
650 fn prepared(shaper: &Shaper, text: &str) -> Vec<Token> {
652 let mut tokens = shaper.tokenize(text);
653 assign_positions(&mut tokens);
654 tokens
655 }
656
657 fn resolved(
659 shaper: &Shaper,
660 cp: u32,
661 position: Position,
662 condition: Option<Condition>,
663 ) -> Token {
664 let text = char::from_u32(cp).expect("scalar value").to_string();
665 let mut token = shaper.tokenize(&text).remove(0);
666 token.position = position;
667 token.condition = condition;
668 shaper.resolve_written(&mut token);
669 token
670 }
671
672 #[test]
673 fn masc_marker_reaches_g_h_paths() {
674 let shaper = Shaper::new(Locale::Mng);
675 let tokens = prepared(&shaper, "\u{1820}\u{182F}\u{182D}");
681 assert!(shaper.masc_marker_reaches_g_h(&tokens, 2));
682
683 let tokens = prepared(
687 &shaper,
688 "\u{1830}\u{1822}\u{182D}\u{1830}\u{1822}\u{182D}\u{1820}",
689 );
690 assert!(shaper.masc_marker_reaches_g_h(&tokens, 2));
691
692 let tokens = prepared(&shaper, "\u{1821}\u{182F}\u{182D}"); assert!(!shaper.masc_marker_reaches_g_h(&tokens, 2));
695 let tokens = prepared(
697 &shaper,
698 "\u{1821}\u{182D}\u{1821}\u{1828}\u{1822}\u{182D}\u{1832}\u{1820}",
699 );
700 assert!(!shaper.masc_marker_reaches_g_h(&tokens, 5));
701
702 let tokens = prepared(&shaper, "\u{1822}\u{182D}\u{182F}\u{180E}\u{1820}");
705 assert!(shaper.masc_marker_reaches_g_h(&tokens, 1));
706 let tokens = prepared(&shaper, "\u{1822}\u{182D}\u{182F}\u{180E}\u{1821}");
708 assert!(!shaper.masc_marker_reaches_g_h(&tokens, 1));
709 }
710
711 #[test]
712 fn resolve_written_condition_branch() {
713 let shaper = Shaper::new(Locale::Mng);
714
715 assert_eq!(
718 shaper.condition_fvs(0x182C, Position::Fina, Condition::MasculineDevsger),
719 Some(None)
720 );
721 let token = resolved(
722 &shaper,
723 0x182C,
724 Position::Fina,
725 Some(Condition::MasculineDevsger),
726 );
727 assert_eq!(token.written, Some(&[WrittenUnit::H][..]));
728
729 let (letter, variant, condition) = mng::DATA
732 .letters
733 .iter()
734 .flat_map(|letter| letter.variants.iter().map(move |variant| (letter, variant)))
735 .filter_map(|(letter, variant)| {
736 let condition = *variant.conditions.first()?;
737 variant.fvs?;
738 let default = shaper.defaults.get(&(letter.cp, variant.position))?;
739 (default.written != variant.written
740 && shaper.condition_fvs(letter.cp, variant.position, condition)
741 == Some(variant.fvs))
742 .then_some((letter, variant, condition))
743 })
744 .next()
745 .expect("MNG has an FVS variant selected by a condition");
746 let default = shaper.defaults[&(letter.cp, variant.position)].written;
747 let token = resolved(&shaper, letter.cp, variant.position, Some(condition));
748 assert_eq!(
749 token.written,
750 Some(variant.written),
751 "U+{:04X} {} {}",
752 letter.cp,
753 variant.position,
754 condition.as_str()
755 );
756 assert_ne!(token.written, Some(default));
757
758 let mut token = shaper.tokenize("\u{182C}\u{180B}").remove(0);
761 token.position = Position::Init;
762 token.condition = Some(Condition::Feminine);
763 shaper.resolve_written(&mut token);
764 assert_eq!(token.written, Some(&[WrittenUnit::Hx][..]));
765
766 assert_eq!(
769 shaper.condition_fvs(0x182C, Position::Fina, Condition::Feminine),
770 None
771 );
772 let token = resolved(&shaper, 0x182C, Position::Fina, Some(Condition::Feminine));
773 assert_eq!(token.written, Some(&[WrittenUnit::H][..]));
774 }
775
776 #[test]
777 fn memoised_written_is_never_invalidated() {
778 let shaper = Shaper::new(Locale::Mng);
779 let text = "\u{182A}\u{1820}\u{180E}\u{180A}\u{1822}\u{1828}";
784 let details = shaper.shape_detailed(text).unwrap();
785 assert_eq!(details[1].alias, Some(Alias::A));
786 assert_eq!(details[1].condition, Some(Condition::PostBowed));
787 assert_eq!(details[1].written, vec![WrittenUnit::A]);
788 assert_eq!(
789 shaper.shape(text).unwrap(),
790 vec![
791 WrittenUnit::B,
792 WrittenUnit::A,
793 WrittenUnit::Mvs,
794 WrittenUnit::Nirugu,
795 WrittenUnit::I,
796 WrittenUnit::I,
797 WrittenUnit::A,
798 ]
799 );
800 assert_eq!(
802 shaper.shape("\u{182A}\u{1820}").unwrap(),
803 vec![WrittenUnit::B, WrittenUnit::Aa]
804 );
805 }
806
807 #[test]
808 fn debug_reports_the_index_sizes() {
809 let shaper = Shaper::new(Locale::Mng);
810 let text = format!("{shaper:?}");
811 assert!(text.starts_with("Shaper { locale: Mng,"), "{text}");
812 assert!(text.contains("letters: 35"), "{text}");
813 assert!(
814 text.contains(&format!("rules: {}", shaper.rule_names().len())),
815 "{text}"
816 );
817 assert!(text.ends_with(".. }"), "{text}");
818 }
819
820 #[test]
821 fn unknown_letters_shape_to_nothing() {
822 let shaper = Shaper::new(Locale::Mng);
824 assert_eq!(shaper.shape("\u{181A}").unwrap(), Vec::<WrittenUnit>::new());
825 let details = shaper.shape_detailed("\u{181A}").unwrap();
826 assert_eq!(details[0].alias, None);
827 }
828}