1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
7#[repr(u8)]
8pub enum CandidateSource {
9 #[default]
10 Database = 0,
11 UserDatabase = 1,
12 CloudSuggestion = 2,
13 AiSuggestion = 3,
14 EnglishDictionary = 4,
15 QuickPhrase = 5,
16 Emoji = 6,
18 Kaomoji = 7,
19 Generated = 8,
20 Fallback = 9,
21 NeuralKeyboard = 11,
23 AiPending = 12,
25 MixedJapanese = 13,
27 Plugin = 14,
29 CloudPending = 15,
31 Contact = 16,
33 Collocation = 17,
36 Multilingual = 18,
38}
39
40impl CandidateSource {
41 pub fn from_u8(value: u8) -> Option<Self> {
42 Some(match value {
43 0 => Self::Database,
44 1 => Self::UserDatabase,
45 2 => Self::CloudSuggestion,
46 3 => Self::AiSuggestion,
47 4 => Self::EnglishDictionary,
48 5 => Self::QuickPhrase,
49 6 => Self::Emoji,
50 7 => Self::Kaomoji,
51 8 => Self::Generated,
52 9 => Self::Fallback,
53 11 => Self::NeuralKeyboard,
54 12 => Self::AiPending,
55 13 => Self::MixedJapanese,
56 14 => Self::Plugin,
57 15 => Self::CloudPending,
58 16 => Self::Contact,
59 17 => Self::Collocation,
60 18 => Self::Multilingual,
61 _ => return None,
62 })
63 }
64
65 pub fn is_online(self) -> bool {
66 matches!(self, Self::CloudSuggestion | Self::AiSuggestion)
67 }
68
69 pub fn is_cloud_pending(self) -> bool {
70 self == Self::CloudPending
71 }
72
73 pub fn holds_ai_seat(self) -> bool {
75 matches!(self, Self::AiSuggestion | Self::AiPending)
76 }
77
78 pub fn takes_online_seat(self) -> bool {
80 self.is_online() || matches!(self, Self::AiPending | Self::CloudPending)
81 }
82
83 pub fn is_generated_or_fallback(self) -> bool {
86 matches!(self, Self::Generated | Self::Fallback | Self::Collocation)
87 }
88
89 pub fn is_sentence_learning(self) -> bool {
91 self.is_generated_or_fallback() || self.is_online() || matches!(self, Self::NeuralKeyboard)
92 }
93
94 pub fn is_dictionary(self) -> bool {
95 matches!(self, Self::Database | Self::UserDatabase)
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
101#[repr(u8)]
102pub enum SchemeType {
103 #[default]
104 Quanpin = 0,
105 Shuangpin = 1,
106 Wubi = 2,
107 JapaneseRomaji = 3,
108 Korean = 4,
110 Cantonese = 5,
112 Zhuyin = 6,
114 Vietnamese = 7,
116 Tibetan = 8,
118 Stroke = 9,
120}
121
122impl SchemeType {
123 pub fn from_u8(value: u8) -> Option<Self> {
124 Some(match value {
125 0 => Self::Quanpin,
126 1 => Self::Shuangpin,
127 2 => Self::Wubi,
128 3 => Self::JapaneseRomaji,
129 4 => Self::Korean,
130 5 => Self::Cantonese,
131 6 => Self::Zhuyin,
132 7 => Self::Vietnamese,
133 8 => Self::Tibetan,
134 9 => Self::Stroke,
135 _ => return None,
136 })
137 }
138
139 pub fn name(self) -> &'static str {
141 match self {
142 Self::Quanpin => "quanpin",
143 Self::Shuangpin => "shuangpin",
144 Self::Wubi => "wubi",
145 Self::JapaneseRomaji => "japanese",
146 Self::Korean => "korean",
147 Self::Cantonese => "cantonese",
148 Self::Zhuyin => "zhuyin",
149 Self::Vietnamese => "vietnamese",
150 Self::Tibetan => "tibetan",
151 Self::Stroke => "stroke",
152 }
153 }
154
155 pub fn is_pinyin(self) -> bool {
156 matches!(self, Self::Quanpin | Self::Shuangpin)
157 }
158
159 pub const fn is_chinese(self) -> bool {
165 match self {
166 Self::Quanpin
167 | Self::Shuangpin
168 | Self::Wubi
169 | Self::Cantonese
170 | Self::Stroke
171 | Self::Zhuyin => true,
172 Self::JapaneseRomaji | Self::Korean | Self::Vietnamese | Self::Tibetan => false,
173 }
174 }
175
176 pub const fn outputs_traditional_natively(self) -> bool {
178 match self {
179 Self::Quanpin
180 | Self::Shuangpin
181 | Self::Wubi
182 | Self::JapaneseRomaji
183 | Self::Korean
184 | Self::Vietnamese
185 | Self::Tibetan => false,
186 Self::Cantonese | Self::Stroke | Self::Zhuyin => true,
187 }
188 }
189
190 pub const fn script_conversion_applies(self) -> bool {
192 match self {
193 Self::Quanpin | Self::Shuangpin | Self::Wubi => true,
194 Self::JapaneseRomaji
195 | Self::Korean
196 | Self::Vietnamese
197 | Self::Tibetan
198 | Self::Cantonese
199 | Self::Stroke
200 | Self::Zhuyin => false,
201 }
202 }
203
204 pub const fn uses_chinese_punctuation(self) -> bool {
206 match self {
207 Self::Quanpin
208 | Self::Shuangpin
209 | Self::Wubi
210 | Self::JapaneseRomaji
211 | Self::Cantonese
212 | Self::Stroke
213 | Self::Zhuyin => true,
214 Self::Korean | Self::Vietnamese | Self::Tibetan => false,
215 }
216 }
217
218 pub const fn host_smart_punctuation(self) -> bool {
220 match self {
221 Self::Quanpin | Self::Shuangpin | Self::Wubi | Self::Cantonese | Self::Stroke => true,
222 Self::JapaneseRomaji
223 | Self::Korean
224 | Self::Vietnamese
225 | Self::Tibetan
226 | Self::Zhuyin => false,
227 }
228 }
229
230 pub const fn widens_full_width(self) -> bool {
232 match self {
233 Self::Quanpin
234 | Self::Shuangpin
235 | Self::Wubi
236 | Self::JapaneseRomaji
237 | Self::Cantonese
238 | Self::Stroke
239 | Self::Zhuyin => true,
240 Self::Korean | Self::Vietnamese | Self::Tibetan => false,
241 }
242 }
243
244 pub const fn allows_english_emoji_mixing(self) -> bool {
246 match self {
247 Self::Quanpin | Self::Shuangpin => true,
248 Self::Wubi
249 | Self::JapaneseRomaji
250 | Self::Korean
251 | Self::Vietnamese
252 | Self::Tibetan
253 | Self::Cantonese
254 | Self::Stroke
255 | Self::Zhuyin => false,
256 }
257 }
258
259 pub const fn opens_local_modes(self) -> bool {
261 match self {
262 Self::Quanpin | Self::Shuangpin => true,
263 Self::Wubi
264 | Self::JapaneseRomaji
265 | Self::Korean
266 | Self::Vietnamese
267 | Self::Tibetan
268 | Self::Cantonese
269 | Self::Stroke
270 | Self::Zhuyin => false,
271 }
272 }
273
274 pub const fn opens_table_modes(self) -> bool {
276 match self {
277 Self::Quanpin | Self::Shuangpin | Self::Wubi => true,
278 Self::JapaneseRomaji
279 | Self::Korean
280 | Self::Vietnamese
281 | Self::Tibetan
282 | Self::Cantonese
283 | Self::Stroke
284 | Self::Zhuyin => false,
285 }
286 }
287
288 pub const fn filters_to_single_characters(self) -> bool {
290 match self {
291 Self::Quanpin | Self::Shuangpin | Self::Wubi | Self::Cantonese => true,
292 Self::JapaneseRomaji
293 | Self::Korean
294 | Self::Vietnamese
295 | Self::Tibetan
296 | Self::Stroke
297 | Self::Zhuyin => false,
298 }
299 }
300
301 pub const fn detects_urls(self) -> bool {
303 match self {
304 Self::Quanpin | Self::Shuangpin | Self::Wubi => true,
305 Self::JapaneseRomaji
306 | Self::Korean
307 | Self::Vietnamese
308 | Self::Tibetan
309 | Self::Cantonese
310 | Self::Stroke
311 | Self::Zhuyin => false,
312 }
313 }
314
315 pub const fn learns_into_main_dictionary(self) -> bool {
317 match self {
318 Self::Quanpin | Self::Shuangpin | Self::Wubi => true,
319 Self::JapaneseRomaji
320 | Self::Korean
321 | Self::Vietnamese
322 | Self::Tibetan
323 | Self::Cantonese
324 | Self::Stroke
325 | Self::Zhuyin => false,
326 }
327 }
328
329 pub const fn learns_english_words(self) -> bool {
331 match self {
332 Self::Quanpin | Self::Shuangpin => true,
333 Self::Wubi
334 | Self::JapaneseRomaji
335 | Self::Korean
336 | Self::Vietnamese
337 | Self::Tibetan
338 | Self::Cantonese
339 | Self::Stroke
340 | Self::Zhuyin => false,
341 }
342 }
343
344 pub const fn cloud_eligible(self) -> bool {
346 match self {
347 Self::Quanpin | Self::Shuangpin | Self::JapaneseRomaji => true,
348 Self::Wubi
349 | Self::Korean
350 | Self::Vietnamese
351 | Self::Tibetan
352 | Self::Cantonese
353 | Self::Stroke
354 | Self::Zhuyin => false,
355 }
356 }
357
358 pub const fn shows_glosses(self) -> bool {
360 match self {
361 Self::Quanpin | Self::Shuangpin | Self::Wubi | Self::Korean => true,
362 Self::JapaneseRomaji
363 | Self::Vietnamese
364 | Self::Tibetan
365 | Self::Cantonese
366 | Self::Stroke
367 | Self::Zhuyin => false,
368 }
369 }
370
371 pub const fn commits_on_blur(self) -> bool {
373 match self {
374 Self::Korean | Self::Vietnamese | Self::Tibetan | Self::Zhuyin => true,
375 Self::Quanpin
376 | Self::Shuangpin
377 | Self::Wubi
378 | Self::JapaneseRomaji
379 | Self::Cantonese
380 | Self::Stroke => false,
381 }
382 }
383
384 pub const fn holds_phrase_progress(self) -> bool {
386 match self {
387 Self::Quanpin | Self::Shuangpin | Self::Wubi | Self::JapaneseRomaji => true,
388 Self::Korean
389 | Self::Vietnamese
390 | Self::Tibetan
391 | Self::Cantonese
392 | Self::Stroke
393 | Self::Zhuyin => false,
394 }
395 }
396
397 pub const fn reranks_with_sentence_model(self) -> bool {
399 match self {
400 Self::Quanpin | Self::Shuangpin => true,
401 Self::Wubi
402 | Self::JapaneseRomaji
403 | Self::Korean
404 | Self::Vietnamese
405 | Self::Tibetan
406 | Self::Cantonese
407 | Self::Stroke
408 | Self::Zhuyin => false,
409 }
410 }
411
412 pub const fn draws_reading(self) -> bool {
414 match self {
415 Self::JapaneseRomaji | Self::Korean | Self::Zhuyin | Self::Stroke => true,
416 Self::Quanpin
417 | Self::Shuangpin
418 | Self::Wubi
419 | Self::Vietnamese
420 | Self::Tibetan
421 | Self::Cantonese => false,
422 }
423 }
424
425 pub const fn has_openable_candidate_list(self) -> bool {
427 match self {
428 Self::Korean | Self::Zhuyin => true,
429 Self::Quanpin
430 | Self::Shuangpin
431 | Self::Wubi
432 | Self::JapaneseRomaji
433 | Self::Vietnamese
434 | Self::Tibetan
435 | Self::Cantonese
436 | Self::Stroke => false,
437 }
438 }
439
440 pub const fn cancel_keeps_composition(self) -> bool {
442 match self {
443 Self::Korean | Self::Zhuyin | Self::Vietnamese | Self::Tibetan => true,
444 Self::Quanpin
445 | Self::Shuangpin
446 | Self::Wubi
447 | Self::JapaneseRomaji
448 | Self::Cantonese
449 | Self::Stroke => false,
450 }
451 }
452
453 pub const fn selection_completes(self) -> bool {
455 match self {
456 Self::JapaneseRomaji | Self::Korean | Self::Vietnamese | Self::Tibetan => true,
457 Self::Quanpin
458 | Self::Shuangpin
459 | Self::Wubi
460 | Self::Cantonese
461 | Self::Stroke
462 | Self::Zhuyin => false,
463 }
464 }
465
466 pub const fn follows_pinyin_candidates(self) -> bool {
468 match self {
469 Self::Quanpin | Self::Shuangpin => true,
470 Self::Wubi
471 | Self::JapaneseRomaji
472 | Self::Korean
473 | Self::Vietnamese
474 | Self::Tibetan
475 | Self::Cantonese
476 | Self::Stroke
477 | Self::Zhuyin => false,
478 }
479 }
480
481 pub const fn accepts_apostrophe(self) -> bool {
483 match self {
484 Self::Quanpin | Self::Shuangpin | Self::JapaneseRomaji | Self::Cantonese => true,
485 Self::Wubi
486 | Self::Korean
487 | Self::Vietnamese
488 | Self::Tibetan
489 | Self::Zhuyin
490 | Self::Stroke => false,
491 }
492 }
493
494 pub const fn locks_caret(self) -> bool {
496 match self {
497 Self::Korean | Self::Vietnamese | Self::Tibetan | Self::Zhuyin => true,
498 Self::Quanpin
499 | Self::Shuangpin
500 | Self::Wubi
501 | Self::JapaneseRomaji
502 | Self::Cantonese
503 | Self::Stroke => false,
504 }
505 }
506
507 pub const fn supports_fuzzy(self) -> bool {
509 match self {
510 Self::Quanpin | Self::Shuangpin => true,
511 Self::Wubi
512 | Self::JapaneseRomaji
513 | Self::Korean
514 | Self::Vietnamese
515 | Self::Tibetan
516 | Self::Cantonese
517 | Self::Stroke
518 | Self::Zhuyin => false,
519 }
520 }
521
522 pub const fn supports_autocorrect(self) -> bool {
524 match self {
525 Self::Quanpin => true,
526 Self::Shuangpin
527 | Self::Wubi
528 | Self::JapaneseRomaji
529 | Self::Korean
530 | Self::Vietnamese
531 | Self::Tibetan
532 | Self::Cantonese
533 | Self::Stroke
534 | Self::Zhuyin => false,
535 }
536 }
537
538 pub const fn nine_key(self) -> bool {
540 match self {
541 Self::Quanpin | Self::Zhuyin => true,
542 Self::Shuangpin
543 | Self::Wubi
544 | Self::JapaneseRomaji
545 | Self::Korean
546 | Self::Vietnamese
547 | Self::Tibetan
548 | Self::Cantonese
549 | Self::Stroke => false,
550 }
551 }
552
553 pub const fn helpcode(self) -> bool {
555 match self {
556 Self::Quanpin | Self::Shuangpin => true,
557 Self::Wubi
558 | Self::JapaneseRomaji
559 | Self::Korean
560 | Self::Vietnamese
561 | Self::Tibetan
562 | Self::Cantonese
563 | Self::Stroke
564 | Self::Zhuyin => false,
565 }
566 }
567}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
573pub struct SchemeSet(u16);
574
575impl SchemeSet {
576 const LAST: SchemeType = SchemeType::Stroke;
578 pub const ALL: Self = Self((1 << (Self::LAST as u16 + 1)) - 1);
580 pub const EMPTY: Self = Self(0);
581
582 pub const fn contains(self, scheme: SchemeType) -> bool {
583 self.0 & (1 << scheme as u16) != 0
584 }
585
586 #[must_use]
587 pub const fn with(self, scheme: SchemeType) -> Self {
588 Self(self.0 | (1 << scheme as u16))
589 }
590
591 pub fn of(schemes: &[SchemeType]) -> Self {
593 schemes
594 .iter()
595 .fold(Self::EMPTY, |set, scheme| set.with(*scheme))
596 }
597
598 pub const fn reads_main_dictionary(self) -> bool {
600 self.contains(SchemeType::Quanpin)
601 || self.contains(SchemeType::Shuangpin)
602 || self.contains(SchemeType::Wubi)
603 }
604
605 pub const fn requires_english_dictionary(self) -> bool {
607 self.contains(SchemeType::Quanpin)
608 || self.contains(SchemeType::Shuangpin)
609 || self.contains(SchemeType::Wubi)
610 }
611}
612
613impl Default for SchemeSet {
614 fn default() -> Self {
615 Self::ALL
616 }
617}
618
619#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
621#[repr(u8)]
622pub enum ShuangpinProfileKind {
623 #[default]
624 Xiaohe = 0,
625 Ziranma = 1,
626 Shoudao = 2,
627 Microsoft = 3,
628 Custom = 4,
630 SmartAbc = 5,
631}
632
633impl ShuangpinProfileKind {
634 pub fn from_u8(value: u8) -> Option<Self> {
635 Some(match value {
636 0 => Self::Xiaohe,
637 1 => Self::Ziranma,
638 2 => Self::Shoudao,
639 3 => Self::Microsoft,
640 4 => Self::Custom,
641 5 => Self::SmartAbc,
642 _ => return None,
643 })
644 }
645
646 pub fn from_name(name: &str) -> Option<Self> {
648 Some(match name {
649 "xiaohe" => Self::Xiaohe,
650 "ziranma" => Self::Ziranma,
651 "shoudao" => Self::Shoudao,
652 "microsoft" => Self::Microsoft,
653 "custom" => Self::Custom,
654 "smart_abc" => Self::SmartAbc,
655 _ => return None,
656 })
657 }
658
659 pub fn name(self) -> &'static str {
660 match self {
661 Self::Xiaohe => "xiaohe",
662 Self::Ziranma => "ziranma",
663 Self::Shoudao => "shoudao",
664 Self::Microsoft => "microsoft",
665 Self::Custom => "custom",
666 Self::SmartAbc => "smart_abc",
667 }
668 }
669}
670
671#[derive(Debug, Clone, PartialEq, Eq, Default)]
673pub struct ShuangpinCustomTable {
674 pub initials: Vec<(String, String)>,
675 pub finals: Vec<(String, String)>,
676 pub zero_initials: Vec<(String, String)>,
677}
678
679#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
681#[repr(u8)]
682pub enum NineKeyShuangpinKind {
683 Migala2 = 0,
685 #[default]
687 Migala3 = 1,
688 Gushi2Low = 2,
690 Custom = 3,
692}
693
694impl NineKeyShuangpinKind {
695 pub const BUILTIN: [Self; 3] = [Self::Migala2, Self::Migala3, Self::Gushi2Low];
697
698 pub fn from_u8(value: u8) -> Option<Self> {
699 Some(match value {
700 0 => Self::Migala2,
701 1 => Self::Migala3,
702 2 => Self::Gushi2Low,
703 3 => Self::Custom,
704 _ => return None,
705 })
706 }
707
708 pub fn from_name(name: &str) -> Option<Self> {
710 Some(match name {
711 "migala2" => Self::Migala2,
712 "migala3" => Self::Migala3,
713 "gushi2_low" => Self::Gushi2Low,
714 "custom" => Self::Custom,
715 _ => return None,
716 })
717 }
718
719 pub fn name(self) -> &'static str {
720 match self {
721 Self::Migala2 => "migala2",
722 Self::Migala3 => "migala3",
723 Self::Gushi2Low => "gushi2_low",
724 Self::Custom => "custom",
725 }
726 }
727}
728
729#[derive(Debug, Clone, PartialEq, Eq, Default)]
731pub struct NineKeyShuangpinTable {
732 pub initials: Vec<(String, String)>,
733 pub finals: Vec<(String, String)>,
734 pub zero_initials: Vec<(String, String)>,
735}
736
737#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
739#[repr(u8)]
740pub enum WubiProfileKind {
741 #[default]
742 Wubi86 = 0,
743 Wubi98 = 1,
744}
745
746impl WubiProfileKind {
747 pub fn from_u8(value: u8) -> Option<Self> {
748 Some(match value {
749 0 => Self::Wubi86,
750 1 => Self::Wubi98,
751 _ => return None,
752 })
753 }
754
755 pub fn from_name(name: &str) -> Option<Self> {
756 Some(match name {
757 "wubi86" => Self::Wubi86,
758 "wubi98" => Self::Wubi98,
759 _ => return None,
760 })
761 }
762
763 pub fn name(self) -> &'static str {
764 match self {
765 Self::Wubi86 => "wubi86",
766 Self::Wubi98 => "wubi98",
767 }
768 }
769
770 pub fn table(self) -> &'static str {
772 self.name()
773 }
774
775 pub fn dictionary_kind(self) -> PersonalDictionaryKind {
777 match self {
778 Self::Wubi86 => PersonalDictionaryKind::Wubi,
779 Self::Wubi98 => PersonalDictionaryKind::Wubi98,
780 }
781 }
782}
783
784pub use crate::shared_string::SharedString;
785
786#[derive(Debug, Clone, PartialEq, Default)]
788pub struct WordItem {
789 pub pinyin: SharedString,
791 pub canonical_pinyin: SharedString,
793 pub word: SharedString,
794 pub weight: i64,
796 pub source: CandidateSource,
797 pub scheme: SchemeType,
799 pub fixed_position: i32,
801 pub fuzzy: bool,
802 pub corrected_from: SharedString,
804 pub sentence_association: bool,
806 pub sentence_words: Vec<String>,
808 pub touch_supported: bool,
810 pub confident_correction: bool,
812}
813
814impl WordItem {
815 #[doc(hidden)]
817 pub fn static_evidence(&self) -> Option<crate::ordering::StaticEvidence> {
818 if !self.sentence_association
819 || !matches!(
820 self.source,
821 CandidateSource::Generated
822 | CandidateSource::NeuralKeyboard
823 | CandidateSource::Collocation
824 )
825 || self.canonical_pinyin.is_empty()
826 || self.word.is_empty()
827 {
828 return None;
829 }
830 let syllables = self.canonical_pinyin.split('\'').count();
831 let bonus =
832 (crate::lattice::decode::PHRASE_LENGTH_BONUS * 1000.0 * syllables as f64) as i64;
833 Some(crate::ordering::StaticEvidence {
834 log_prob_milli_nats: self.weight.saturating_sub(bonus),
835 characters: self.word.chars().count(),
836 })
837 }
838
839 pub fn new(
840 pinyin: impl Into<String>,
841 word: impl Into<String>,
842 weight: i64,
843 source: CandidateSource,
844 canonical_pinyin: impl Into<String>,
845 ) -> Self {
846 Self {
847 pinyin: SharedString::from(Into::<String>::into(pinyin)),
848 canonical_pinyin: SharedString::from(Into::<String>::into(canonical_pinyin)),
849 word: SharedString::from(Into::<String>::into(word)),
850 weight,
851 source,
852 ..Self::default()
853 }
854 }
855}
856
857pub mod fuzzy_rule {
859 pub const Z_ZH: u32 = 1 << 0;
860 pub const C_CH: u32 = 1 << 1;
861 pub const S_SH: u32 = 1 << 2;
862 pub const N_L: u32 = 1 << 3;
863 pub const F_H: u32 = 1 << 4;
864 pub const R_L: u32 = 1 << 5;
865 pub const AN_ANG: u32 = 1 << 6;
866 pub const EN_ENG: u32 = 1 << 7;
867 pub const IN_ING: u32 = 1 << 8;
868 pub const IAN_IANG: u32 = 1 << 9;
869 pub const UAN_UANG: u32 = 1 << 10;
870 pub const ALL: u32 = 0x7ff;
872}
873
874#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
875pub struct FuzzyPinyinOptions {
876 pub rules: u32,
877}
878
879impl FuzzyPinyinOptions {
880 pub fn enabled(self, rule: u32) -> bool {
881 self.rules & rule != 0
882 }
883}
884
885pub mod autocorrect_type {
887 pub const TRANSPOSITION: u32 = 1 << 0;
888 pub const NEIGHBOR: u32 = 1 << 1;
889 pub const DELETION: u32 = 1 << 2;
890 pub const INSERTION: u32 = 1 << 3;
891 pub const MISSING_OR_EXTRA: u32 = DELETION | INSERTION;
892}
893
894pub fn request_autocorrect_mask(transposition: bool, neighbor: bool) -> u32 {
896 let legacy = if transposition {
897 autocorrect_type::TRANSPOSITION
898 } else {
899 0
900 } | if neighbor {
901 autocorrect_type::NEIGHBOR
902 } else {
903 0
904 };
905 if legacy == 0 {
906 0
907 } else {
908 legacy | autocorrect_type::MISSING_OR_EXTRA
909 }
910}
911
912#[derive(Debug, Clone, PartialEq)]
914pub struct SentenceAssociationOptions {
915 pub word_lattice: bool,
916 pub neural_keyboard: bool,
918 pub show_next_on_duplicate: bool,
919 pub collocation_model: std::path::PathBuf,
922 pub collocation_weight: f64,
924 pub collocation_rerank: bool,
926 pub collocation_grammar: crate::lattice::octagram::GramConfig,
928}
929
930impl Default for SentenceAssociationOptions {
931 fn default() -> Self {
932 Self {
933 word_lattice: true,
934 neural_keyboard: false,
935 show_next_on_duplicate: false,
936 collocation_model: std::path::PathBuf::new(),
937 collocation_weight: 1.0,
938 collocation_rerank: false,
939 collocation_grammar: crate::lattice::octagram::GramConfig::default(),
940 }
941 }
942}
943
944pub const DEFAULT_FIRST_PAGE_ROWS: usize = 6;
946
947#[derive(Debug, Clone, Copy, PartialEq, Eq)]
949pub struct FirstPageRows(usize);
950
951impl FirstPageRows {
952 pub const fn new(rows: usize) -> Self {
953 Self(if rows == 0 { 1 } else { rows })
954 }
955
956 pub const fn get(self) -> usize {
957 self.0
958 }
959}
960
961impl Default for FirstPageRows {
962 fn default() -> Self {
963 Self(DEFAULT_FIRST_PAGE_ROWS)
964 }
965}
966
967#[derive(Debug, Clone, Copy, PartialEq, Eq)]
969pub struct ShuangpinHelpcodeModes {
970 pub manual_tab: bool,
972 pub direct: bool,
974 pub trigger_backtick: bool,
976 pub trigger_semicolon: bool,
978 pub trigger_uppercase: bool,
980 pub mid_sentence: bool,
982 pub mid_sentence_backtick: bool,
983 pub mid_sentence_semicolon: bool,
984 pub mid_sentence_uppercase: bool,
985 pub direct_helpcode: bool,
987 pub direct_helpcode_slash: bool,
988 pub direct_helpcode_uppercase: bool,
989}
990
991impl Default for ShuangpinHelpcodeModes {
992 fn default() -> Self {
994 Self {
995 manual_tab: false,
996 direct: true,
997 trigger_backtick: false,
998 trigger_semicolon: true,
999 trigger_uppercase: false,
1000 mid_sentence: false,
1001 mid_sentence_backtick: true,
1002 mid_sentence_semicolon: false,
1003 mid_sentence_uppercase: false,
1004 direct_helpcode: false,
1005 direct_helpcode_slash: true,
1006 direct_helpcode_uppercase: false,
1007 }
1008 }
1009}
1010
1011#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1013pub struct SyllableHelpcode {
1014 pub syllable: usize,
1016 pub first: u8,
1018 pub second: u8,
1020}
1021
1022#[derive(Debug, Clone, PartialEq, Default)]
1024pub struct QueryRequest {
1025 pub scheme: SchemeType,
1026 pub raw_input: String,
1028 pub raw_input_with_cases: String,
1029 pub raw_input_with_helpcode_marks: String,
1032 pub syllable_helpcodes: Vec<SyllableHelpcode>,
1034 pub end_helpcodes: String,
1036 pub helpcode_source_index: Vec<usize>,
1039 pub helpcode_decorations: Vec<(usize, String)>,
1041 pub normalized_input: String,
1043 pub raw_segmentation: String,
1044 pub normalized_segmentation: String,
1045 pub segmentation: String,
1046 pub enable_shuangpin_helpcode: bool,
1047 pub shuangpin_helpcode_modes: ShuangpinHelpcodeModes,
1049 pub enable_quanpin_helpcode: bool,
1050 pub strict_quanpin_helpcode: bool,
1052 pub enable_quanpin_autocorrect_transposition: bool,
1054 pub enable_quanpin_autocorrect_neighbor: bool,
1055 pub fuzzy_pinyin: FuzzyPinyinOptions,
1056 pub sentence_alternatives: bool,
1058 pub sentence_association: SentenceAssociationOptions,
1059 pub first_page_rows: FirstPageRows,
1061 pub rescoring_context: String,
1063 pub korean_hanja: bool,
1065 pub wubi_z_wildcard: bool,
1067 pub wubi_grid: bool,
1069 pub key_touches: Vec<Option<crate::touch::KeyTouch>>,
1071 pub key_geometry: Option<std::sync::Arc<crate::touch::KeyGeometry>>,
1073 pub touch_tuning: crate::touch::TouchTuning,
1075 pub valid: bool,
1077}
1078
1079#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1081pub enum SchemeKey {
1082 Letter(u8),
1084 Apostrophe,
1085 Semicolon,
1087 Minus,
1089 Symbol(u8),
1091 Backspace,
1092 Requery,
1094}
1095
1096#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1098#[repr(u8)]
1099pub enum Command {
1100 Backspace = 0,
1101 CommitCandidate = 1,
1102 CommitRaw = 2,
1103 Cancel = 3,
1105 MoveLeft = 4,
1106 MoveRight = 5,
1107 MoveHome = 6,
1108 MoveEnd = 7,
1109 DeleteForward = 8,
1110 CycleKanaVariant = 9,
1112 CommitReading = 10,
1114 ConvertHanja = 11,
1116 ConversionLeft = 12,
1118 ConversionRight = 13,
1120 CommitRawWithoutLearning = 14,
1122}
1123
1124impl Command {
1125 pub fn from_u8(value: u8) -> Option<Self> {
1126 Some(match value {
1127 0 => Self::Backspace,
1128 1 => Self::CommitCandidate,
1129 2 => Self::CommitRaw,
1130 3 => Self::Cancel,
1131 4 => Self::MoveLeft,
1132 5 => Self::MoveRight,
1133 6 => Self::MoveHome,
1134 7 => Self::MoveEnd,
1135 8 => Self::DeleteForward,
1136 9 => Self::CycleKanaVariant,
1137 10 => Self::CommitReading,
1138 11 => Self::ConvertHanja,
1139 12 => Self::ConversionLeft,
1140 13 => Self::ConversionRight,
1141 14 => Self::CommitRawWithoutLearning,
1142 _ => return None,
1143 })
1144 }
1145
1146 pub fn name(self) -> &'static str {
1147 match self {
1148 Self::Backspace => "Backspace",
1149 Self::CommitCandidate => "CommitCandidate",
1150 Self::CommitRaw => "CommitRaw",
1151 Self::Cancel => "Cancel",
1152 Self::MoveLeft => "MoveLeft",
1153 Self::MoveRight => "MoveRight",
1154 Self::MoveHome => "MoveHome",
1155 Self::MoveEnd => "MoveEnd",
1156 Self::DeleteForward => "DeleteForward",
1157 Self::CycleKanaVariant => "CycleKanaVariant",
1158 Self::CommitReading => "CommitReading",
1159 Self::ConvertHanja => "ConvertHanja",
1160 Self::ConversionLeft => "ConversionLeft",
1161 Self::ConversionRight => "ConversionRight",
1162 Self::CommitRawWithoutLearning => "CommitRawWithoutLearning",
1163 }
1164 }
1165
1166 pub fn from_name(name: &str) -> Option<Self> {
1167 (0..=14)
1168 .filter_map(Self::from_u8)
1169 .find(|command| command.name() == name)
1170 }
1171}
1172
1173#[derive(Debug, Clone, PartialEq, Eq, Default)]
1175pub struct KeyResult {
1176 pub handled: bool,
1177 pub commit: Option<String>,
1178 pub diagnostic: Option<String>,
1179}
1180
1181impl KeyResult {
1182 pub fn unhandled() -> Self {
1183 Self::default()
1184 }
1185
1186 pub fn handled() -> Self {
1187 Self {
1188 handled: true,
1189 ..Self::default()
1190 }
1191 }
1192
1193 pub fn committed(text: impl Into<String>) -> Self {
1194 Self {
1195 handled: true,
1196 commit: Some(text.into()),
1197 diagnostic: None,
1198 }
1199 }
1200
1201 pub fn with_diagnostic(mut self, diagnostic: Option<String>) -> Self {
1202 self.diagnostic = diagnostic;
1203 self
1204 }
1205
1206 pub fn diagnostic_code(&self) -> Option<crate::diagnostics::DiagnosticCode> {
1208 self.diagnostic
1209 .as_deref()
1210 .map(crate::diagnostics::DiagnosticCode::from_message)
1211 }
1212}
1213
1214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1215#[repr(u8)]
1216pub enum CandidateEdge {
1217 FirstHan = 0,
1218 LastHan = 1,
1219}
1220
1221impl CandidateEdge {
1222 pub fn character(self, text: &str) -> &str {
1224 match self {
1225 Self::FirstHan => crate::text::first_han_char(text),
1226 Self::LastHan => crate::text::last_han_char(text),
1227 }
1228 }
1229}
1230
1231#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1232pub enum LocalInputMode {
1233 #[default]
1234 None,
1235 Unicode,
1236 DateTime,
1237 QuickPhrase,
1238 Emoji,
1239 Kaomoji,
1240 SuperJianpin,
1241 TemporaryEnglish,
1242 TemporaryJapanese,
1243 Expression,
1245 Command,
1247 Mention,
1249 Url,
1251 Chaizi,
1253 Symbol,
1255}
1256
1257impl LocalInputMode {
1258 pub fn name(self) -> &'static str {
1260 match self {
1261 Self::None => "none",
1262 Self::Unicode => "unicode",
1263 Self::DateTime => "date_time",
1264 Self::QuickPhrase => "quick_phrase",
1265 Self::Emoji => "emoji",
1266 Self::Kaomoji => "kaomoji",
1267 Self::SuperJianpin => "super_jianpin",
1268 Self::TemporaryEnglish => "temporary_english",
1269 Self::TemporaryJapanese => "temporary_japanese",
1270 Self::Expression => "expression",
1271 Self::Command => "command",
1272 Self::Mention => "mention",
1273 Self::Url => "url",
1274 Self::Chaizi => "chaizi",
1275 Self::Symbol => "symbol",
1276 }
1277 }
1278
1279 pub fn from_name(name: &str) -> Option<Self> {
1281 [
1282 Self::None,
1283 Self::Unicode,
1284 Self::DateTime,
1285 Self::QuickPhrase,
1286 Self::Emoji,
1287 Self::Kaomoji,
1288 Self::SuperJianpin,
1289 Self::TemporaryEnglish,
1290 Self::TemporaryJapanese,
1291 Self::Expression,
1292 Self::Command,
1293 Self::Mention,
1294 Self::Url,
1295 Self::Chaizi,
1296 Self::Symbol,
1297 ]
1298 .into_iter()
1299 .find(|mode| mode.name() == name)
1300 }
1301
1302 pub fn spelling_symbols(self) -> &'static str {
1304 match self {
1305 Self::Unicode => "0123456789",
1306 Self::Expression => crate::local::expression::SPELLING_SYMBOLS,
1307 Self::Url => crate::local::url::SPELLING_SYMBOLS,
1308 Self::TemporaryJapanese => "-",
1309 _ => "",
1310 }
1311 }
1312
1313 pub fn generates_text(self) -> bool {
1315 matches!(self, Self::Expression | Self::Command | Self::Mention)
1316 }
1317
1318 pub fn without_entry_marker(self, text: &str) -> &str {
1320 match self {
1321 Self::TemporaryEnglish | Self::TemporaryJapanese => text.get(1..).unwrap_or_default(),
1322 _ => text,
1323 }
1324 }
1325
1326 pub fn annotates_rows(self) -> bool {
1328 matches!(
1329 self,
1330 Self::Expression | Self::Command | Self::Mention | Self::Chaizi
1331 )
1332 }
1333}
1334
1335#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1336pub enum FrequencyAdjustmentMode {
1337 #[default]
1338 Disabled,
1339 Pin,
1340 Halve,
1341 Linear,
1342 Promote,
1343}
1344
1345impl FrequencyAdjustmentMode {
1346 pub fn name(self) -> &'static str {
1348 match self {
1349 Self::Disabled => "disabled",
1350 Self::Pin => "pin",
1351 Self::Halve => "halve",
1352 Self::Linear => "linear",
1353 Self::Promote => "promote",
1354 }
1355 }
1356
1357 pub fn from_name(name: &str) -> Option<Self> {
1358 Some(match name {
1359 "disabled" => Self::Disabled,
1360 "pin" => Self::Pin,
1361 "halve" => Self::Halve,
1362 "linear" => Self::Linear,
1363 "promote" => Self::Promote,
1364 _ => return None,
1365 })
1366 }
1367}
1368
1369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1370pub struct FrequencyAdjustmentOptions {
1371 pub mode: FrequencyAdjustmentMode,
1372 pub trigger_count: i32,
1374 pub linear_step: i32,
1376}
1377
1378impl Default for FrequencyAdjustmentOptions {
1379 fn default() -> Self {
1380 Self {
1381 mode: FrequencyAdjustmentMode::Disabled,
1382 trigger_count: 1,
1383 linear_step: 1,
1384 }
1385 }
1386}
1387
1388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1390pub struct LocalModeOptions {
1391 pub unicode: bool,
1392 pub date_time: bool,
1394 pub date_time_inline: bool,
1396 pub date_time_menu: bool,
1398 pub quick_phrase: bool,
1399 pub emoji: bool,
1400 pub kaomoji: bool,
1401 pub super_jianpin: bool,
1402 pub temporary_english: bool,
1403 pub temporary_japanese: bool,
1404 pub expression: bool,
1405 pub command: bool,
1406 pub mention: bool,
1407 pub chaizi: bool,
1409 pub symbol: bool,
1411 pub shift_letter_entry: bool,
1413}
1414
1415impl Default for LocalModeOptions {
1416 fn default() -> Self {
1417 Self {
1418 unicode: true,
1419 date_time: true,
1420 date_time_inline: true,
1421 date_time_menu: true,
1422 quick_phrase: true,
1423 emoji: true,
1424 kaomoji: true,
1425 super_jianpin: true,
1426 temporary_english: true,
1427 temporary_japanese: true,
1428 expression: false,
1429 command: false,
1430 mention: false,
1431 chaizi: false,
1432 symbol: true,
1433 shift_letter_entry: true,
1434 }
1435 }
1436}
1437
1438#[derive(Debug, Clone, PartialEq, Eq)]
1442pub struct CommandTableEntry {
1443 pub trigger: String,
1445 pub title: String,
1447 pub template: String,
1448}
1449
1450#[derive(Debug, Clone, PartialEq, Eq)]
1452pub struct PluginCommand {
1453 pub trigger: String,
1455 pub title: String,
1457 pub extended: bool,
1459}
1460
1461#[derive(Debug, Clone, PartialEq, Eq)]
1463pub struct QuickPhraseEntry {
1464 pub key: String,
1466 pub text: String,
1468}
1469
1470#[derive(Debug, Clone, PartialEq, Eq)]
1472pub struct MentionEntry {
1473 pub text: String,
1475 pub key: String,
1477 pub phone: String,
1479}
1480
1481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1482pub struct EnglishInputOptions {
1483 pub mixed_candidates: bool,
1484 pub minimum_prefix: usize,
1486 pub first_page_count: usize,
1488}
1489
1490impl Default for EnglishInputOptions {
1491 fn default() -> Self {
1492 Self {
1493 mixed_candidates: false,
1494 minimum_prefix: 2,
1495 first_page_count: 1,
1496 }
1497 }
1498}
1499
1500#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1501pub struct MixedExpressiveOptions {
1502 pub emoji_candidates: bool,
1503 pub kaomoji_candidates: bool,
1504 pub symbol_candidates: bool,
1506}
1507
1508#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1509pub struct WubiInputOptions {
1510 pub mixed_pinyin: bool,
1512 pub profile: WubiProfileKind,
1514 pub z_wildcard: bool,
1518}
1519
1520#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
1522#[serde(rename_all = "snake_case")]
1523pub enum OnlineInputMode {
1524 #[default]
1525 Pinyin,
1526 English,
1527}
1528
1529impl OnlineInputMode {
1530 pub fn is_pinyin(&self) -> bool {
1531 *self == Self::Pinyin
1532 }
1533}
1534
1535#[derive(Debug, Clone, PartialEq, Eq, Default)]
1537pub struct OnlineQuery {
1538 pub mode: OnlineInputMode,
1539 pub scheme: SchemeType,
1540 pub generation: u64,
1541 pub identity: String,
1542 pub query_text: String,
1543 pub cache_key: String,
1544 pub pinyin_segments: Vec<String>,
1545 pub cloud_eligible: bool,
1546 pub ai_eligible: bool,
1547 pub session_id: u64,
1548}
1549
1550#[derive(Debug, Clone, PartialEq, Eq, Default)]
1552pub struct CommandTranslationQuery {
1553 pub session_id: u64,
1554 pub text: String,
1555}
1556
1557#[derive(Debug, Clone, PartialEq, Eq, Default)]
1559pub struct PluginCommandQuery {
1560 pub session_id: u64,
1561 pub preedit: String,
1563 pub trigger: String,
1564 pub argument: String,
1566}
1567
1568#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
1572#[repr(u8)]
1573#[non_exhaustive]
1574pub enum PersonalDictionaryKind {
1575 #[default]
1576 Pinyin = 0,
1577 Wubi = 1,
1578 QuickPhrase = 2,
1579 English = 3,
1580 Wubi98 = 4,
1582}
1583
1584impl PersonalDictionaryKind {
1585 pub fn from_u8(value: u8) -> Option<Self> {
1586 Some(match value {
1587 0 => Self::Pinyin,
1588 1 => Self::Wubi,
1589 2 => Self::QuickPhrase,
1590 3 => Self::English,
1591 4 => Self::Wubi98,
1592 _ => return None,
1593 })
1594 }
1595
1596 pub fn is_wubi(self) -> bool {
1598 matches!(self, Self::Wubi | Self::Wubi98)
1599 }
1600
1601 pub fn wubi_table(self) -> Option<&'static str> {
1603 match self {
1604 Self::Wubi => Some(WubiProfileKind::Wubi86.table()),
1605 Self::Wubi98 => Some(WubiProfileKind::Wubi98.table()),
1606 Self::Pinyin | Self::QuickPhrase | Self::English => None,
1607 }
1608 }
1609
1610 pub fn journal_name(self) -> &'static str {
1612 match self {
1613 Self::Pinyin => "pinyin",
1614 Self::Wubi => "wubi",
1615 Self::QuickPhrase => "quick",
1616 Self::English => "english",
1617 Self::Wubi98 => "wubi98",
1618 }
1619 }
1620
1621 pub fn from_journal_name(name: &str) -> Option<Self> {
1622 Some(match name {
1623 "pinyin" => Self::Pinyin,
1624 "wubi" => Self::Wubi,
1625 "quick" => Self::QuickPhrase,
1626 "english" => Self::English,
1627 "wubi98" => Self::Wubi98,
1628 _ => return None,
1629 })
1630 }
1631}
1632
1633#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1635pub struct PersonalDictionaryEntry {
1636 pub kind: PersonalDictionaryKind,
1637 pub key: String,
1638 pub value: String,
1639 pub weight: i64,
1640}
1641
1642impl PersonalDictionaryEntry {
1643 pub const DEFAULT_WEIGHT: i64 = 100_000;
1644}
1645
1646impl Default for PersonalDictionaryEntry {
1647 fn default() -> Self {
1648 Self {
1649 kind: PersonalDictionaryKind::Pinyin,
1650 key: String::new(),
1651 value: String::new(),
1652 weight: Self::DEFAULT_WEIGHT,
1653 }
1654 }
1655}
1656
1657#[cfg(test)]
1658mod tests {
1659 use super::{CandidateSource, SchemeSet, SchemeType, WordItem};
1660
1661 #[test]
1663 fn candidate_sources_round_trip_and_mixed_japanese_matches_no_predicate() {
1664 for value in 0..=u8::MAX {
1665 match CandidateSource::from_u8(value) {
1666 Some(source) => assert_eq!(source as u8, value),
1667 None => assert!(value == 10 || value > 14, "{value}"),
1668 }
1669 }
1670 let japanese = CandidateSource::MixedJapanese;
1671 assert_eq!(CandidateSource::from_u8(13), Some(japanese));
1672 assert!(!japanese.is_dictionary());
1673 assert!(!japanese.is_generated_or_fallback());
1674 assert!(!japanese.is_sentence_learning());
1675 assert!(!japanese.is_online());
1676 assert!(!japanese.takes_online_seat());
1677 assert!(!japanese.holds_ai_seat());
1678 }
1679
1680 #[test]
1682 fn plugin_rows_match_no_predicate() {
1683 let plugin = CandidateSource::Plugin;
1684 assert_eq!(CandidateSource::from_u8(14), Some(plugin));
1685 assert_eq!(plugin as u8, 14);
1686 assert!(!plugin.is_dictionary());
1687 assert!(!plugin.is_generated_or_fallback());
1688 assert!(!plugin.is_sentence_learning());
1689 assert!(!plugin.is_online());
1690 assert!(!plugin.takes_online_seat());
1691 assert!(!plugin.holds_ai_seat());
1692 }
1693
1694 type Row = (&'static str, fn(SchemeType) -> bool, [bool; 5]);
1695 type Named = (&'static str, fn(SchemeType) -> bool);
1696
1697 const SCHEMES: [SchemeType; 5] = [
1698 SchemeType::Quanpin,
1699 SchemeType::Shuangpin,
1700 SchemeType::Wubi,
1701 SchemeType::JapaneseRomaji,
1702 SchemeType::Korean,
1703 ];
1704
1705 #[test]
1707 fn predicates_keep_the_behaviour_of_the_existing_schemes() {
1708 let table: [Row; 26] = [
1709 (
1710 "is_chinese",
1711 SchemeType::is_chinese,
1712 [true, true, true, false, false],
1713 ),
1714 (
1715 "outputs_traditional_natively",
1716 SchemeType::outputs_traditional_natively,
1717 [false; 5],
1718 ),
1719 (
1720 "script_conversion_applies",
1721 SchemeType::script_conversion_applies,
1722 [true, true, true, false, false],
1723 ),
1724 (
1725 "uses_chinese_punctuation",
1726 SchemeType::uses_chinese_punctuation,
1727 [true, true, true, true, false],
1728 ),
1729 (
1730 "host_smart_punctuation",
1731 SchemeType::host_smart_punctuation,
1732 [true, true, true, false, false],
1733 ),
1734 (
1735 "widens_full_width",
1736 SchemeType::widens_full_width,
1737 [true, true, true, true, false],
1738 ),
1739 (
1740 "allows_english_emoji_mixing",
1741 SchemeType::allows_english_emoji_mixing,
1742 [true, true, false, false, false],
1743 ),
1744 (
1745 "opens_local_modes",
1746 SchemeType::opens_local_modes,
1747 [true, true, false, false, false],
1748 ),
1749 (
1750 "learns_into_main_dictionary",
1751 SchemeType::learns_into_main_dictionary,
1752 [true, true, true, false, false],
1753 ),
1754 (
1755 "learns_english_words",
1756 SchemeType::learns_english_words,
1757 [true, true, false, false, false],
1758 ),
1759 (
1760 "cloud_eligible",
1761 SchemeType::cloud_eligible,
1762 [true, true, false, true, false],
1763 ),
1764 (
1765 "shows_glosses",
1766 SchemeType::shows_glosses,
1767 [true, true, true, false, true],
1768 ),
1769 (
1770 "commits_on_blur",
1771 SchemeType::commits_on_blur,
1772 [false, false, false, false, true],
1773 ),
1774 (
1775 "holds_phrase_progress",
1776 SchemeType::holds_phrase_progress,
1777 [true, true, true, true, false],
1778 ),
1779 (
1780 "reranks_with_sentence_model",
1781 SchemeType::reranks_with_sentence_model,
1782 [true, true, false, false, false],
1783 ),
1784 (
1785 "draws_reading",
1786 SchemeType::draws_reading,
1787 [false, false, false, true, true],
1788 ),
1789 (
1790 "has_openable_candidate_list",
1791 SchemeType::has_openable_candidate_list,
1792 [false, false, false, false, true],
1793 ),
1794 (
1795 "cancel_keeps_composition",
1796 SchemeType::cancel_keeps_composition,
1797 [false, false, false, false, true],
1798 ),
1799 (
1800 "selection_completes",
1801 SchemeType::selection_completes,
1802 [false, false, false, true, true],
1803 ),
1804 (
1805 "follows_pinyin_candidates",
1806 SchemeType::follows_pinyin_candidates,
1807 [true, true, false, false, false],
1808 ),
1809 (
1810 "accepts_apostrophe",
1811 SchemeType::accepts_apostrophe,
1812 [true, true, false, true, false],
1813 ),
1814 (
1815 "locks_caret",
1816 SchemeType::locks_caret,
1817 [false, false, false, false, true],
1818 ),
1819 (
1820 "supports_fuzzy",
1821 SchemeType::supports_fuzzy,
1822 [true, true, false, false, false],
1823 ),
1824 (
1825 "supports_autocorrect",
1826 SchemeType::supports_autocorrect,
1827 [true, false, false, false, false],
1828 ),
1829 (
1830 "nine_key",
1831 SchemeType::nine_key,
1832 [true, false, false, false, false],
1833 ),
1834 (
1835 "helpcode",
1836 SchemeType::helpcode,
1837 [true, true, false, false, false],
1838 ),
1839 ];
1840 for (name, predicate, expected) in table {
1841 for (scheme, want) in SCHEMES.into_iter().zip(expected) {
1842 assert_eq!(predicate(scheme), want, "{name} for {scheme:?}");
1843 }
1844 }
1845 }
1846
1847 #[test]
1848 fn only_the_chinese_typing_schemes_detect_urls() {
1849 for code in 0..=9 {
1850 let scheme = SchemeType::from_u8(code).expect("scheme code");
1851 let expected = matches!(
1852 scheme,
1853 SchemeType::Quanpin | SchemeType::Shuangpin | SchemeType::Wubi
1854 );
1855 assert_eq!(scheme.detects_urls(), expected, "{scheme:?}");
1856 }
1857 }
1858
1859 #[test]
1860 fn local_input_mode_names_round_trip() {
1861 use super::LocalInputMode;
1862 fn next(mode: LocalInputMode) -> Option<LocalInputMode> {
1864 Some(match mode {
1865 LocalInputMode::None => LocalInputMode::Unicode,
1866 LocalInputMode::Unicode => LocalInputMode::DateTime,
1867 LocalInputMode::DateTime => LocalInputMode::QuickPhrase,
1868 LocalInputMode::QuickPhrase => LocalInputMode::Emoji,
1869 LocalInputMode::Emoji => LocalInputMode::Kaomoji,
1870 LocalInputMode::Kaomoji => LocalInputMode::SuperJianpin,
1871 LocalInputMode::SuperJianpin => LocalInputMode::TemporaryEnglish,
1872 LocalInputMode::TemporaryEnglish => LocalInputMode::TemporaryJapanese,
1873 LocalInputMode::TemporaryJapanese => LocalInputMode::Expression,
1874 LocalInputMode::Expression => LocalInputMode::Command,
1875 LocalInputMode::Command => LocalInputMode::Mention,
1876 LocalInputMode::Mention => LocalInputMode::Url,
1877 LocalInputMode::Url => LocalInputMode::Chaizi,
1878 LocalInputMode::Chaizi => LocalInputMode::Symbol,
1879 LocalInputMode::Symbol => return None,
1880 })
1881 }
1882 let mut names = Vec::new();
1883 let mut mode = Some(LocalInputMode::None);
1884 while let Some(current) = mode {
1885 assert_eq!(
1886 LocalInputMode::from_name(current.name()),
1887 Some(current),
1888 "{current:?}"
1889 );
1890 names.push(current.name());
1891 mode = next(current);
1892 }
1893 assert_eq!(names.len(), 15);
1894 assert!(names.contains(&"url"));
1895 assert!(names.contains(&"chaizi"));
1896 assert!(names.contains(&"symbol"));
1897 assert_eq!(LocalInputMode::from_name("unknown"), None);
1898 }
1899
1900 #[test]
1901 fn existing_scheme_codes_are_unchanged() {
1902 for (code, scheme) in SCHEMES.into_iter().enumerate() {
1903 assert_eq!(scheme as u8, code as u8);
1904 assert_eq!(SchemeType::from_u8(code as u8), Some(scheme));
1905 }
1906 }
1907
1908 #[test]
1909 fn vietnamese_round_trips_through_code_seven() {
1910 assert_eq!(SchemeType::Vietnamese as u8, 7);
1911 assert_eq!(SchemeType::from_u8(7), Some(SchemeType::Vietnamese));
1912 assert_eq!(SchemeType::Vietnamese.name(), "vietnamese");
1913 assert!(!SchemeType::Vietnamese.is_pinyin());
1914 }
1915
1916 #[test]
1917 fn tibetan_round_trips_through_code_eight() {
1918 assert_eq!(SchemeType::Tibetan as u8, 8);
1919 assert_eq!(SchemeType::from_u8(8), Some(SchemeType::Tibetan));
1920 assert_eq!(SchemeType::Tibetan.name(), "tibetan");
1921 assert!(!SchemeType::Tibetan.is_pinyin());
1922 }
1923
1924 #[test]
1925 fn cantonese_round_trips_through_code_five() {
1926 assert_eq!(SchemeType::Cantonese as u8, 5);
1927 assert_eq!(SchemeType::from_u8(5), Some(SchemeType::Cantonese));
1928 assert_eq!(SchemeType::Cantonese.name(), "cantonese");
1929 assert!(!SchemeType::Cantonese.is_pinyin());
1930 }
1931
1932 #[test]
1933 fn zhuyin_round_trips_through_code_six() {
1934 assert_eq!(SchemeType::Zhuyin as u8, 6);
1935 assert_eq!(SchemeType::from_u8(6), Some(SchemeType::Zhuyin));
1936 assert_eq!(SchemeType::Zhuyin.name(), "zhuyin");
1937 assert!(!SchemeType::Zhuyin.is_pinyin());
1938 }
1939
1940 #[test]
1941 fn stroke_round_trips_through_code_nine() {
1942 assert_eq!(SchemeType::Stroke as u8, 9);
1943 assert_eq!(SchemeType::from_u8(9), Some(SchemeType::Stroke));
1944 assert_eq!(SchemeType::Stroke.name(), "stroke");
1945 assert!(!SchemeType::Stroke.is_pinyin());
1946 assert_eq!(SchemeType::from_u8(10), None);
1947 }
1948
1949 #[test]
1951 fn stroke_predicates() {
1952 let scheme = SchemeType::Stroke;
1953 let on: [Named; 6] = [
1954 ("is_chinese", SchemeType::is_chinese),
1955 (
1956 "outputs_traditional_natively",
1957 SchemeType::outputs_traditional_natively,
1958 ),
1959 (
1960 "uses_chinese_punctuation",
1961 SchemeType::uses_chinese_punctuation,
1962 ),
1963 ("host_smart_punctuation", SchemeType::host_smart_punctuation),
1964 ("widens_full_width", SchemeType::widens_full_width),
1965 ("draws_reading", SchemeType::draws_reading),
1966 ];
1967 let off: [Named; 20] = [
1968 (
1969 "cancel_keeps_composition",
1970 SchemeType::cancel_keeps_composition,
1971 ),
1972 (
1973 "script_conversion_applies",
1974 SchemeType::script_conversion_applies,
1975 ),
1976 (
1977 "allows_english_emoji_mixing",
1978 SchemeType::allows_english_emoji_mixing,
1979 ),
1980 ("opens_local_modes", SchemeType::opens_local_modes),
1981 (
1982 "learns_into_main_dictionary",
1983 SchemeType::learns_into_main_dictionary,
1984 ),
1985 ("learns_english_words", SchemeType::learns_english_words),
1986 ("cloud_eligible", SchemeType::cloud_eligible),
1987 ("shows_glosses", SchemeType::shows_glosses),
1988 ("commits_on_blur", SchemeType::commits_on_blur),
1989 ("holds_phrase_progress", SchemeType::holds_phrase_progress),
1990 (
1991 "reranks_with_sentence_model",
1992 SchemeType::reranks_with_sentence_model,
1993 ),
1994 (
1995 "has_openable_candidate_list",
1996 SchemeType::has_openable_candidate_list,
1997 ),
1998 ("selection_completes", SchemeType::selection_completes),
1999 (
2000 "follows_pinyin_candidates",
2001 SchemeType::follows_pinyin_candidates,
2002 ),
2003 ("accepts_apostrophe", SchemeType::accepts_apostrophe),
2004 ("locks_caret", SchemeType::locks_caret),
2005 ("supports_fuzzy", SchemeType::supports_fuzzy),
2006 ("supports_autocorrect", SchemeType::supports_autocorrect),
2007 ("nine_key", SchemeType::nine_key),
2008 ("helpcode", SchemeType::helpcode),
2009 ];
2010 for (name, predicate) in on {
2011 assert!(predicate(scheme), "{name}");
2012 }
2013 for (name, predicate) in off {
2014 assert!(!predicate(scheme), "{name}");
2015 }
2016 for (name, predicate) in on.into_iter().chain(off) {
2018 if name == "accepts_apostrophe" || name == "draws_reading" {
2019 assert_ne!(
2020 predicate(scheme),
2021 predicate(SchemeType::Cantonese),
2022 "{name}"
2023 );
2024 } else {
2025 assert_eq!(
2026 predicate(scheme),
2027 predicate(SchemeType::Cantonese),
2028 "{name}"
2029 );
2030 }
2031 }
2032 }
2033
2034 #[test]
2036 fn zhuyin_predicates() {
2037 let scheme = SchemeType::Zhuyin;
2038 let on: [Named; 10] = [
2039 ("is_chinese", SchemeType::is_chinese),
2040 (
2041 "cancel_keeps_composition",
2042 SchemeType::cancel_keeps_composition,
2043 ),
2044 (
2045 "outputs_traditional_natively",
2046 SchemeType::outputs_traditional_natively,
2047 ),
2048 (
2049 "uses_chinese_punctuation",
2050 SchemeType::uses_chinese_punctuation,
2051 ),
2052 ("widens_full_width", SchemeType::widens_full_width),
2053 ("commits_on_blur", SchemeType::commits_on_blur),
2054 ("draws_reading", SchemeType::draws_reading),
2055 (
2056 "has_openable_candidate_list",
2057 SchemeType::has_openable_candidate_list,
2058 ),
2059 ("locks_caret", SchemeType::locks_caret),
2060 ("nine_key", SchemeType::nine_key),
2061 ];
2062 let off: [Named; 16] = [
2063 (
2064 "script_conversion_applies",
2065 SchemeType::script_conversion_applies,
2066 ),
2067 ("host_smart_punctuation", SchemeType::host_smart_punctuation),
2068 (
2069 "allows_english_emoji_mixing",
2070 SchemeType::allows_english_emoji_mixing,
2071 ),
2072 ("opens_local_modes", SchemeType::opens_local_modes),
2073 (
2074 "learns_into_main_dictionary",
2075 SchemeType::learns_into_main_dictionary,
2076 ),
2077 ("learns_english_words", SchemeType::learns_english_words),
2078 ("cloud_eligible", SchemeType::cloud_eligible),
2079 ("shows_glosses", SchemeType::shows_glosses),
2080 ("holds_phrase_progress", SchemeType::holds_phrase_progress),
2081 (
2082 "reranks_with_sentence_model",
2083 SchemeType::reranks_with_sentence_model,
2084 ),
2085 ("selection_completes", SchemeType::selection_completes),
2086 (
2087 "follows_pinyin_candidates",
2088 SchemeType::follows_pinyin_candidates,
2089 ),
2090 ("accepts_apostrophe", SchemeType::accepts_apostrophe),
2091 ("supports_fuzzy", SchemeType::supports_fuzzy),
2092 ("supports_autocorrect", SchemeType::supports_autocorrect),
2093 ("helpcode", SchemeType::helpcode),
2094 ];
2095 for (name, predicate) in on {
2096 assert!(predicate(scheme), "{name}");
2097 }
2098 for (name, predicate) in off {
2099 assert!(!predicate(scheme), "{name}");
2100 }
2101 }
2102
2103 #[test]
2105 fn cantonese_predicates() {
2106 let scheme = SchemeType::Cantonese;
2107 let on: [Named; 6] = [
2108 ("is_chinese", SchemeType::is_chinese),
2109 (
2110 "outputs_traditional_natively",
2111 SchemeType::outputs_traditional_natively,
2112 ),
2113 (
2114 "uses_chinese_punctuation",
2115 SchemeType::uses_chinese_punctuation,
2116 ),
2117 ("host_smart_punctuation", SchemeType::host_smart_punctuation),
2118 ("widens_full_width", SchemeType::widens_full_width),
2119 ("accepts_apostrophe", SchemeType::accepts_apostrophe),
2120 ];
2121 let off: [Named; 20] = [
2122 (
2123 "cancel_keeps_composition",
2124 SchemeType::cancel_keeps_composition,
2125 ),
2126 (
2127 "script_conversion_applies",
2128 SchemeType::script_conversion_applies,
2129 ),
2130 (
2131 "allows_english_emoji_mixing",
2132 SchemeType::allows_english_emoji_mixing,
2133 ),
2134 ("opens_local_modes", SchemeType::opens_local_modes),
2135 (
2136 "learns_into_main_dictionary",
2137 SchemeType::learns_into_main_dictionary,
2138 ),
2139 ("learns_english_words", SchemeType::learns_english_words),
2140 ("cloud_eligible", SchemeType::cloud_eligible),
2141 ("shows_glosses", SchemeType::shows_glosses),
2142 ("commits_on_blur", SchemeType::commits_on_blur),
2143 ("holds_phrase_progress", SchemeType::holds_phrase_progress),
2144 (
2145 "reranks_with_sentence_model",
2146 SchemeType::reranks_with_sentence_model,
2147 ),
2148 ("draws_reading", SchemeType::draws_reading),
2149 (
2150 "has_openable_candidate_list",
2151 SchemeType::has_openable_candidate_list,
2152 ),
2153 ("selection_completes", SchemeType::selection_completes),
2154 (
2155 "follows_pinyin_candidates",
2156 SchemeType::follows_pinyin_candidates,
2157 ),
2158 ("locks_caret", SchemeType::locks_caret),
2159 ("supports_fuzzy", SchemeType::supports_fuzzy),
2160 ("supports_autocorrect", SchemeType::supports_autocorrect),
2161 ("nine_key", SchemeType::nine_key),
2162 ("helpcode", SchemeType::helpcode),
2163 ];
2164 for (name, predicate) in on {
2165 assert!(predicate(scheme), "{name}");
2166 }
2167 for (name, predicate) in off {
2168 assert!(!predicate(scheme), "{name}");
2169 }
2170 }
2171
2172 #[test]
2174 fn vietnamese_and_tibetan_predicates() {
2175 let on: [Named; 4] = [
2176 (
2177 "cancel_keeps_composition",
2178 SchemeType::cancel_keeps_composition,
2179 ),
2180 ("commits_on_blur", SchemeType::commits_on_blur),
2181 ("locks_caret", SchemeType::locks_caret),
2182 ("selection_completes", SchemeType::selection_completes),
2183 ];
2184 let off: [Named; 22] = [
2185 ("is_chinese", SchemeType::is_chinese),
2186 (
2187 "outputs_traditional_natively",
2188 SchemeType::outputs_traditional_natively,
2189 ),
2190 (
2191 "script_conversion_applies",
2192 SchemeType::script_conversion_applies,
2193 ),
2194 (
2195 "uses_chinese_punctuation",
2196 SchemeType::uses_chinese_punctuation,
2197 ),
2198 ("host_smart_punctuation", SchemeType::host_smart_punctuation),
2199 ("widens_full_width", SchemeType::widens_full_width),
2200 (
2201 "allows_english_emoji_mixing",
2202 SchemeType::allows_english_emoji_mixing,
2203 ),
2204 ("opens_local_modes", SchemeType::opens_local_modes),
2205 (
2206 "learns_into_main_dictionary",
2207 SchemeType::learns_into_main_dictionary,
2208 ),
2209 ("learns_english_words", SchemeType::learns_english_words),
2210 ("cloud_eligible", SchemeType::cloud_eligible),
2211 ("shows_glosses", SchemeType::shows_glosses),
2212 ("holds_phrase_progress", SchemeType::holds_phrase_progress),
2213 (
2214 "reranks_with_sentence_model",
2215 SchemeType::reranks_with_sentence_model,
2216 ),
2217 ("draws_reading", SchemeType::draws_reading),
2218 (
2219 "has_openable_candidate_list",
2220 SchemeType::has_openable_candidate_list,
2221 ),
2222 (
2223 "follows_pinyin_candidates",
2224 SchemeType::follows_pinyin_candidates,
2225 ),
2226 ("accepts_apostrophe", SchemeType::accepts_apostrophe),
2227 ("supports_fuzzy", SchemeType::supports_fuzzy),
2228 ("supports_autocorrect", SchemeType::supports_autocorrect),
2229 ("nine_key", SchemeType::nine_key),
2230 ("helpcode", SchemeType::helpcode),
2231 ];
2232 for scheme in [SchemeType::Vietnamese, SchemeType::Tibetan] {
2233 for (name, predicate) in on {
2234 assert!(predicate(scheme), "{name} for {scheme:?}");
2235 }
2236 for (name, predicate) in off {
2237 assert!(!predicate(scheme), "{name} for {scheme:?}");
2238 }
2239 }
2240 }
2241
2242 #[test]
2244 fn scheme_set_all_covers_every_scheme() {
2245 for value in 0..=u8::MAX {
2246 match SchemeType::from_u8(value) {
2247 Some(scheme) => assert!(
2248 SchemeSet::ALL.contains(scheme),
2249 "{scheme:?} is outside SchemeSet::ALL"
2250 ),
2251 None => assert!(
2252 u32::from(value) >= u16::BITS || SchemeSet::ALL.0 & (1 << value) == 0,
2253 "SchemeSet::ALL sets bit {value}, which names no scheme"
2254 ),
2255 }
2256 }
2257 }
2258
2259 #[test]
2260 fn only_pinyin_and_wubi_read_the_main_dictionary() {
2261 assert!(SchemeSet::ALL.reads_main_dictionary());
2262 assert!(!SchemeSet::EMPTY.reads_main_dictionary());
2263 for value in 0..=u8::MAX {
2264 let Some(scheme) = SchemeType::from_u8(value) else {
2265 continue;
2266 };
2267 let reads = matches!(
2268 scheme,
2269 SchemeType::Quanpin | SchemeType::Shuangpin | SchemeType::Wubi
2270 );
2271 assert_eq!(
2272 SchemeSet::of(&[scheme]).reads_main_dictionary(),
2273 reads,
2274 "{scheme:?}"
2275 );
2276 }
2277 assert!(!SchemeSet::of(&[
2278 SchemeType::JapaneseRomaji,
2279 SchemeType::Vietnamese,
2280 SchemeType::Tibetan,
2281 ])
2282 .reads_main_dictionary());
2283 }
2284
2285 #[test]
2287 fn only_pinyin_and_wubi_require_the_english_dictionary() {
2288 assert!(SchemeSet::ALL.requires_english_dictionary());
2289 assert!(!SchemeSet::EMPTY.requires_english_dictionary());
2290 for value in 0..=u8::MAX {
2291 let Some(scheme) = SchemeType::from_u8(value) else {
2292 continue;
2293 };
2294 let set = SchemeSet::of(&[scheme]);
2295 let requires = matches!(
2296 scheme,
2297 SchemeType::Quanpin | SchemeType::Shuangpin | SchemeType::Wubi
2298 );
2299 assert_eq!(set.requires_english_dictionary(), requires, "{scheme:?}");
2300 assert!(!set.reads_main_dictionary() || set.requires_english_dictionary());
2301 }
2302 assert!(!SchemeSet::of(&[
2303 SchemeType::JapaneseRomaji,
2304 SchemeType::Korean,
2305 SchemeType::Vietnamese,
2306 SchemeType::Tibetan,
2307 SchemeType::Cantonese,
2308 SchemeType::Zhuyin,
2309 SchemeType::Stroke,
2310 ])
2311 .requires_english_dictionary());
2312 }
2313
2314 #[test]
2315 fn lattice_static_evidence_removes_syllable_bonus_and_counts_unicode_characters() {
2316 let mut item = WordItem::new("xian", "西安", 36000, CandidateSource::Generated, "xi'an");
2317 assert_eq!(item.static_evidence(), None);
2318 item.sentence_association = true;
2319 let expected = Some(crate::ordering::StaticEvidence {
2320 log_prob_milli_nats: -4000,
2321 characters: 2,
2322 });
2323 assert_eq!(item.static_evidence(), expected);
2324 item.source = CandidateSource::NeuralKeyboard;
2325 assert_eq!(item.static_evidence(), expected);
2326 item.source = CandidateSource::Database;
2327 assert_eq!(item.static_evidence(), None);
2328 item.source = CandidateSource::Generated;
2329 item.canonical_pinyin.clear();
2330 assert_eq!(item.static_evidence(), None);
2331 item.canonical_pinyin = "xi'an".into();
2332 item.word.clear();
2333 assert_eq!(item.static_evidence(), None);
2334 }
2335}