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
606impl Default for SchemeSet {
607 fn default() -> Self {
608 Self::ALL
609 }
610}
611
612#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
614#[repr(u8)]
615pub enum ShuangpinProfileKind {
616 #[default]
617 Xiaohe = 0,
618 Ziranma = 1,
619 Shoudao = 2,
620 Microsoft = 3,
621 Custom = 4,
623 SmartAbc = 5,
624}
625
626impl ShuangpinProfileKind {
627 pub fn from_u8(value: u8) -> Option<Self> {
628 Some(match value {
629 0 => Self::Xiaohe,
630 1 => Self::Ziranma,
631 2 => Self::Shoudao,
632 3 => Self::Microsoft,
633 4 => Self::Custom,
634 5 => Self::SmartAbc,
635 _ => return None,
636 })
637 }
638
639 pub fn from_name(name: &str) -> Option<Self> {
641 Some(match name {
642 "xiaohe" => Self::Xiaohe,
643 "ziranma" => Self::Ziranma,
644 "shoudao" => Self::Shoudao,
645 "microsoft" => Self::Microsoft,
646 "custom" => Self::Custom,
647 "smart_abc" => Self::SmartAbc,
648 _ => return None,
649 })
650 }
651
652 pub fn name(self) -> &'static str {
653 match self {
654 Self::Xiaohe => "xiaohe",
655 Self::Ziranma => "ziranma",
656 Self::Shoudao => "shoudao",
657 Self::Microsoft => "microsoft",
658 Self::Custom => "custom",
659 Self::SmartAbc => "smart_abc",
660 }
661 }
662}
663
664#[derive(Debug, Clone, PartialEq, Eq, Default)]
666pub struct ShuangpinCustomTable {
667 pub initials: Vec<(String, String)>,
668 pub finals: Vec<(String, String)>,
669 pub zero_initials: Vec<(String, String)>,
670}
671
672#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
674#[repr(u8)]
675pub enum NineKeyShuangpinKind {
676 Migala2 = 0,
678 #[default]
680 Migala3 = 1,
681 Gushi2Low = 2,
683 Custom = 3,
685}
686
687impl NineKeyShuangpinKind {
688 pub const BUILTIN: [Self; 3] = [Self::Migala2, Self::Migala3, Self::Gushi2Low];
690
691 pub fn from_u8(value: u8) -> Option<Self> {
692 Some(match value {
693 0 => Self::Migala2,
694 1 => Self::Migala3,
695 2 => Self::Gushi2Low,
696 3 => Self::Custom,
697 _ => return None,
698 })
699 }
700
701 pub fn from_name(name: &str) -> Option<Self> {
703 Some(match name {
704 "migala2" => Self::Migala2,
705 "migala3" => Self::Migala3,
706 "gushi2_low" => Self::Gushi2Low,
707 "custom" => Self::Custom,
708 _ => return None,
709 })
710 }
711
712 pub fn name(self) -> &'static str {
713 match self {
714 Self::Migala2 => "migala2",
715 Self::Migala3 => "migala3",
716 Self::Gushi2Low => "gushi2_low",
717 Self::Custom => "custom",
718 }
719 }
720}
721
722#[derive(Debug, Clone, PartialEq, Eq, Default)]
724pub struct NineKeyShuangpinTable {
725 pub initials: Vec<(String, String)>,
726 pub finals: Vec<(String, String)>,
727 pub zero_initials: Vec<(String, String)>,
728}
729
730#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
732#[repr(u8)]
733pub enum WubiProfileKind {
734 #[default]
735 Wubi86 = 0,
736 Wubi98 = 1,
737}
738
739impl WubiProfileKind {
740 pub fn from_u8(value: u8) -> Option<Self> {
741 Some(match value {
742 0 => Self::Wubi86,
743 1 => Self::Wubi98,
744 _ => return None,
745 })
746 }
747
748 pub fn from_name(name: &str) -> Option<Self> {
749 Some(match name {
750 "wubi86" => Self::Wubi86,
751 "wubi98" => Self::Wubi98,
752 _ => return None,
753 })
754 }
755
756 pub fn name(self) -> &'static str {
757 match self {
758 Self::Wubi86 => "wubi86",
759 Self::Wubi98 => "wubi98",
760 }
761 }
762
763 pub fn table(self) -> &'static str {
765 self.name()
766 }
767
768 pub fn dictionary_kind(self) -> PersonalDictionaryKind {
770 match self {
771 Self::Wubi86 => PersonalDictionaryKind::Wubi,
772 Self::Wubi98 => PersonalDictionaryKind::Wubi98,
773 }
774 }
775}
776
777pub use crate::shared_string::SharedString;
778
779#[derive(Debug, Clone, PartialEq, Default)]
781pub struct WordItem {
782 pub pinyin: SharedString,
784 pub canonical_pinyin: SharedString,
786 pub word: SharedString,
787 pub weight: i64,
789 pub source: CandidateSource,
790 pub scheme: SchemeType,
792 pub fixed_position: i32,
794 pub fuzzy: bool,
795 pub corrected_from: SharedString,
797 pub sentence_association: bool,
799 pub sentence_words: Vec<String>,
801 pub touch_supported: bool,
803 pub confident_correction: bool,
805}
806
807impl WordItem {
808 #[doc(hidden)]
810 pub fn static_evidence(&self) -> Option<crate::ordering::StaticEvidence> {
811 if !self.sentence_association
812 || !matches!(
813 self.source,
814 CandidateSource::Generated
815 | CandidateSource::NeuralKeyboard
816 | CandidateSource::Collocation
817 )
818 || self.canonical_pinyin.is_empty()
819 || self.word.is_empty()
820 {
821 return None;
822 }
823 let syllables = self.canonical_pinyin.split('\'').count();
824 let bonus =
825 (crate::lattice::decode::PHRASE_LENGTH_BONUS * 1000.0 * syllables as f64) as i64;
826 Some(crate::ordering::StaticEvidence {
827 log_prob_milli_nats: self.weight.saturating_sub(bonus),
828 characters: self.word.chars().count(),
829 })
830 }
831
832 pub fn new(
833 pinyin: impl Into<String>,
834 word: impl Into<String>,
835 weight: i64,
836 source: CandidateSource,
837 canonical_pinyin: impl Into<String>,
838 ) -> Self {
839 Self {
840 pinyin: SharedString::from(Into::<String>::into(pinyin)),
841 canonical_pinyin: SharedString::from(Into::<String>::into(canonical_pinyin)),
842 word: SharedString::from(Into::<String>::into(word)),
843 weight,
844 source,
845 ..Self::default()
846 }
847 }
848}
849
850pub mod fuzzy_rule {
852 pub const Z_ZH: u32 = 1 << 0;
853 pub const C_CH: u32 = 1 << 1;
854 pub const S_SH: u32 = 1 << 2;
855 pub const N_L: u32 = 1 << 3;
856 pub const F_H: u32 = 1 << 4;
857 pub const R_L: u32 = 1 << 5;
858 pub const AN_ANG: u32 = 1 << 6;
859 pub const EN_ENG: u32 = 1 << 7;
860 pub const IN_ING: u32 = 1 << 8;
861 pub const IAN_IANG: u32 = 1 << 9;
862 pub const UAN_UANG: u32 = 1 << 10;
863 pub const ALL: u32 = 0x7ff;
865}
866
867#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
868pub struct FuzzyPinyinOptions {
869 pub rules: u32,
870}
871
872impl FuzzyPinyinOptions {
873 pub fn enabled(self, rule: u32) -> bool {
874 self.rules & rule != 0
875 }
876}
877
878pub mod autocorrect_type {
880 pub const TRANSPOSITION: u32 = 1 << 0;
881 pub const NEIGHBOR: u32 = 1 << 1;
882 pub const DELETION: u32 = 1 << 2;
883 pub const INSERTION: u32 = 1 << 3;
884 pub const MISSING_OR_EXTRA: u32 = DELETION | INSERTION;
885}
886
887pub fn request_autocorrect_mask(transposition: bool, neighbor: bool) -> u32 {
889 let legacy = if transposition {
890 autocorrect_type::TRANSPOSITION
891 } else {
892 0
893 } | if neighbor {
894 autocorrect_type::NEIGHBOR
895 } else {
896 0
897 };
898 if legacy == 0 {
899 0
900 } else {
901 legacy | autocorrect_type::MISSING_OR_EXTRA
902 }
903}
904
905#[derive(Debug, Clone, PartialEq)]
907pub struct SentenceAssociationOptions {
908 pub word_lattice: bool,
909 pub neural_keyboard: bool,
911 pub show_next_on_duplicate: bool,
912 pub collocation_model: std::path::PathBuf,
915 pub collocation_weight: f64,
917 pub collocation_rerank: bool,
919 pub collocation_grammar: crate::lattice::octagram::GramConfig,
921}
922
923impl Default for SentenceAssociationOptions {
924 fn default() -> Self {
925 Self {
926 word_lattice: true,
927 neural_keyboard: false,
928 show_next_on_duplicate: false,
929 collocation_model: std::path::PathBuf::new(),
930 collocation_weight: 1.0,
931 collocation_rerank: false,
932 collocation_grammar: crate::lattice::octagram::GramConfig::default(),
933 }
934 }
935}
936
937pub const DEFAULT_FIRST_PAGE_ROWS: usize = 6;
939
940#[derive(Debug, Clone, Copy, PartialEq, Eq)]
942pub struct FirstPageRows(usize);
943
944impl FirstPageRows {
945 pub const fn new(rows: usize) -> Self {
946 Self(if rows == 0 { 1 } else { rows })
947 }
948
949 pub const fn get(self) -> usize {
950 self.0
951 }
952}
953
954impl Default for FirstPageRows {
955 fn default() -> Self {
956 Self(DEFAULT_FIRST_PAGE_ROWS)
957 }
958}
959
960#[derive(Debug, Clone, Copy, PartialEq, Eq)]
962pub struct ShuangpinHelpcodeModes {
963 pub manual_tab: bool,
965 pub direct: bool,
967 pub trigger_backtick: bool,
969 pub trigger_semicolon: bool,
971 pub trigger_uppercase: bool,
973 pub mid_sentence: bool,
975 pub mid_sentence_backtick: bool,
976 pub mid_sentence_semicolon: bool,
977 pub mid_sentence_uppercase: bool,
978 pub direct_helpcode: bool,
980 pub direct_helpcode_slash: bool,
981 pub direct_helpcode_uppercase: bool,
982}
983
984impl Default for ShuangpinHelpcodeModes {
985 fn default() -> Self {
987 Self {
988 manual_tab: false,
989 direct: true,
990 trigger_backtick: false,
991 trigger_semicolon: true,
992 trigger_uppercase: false,
993 mid_sentence: false,
994 mid_sentence_backtick: true,
995 mid_sentence_semicolon: false,
996 mid_sentence_uppercase: false,
997 direct_helpcode: false,
998 direct_helpcode_slash: true,
999 direct_helpcode_uppercase: false,
1000 }
1001 }
1002}
1003
1004#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1006pub struct SyllableHelpcode {
1007 pub syllable: usize,
1009 pub first: u8,
1011 pub second: u8,
1013}
1014
1015#[derive(Debug, Clone, PartialEq, Default)]
1017pub struct QueryRequest {
1018 pub scheme: SchemeType,
1019 pub raw_input: String,
1021 pub raw_input_with_cases: String,
1022 pub raw_input_with_helpcode_marks: String,
1025 pub syllable_helpcodes: Vec<SyllableHelpcode>,
1027 pub end_helpcodes: String,
1029 pub helpcode_source_index: Vec<usize>,
1032 pub helpcode_decorations: Vec<(usize, String)>,
1034 pub normalized_input: String,
1036 pub raw_segmentation: String,
1037 pub normalized_segmentation: String,
1038 pub segmentation: String,
1039 pub enable_shuangpin_helpcode: bool,
1040 pub shuangpin_helpcode_modes: ShuangpinHelpcodeModes,
1042 pub enable_quanpin_helpcode: bool,
1043 pub strict_quanpin_helpcode: bool,
1045 pub enable_quanpin_autocorrect_transposition: bool,
1047 pub enable_quanpin_autocorrect_neighbor: bool,
1048 pub fuzzy_pinyin: FuzzyPinyinOptions,
1049 pub sentence_alternatives: bool,
1051 pub sentence_association: SentenceAssociationOptions,
1052 pub first_page_rows: FirstPageRows,
1054 pub rescoring_context: String,
1056 pub korean_hanja: bool,
1058 pub wubi_z_wildcard: bool,
1060 pub wubi_grid: bool,
1062 pub key_touches: Vec<Option<crate::touch::KeyTouch>>,
1064 pub key_geometry: Option<std::sync::Arc<crate::touch::KeyGeometry>>,
1066 pub touch_tuning: crate::touch::TouchTuning,
1068 pub valid: bool,
1070}
1071
1072#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1074pub enum SchemeKey {
1075 Letter(u8),
1077 Apostrophe,
1078 Semicolon,
1080 Minus,
1082 Symbol(u8),
1084 Backspace,
1085 Requery,
1087}
1088
1089#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1091#[repr(u8)]
1092pub enum Command {
1093 Backspace = 0,
1094 CommitCandidate = 1,
1095 CommitRaw = 2,
1096 Cancel = 3,
1098 MoveLeft = 4,
1099 MoveRight = 5,
1100 MoveHome = 6,
1101 MoveEnd = 7,
1102 DeleteForward = 8,
1103 CycleKanaVariant = 9,
1105 CommitReading = 10,
1107 ConvertHanja = 11,
1109 ConversionLeft = 12,
1111 ConversionRight = 13,
1113}
1114
1115impl Command {
1116 pub fn from_u8(value: u8) -> Option<Self> {
1117 Some(match value {
1118 0 => Self::Backspace,
1119 1 => Self::CommitCandidate,
1120 2 => Self::CommitRaw,
1121 3 => Self::Cancel,
1122 4 => Self::MoveLeft,
1123 5 => Self::MoveRight,
1124 6 => Self::MoveHome,
1125 7 => Self::MoveEnd,
1126 8 => Self::DeleteForward,
1127 9 => Self::CycleKanaVariant,
1128 10 => Self::CommitReading,
1129 11 => Self::ConvertHanja,
1130 12 => Self::ConversionLeft,
1131 13 => Self::ConversionRight,
1132 _ => return None,
1133 })
1134 }
1135
1136 pub fn name(self) -> &'static str {
1137 match self {
1138 Self::Backspace => "Backspace",
1139 Self::CommitCandidate => "CommitCandidate",
1140 Self::CommitRaw => "CommitRaw",
1141 Self::Cancel => "Cancel",
1142 Self::MoveLeft => "MoveLeft",
1143 Self::MoveRight => "MoveRight",
1144 Self::MoveHome => "MoveHome",
1145 Self::MoveEnd => "MoveEnd",
1146 Self::DeleteForward => "DeleteForward",
1147 Self::CycleKanaVariant => "CycleKanaVariant",
1148 Self::CommitReading => "CommitReading",
1149 Self::ConvertHanja => "ConvertHanja",
1150 Self::ConversionLeft => "ConversionLeft",
1151 Self::ConversionRight => "ConversionRight",
1152 }
1153 }
1154
1155 pub fn from_name(name: &str) -> Option<Self> {
1156 (0..=13)
1157 .filter_map(Self::from_u8)
1158 .find(|command| command.name() == name)
1159 }
1160}
1161
1162#[derive(Debug, Clone, PartialEq, Eq, Default)]
1164pub struct KeyResult {
1165 pub handled: bool,
1166 pub commit: Option<String>,
1167 pub diagnostic: Option<String>,
1168}
1169
1170impl KeyResult {
1171 pub fn unhandled() -> Self {
1172 Self::default()
1173 }
1174
1175 pub fn handled() -> Self {
1176 Self {
1177 handled: true,
1178 ..Self::default()
1179 }
1180 }
1181
1182 pub fn committed(text: impl Into<String>) -> Self {
1183 Self {
1184 handled: true,
1185 commit: Some(text.into()),
1186 diagnostic: None,
1187 }
1188 }
1189
1190 pub fn with_diagnostic(mut self, diagnostic: Option<String>) -> Self {
1191 self.diagnostic = diagnostic;
1192 self
1193 }
1194
1195 pub fn diagnostic_code(&self) -> Option<crate::diagnostics::DiagnosticCode> {
1197 self.diagnostic
1198 .as_deref()
1199 .map(crate::diagnostics::DiagnosticCode::from_message)
1200 }
1201}
1202
1203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1204#[repr(u8)]
1205pub enum CandidateEdge {
1206 FirstHan = 0,
1207 LastHan = 1,
1208}
1209
1210impl CandidateEdge {
1211 pub fn character(self, text: &str) -> &str {
1213 match self {
1214 Self::FirstHan => crate::text::first_han_char(text),
1215 Self::LastHan => crate::text::last_han_char(text),
1216 }
1217 }
1218}
1219
1220#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1221pub enum LocalInputMode {
1222 #[default]
1223 None,
1224 Unicode,
1225 DateTime,
1226 QuickPhrase,
1227 Emoji,
1228 Kaomoji,
1229 SuperJianpin,
1230 TemporaryEnglish,
1231 TemporaryJapanese,
1232 Expression,
1234 Command,
1236 Mention,
1238 Url,
1240 Chaizi,
1242 Symbol,
1244}
1245
1246impl LocalInputMode {
1247 pub fn name(self) -> &'static str {
1249 match self {
1250 Self::None => "none",
1251 Self::Unicode => "unicode",
1252 Self::DateTime => "date_time",
1253 Self::QuickPhrase => "quick_phrase",
1254 Self::Emoji => "emoji",
1255 Self::Kaomoji => "kaomoji",
1256 Self::SuperJianpin => "super_jianpin",
1257 Self::TemporaryEnglish => "temporary_english",
1258 Self::TemporaryJapanese => "temporary_japanese",
1259 Self::Expression => "expression",
1260 Self::Command => "command",
1261 Self::Mention => "mention",
1262 Self::Url => "url",
1263 Self::Chaizi => "chaizi",
1264 Self::Symbol => "symbol",
1265 }
1266 }
1267
1268 pub fn from_name(name: &str) -> Option<Self> {
1270 [
1271 Self::None,
1272 Self::Unicode,
1273 Self::DateTime,
1274 Self::QuickPhrase,
1275 Self::Emoji,
1276 Self::Kaomoji,
1277 Self::SuperJianpin,
1278 Self::TemporaryEnglish,
1279 Self::TemporaryJapanese,
1280 Self::Expression,
1281 Self::Command,
1282 Self::Mention,
1283 Self::Url,
1284 Self::Chaizi,
1285 Self::Symbol,
1286 ]
1287 .into_iter()
1288 .find(|mode| mode.name() == name)
1289 }
1290
1291 pub fn spelling_symbols(self) -> &'static str {
1293 match self {
1294 Self::Unicode => "0123456789",
1295 Self::Expression => crate::local::expression::SPELLING_SYMBOLS,
1296 Self::Url => crate::local::url::SPELLING_SYMBOLS,
1297 Self::TemporaryJapanese => "-",
1298 _ => "",
1299 }
1300 }
1301
1302 pub fn generates_text(self) -> bool {
1304 matches!(self, Self::Expression | Self::Command | Self::Mention)
1305 }
1306
1307 pub fn without_entry_marker(self, text: &str) -> &str {
1309 match self {
1310 Self::TemporaryEnglish | Self::TemporaryJapanese => text.get(1..).unwrap_or_default(),
1311 _ => text,
1312 }
1313 }
1314
1315 pub fn annotates_rows(self) -> bool {
1317 matches!(
1318 self,
1319 Self::Expression | Self::Command | Self::Mention | Self::Chaizi
1320 )
1321 }
1322}
1323
1324#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1325pub enum FrequencyAdjustmentMode {
1326 #[default]
1327 Disabled,
1328 Pin,
1329 Halve,
1330 Linear,
1331 Promote,
1332}
1333
1334impl FrequencyAdjustmentMode {
1335 pub fn name(self) -> &'static str {
1337 match self {
1338 Self::Disabled => "disabled",
1339 Self::Pin => "pin",
1340 Self::Halve => "halve",
1341 Self::Linear => "linear",
1342 Self::Promote => "promote",
1343 }
1344 }
1345
1346 pub fn from_name(name: &str) -> Option<Self> {
1347 Some(match name {
1348 "disabled" => Self::Disabled,
1349 "pin" => Self::Pin,
1350 "halve" => Self::Halve,
1351 "linear" => Self::Linear,
1352 "promote" => Self::Promote,
1353 _ => return None,
1354 })
1355 }
1356}
1357
1358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1359pub struct FrequencyAdjustmentOptions {
1360 pub mode: FrequencyAdjustmentMode,
1361 pub trigger_count: i32,
1363 pub linear_step: i32,
1365}
1366
1367impl Default for FrequencyAdjustmentOptions {
1368 fn default() -> Self {
1369 Self {
1370 mode: FrequencyAdjustmentMode::Disabled,
1371 trigger_count: 1,
1372 linear_step: 1,
1373 }
1374 }
1375}
1376
1377#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1379pub struct LocalModeOptions {
1380 pub unicode: bool,
1381 pub date_time: bool,
1383 pub date_time_inline: bool,
1385 pub date_time_menu: bool,
1387 pub quick_phrase: bool,
1388 pub emoji: bool,
1389 pub kaomoji: bool,
1390 pub super_jianpin: bool,
1391 pub temporary_english: bool,
1392 pub temporary_japanese: bool,
1393 pub expression: bool,
1394 pub command: bool,
1395 pub mention: bool,
1396 pub chaizi: bool,
1398 pub symbol: bool,
1400 pub shift_letter_entry: bool,
1402}
1403
1404impl Default for LocalModeOptions {
1405 fn default() -> Self {
1406 Self {
1407 unicode: true,
1408 date_time: true,
1409 date_time_inline: true,
1410 date_time_menu: true,
1411 quick_phrase: true,
1412 emoji: true,
1413 kaomoji: true,
1414 super_jianpin: true,
1415 temporary_english: true,
1416 temporary_japanese: true,
1417 expression: false,
1418 command: false,
1419 mention: false,
1420 chaizi: false,
1421 symbol: true,
1422 shift_letter_entry: true,
1423 }
1424 }
1425}
1426
1427#[derive(Debug, Clone, PartialEq, Eq)]
1431pub struct CommandTableEntry {
1432 pub trigger: String,
1434 pub title: String,
1436 pub template: String,
1437}
1438
1439#[derive(Debug, Clone, PartialEq, Eq)]
1441pub struct PluginCommand {
1442 pub trigger: String,
1444 pub title: String,
1446 pub extended: bool,
1448}
1449
1450#[derive(Debug, Clone, PartialEq, Eq)]
1452pub struct QuickPhraseEntry {
1453 pub key: String,
1455 pub text: String,
1457}
1458
1459#[derive(Debug, Clone, PartialEq, Eq)]
1461pub struct MentionEntry {
1462 pub text: String,
1464 pub key: String,
1466 pub phone: String,
1468}
1469
1470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1471pub struct EnglishInputOptions {
1472 pub mixed_candidates: bool,
1473 pub minimum_prefix: usize,
1475 pub first_page_count: usize,
1477}
1478
1479impl Default for EnglishInputOptions {
1480 fn default() -> Self {
1481 Self {
1482 mixed_candidates: false,
1483 minimum_prefix: 2,
1484 first_page_count: 1,
1485 }
1486 }
1487}
1488
1489#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1490pub struct MixedExpressiveOptions {
1491 pub emoji_candidates: bool,
1492 pub kaomoji_candidates: bool,
1493 pub symbol_candidates: bool,
1495}
1496
1497#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1498pub struct WubiInputOptions {
1499 pub mixed_pinyin: bool,
1501 pub profile: WubiProfileKind,
1503 pub z_wildcard: bool,
1507}
1508
1509#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
1511#[serde(rename_all = "snake_case")]
1512pub enum OnlineInputMode {
1513 #[default]
1514 Pinyin,
1515 English,
1516}
1517
1518impl OnlineInputMode {
1519 pub fn is_pinyin(&self) -> bool {
1520 *self == Self::Pinyin
1521 }
1522}
1523
1524#[derive(Debug, Clone, PartialEq, Eq, Default)]
1526pub struct OnlineQuery {
1527 pub mode: OnlineInputMode,
1528 pub scheme: SchemeType,
1529 pub generation: u64,
1530 pub identity: String,
1531 pub query_text: String,
1532 pub cache_key: String,
1533 pub pinyin_segments: Vec<String>,
1534 pub cloud_eligible: bool,
1535 pub ai_eligible: bool,
1536 pub session_id: u64,
1537}
1538
1539#[derive(Debug, Clone, PartialEq, Eq, Default)]
1541pub struct CommandTranslationQuery {
1542 pub session_id: u64,
1543 pub text: String,
1544}
1545
1546#[derive(Debug, Clone, PartialEq, Eq, Default)]
1548pub struct PluginCommandQuery {
1549 pub session_id: u64,
1550 pub preedit: String,
1552 pub trigger: String,
1553 pub argument: String,
1555}
1556
1557#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
1561#[repr(u8)]
1562#[non_exhaustive]
1563pub enum PersonalDictionaryKind {
1564 #[default]
1565 Pinyin = 0,
1566 Wubi = 1,
1567 QuickPhrase = 2,
1568 English = 3,
1569 Wubi98 = 4,
1571}
1572
1573impl PersonalDictionaryKind {
1574 pub fn from_u8(value: u8) -> Option<Self> {
1575 Some(match value {
1576 0 => Self::Pinyin,
1577 1 => Self::Wubi,
1578 2 => Self::QuickPhrase,
1579 3 => Self::English,
1580 4 => Self::Wubi98,
1581 _ => return None,
1582 })
1583 }
1584
1585 pub fn is_wubi(self) -> bool {
1587 matches!(self, Self::Wubi | Self::Wubi98)
1588 }
1589
1590 pub fn wubi_table(self) -> Option<&'static str> {
1592 match self {
1593 Self::Wubi => Some(WubiProfileKind::Wubi86.table()),
1594 Self::Wubi98 => Some(WubiProfileKind::Wubi98.table()),
1595 Self::Pinyin | Self::QuickPhrase | Self::English => None,
1596 }
1597 }
1598
1599 pub fn journal_name(self) -> &'static str {
1601 match self {
1602 Self::Pinyin => "pinyin",
1603 Self::Wubi => "wubi",
1604 Self::QuickPhrase => "quick",
1605 Self::English => "english",
1606 Self::Wubi98 => "wubi98",
1607 }
1608 }
1609
1610 pub fn from_journal_name(name: &str) -> Option<Self> {
1611 Some(match name {
1612 "pinyin" => Self::Pinyin,
1613 "wubi" => Self::Wubi,
1614 "quick" => Self::QuickPhrase,
1615 "english" => Self::English,
1616 "wubi98" => Self::Wubi98,
1617 _ => return None,
1618 })
1619 }
1620}
1621
1622#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1624pub struct PersonalDictionaryEntry {
1625 pub kind: PersonalDictionaryKind,
1626 pub key: String,
1627 pub value: String,
1628 pub weight: i64,
1629}
1630
1631impl PersonalDictionaryEntry {
1632 pub const DEFAULT_WEIGHT: i64 = 100_000;
1633}
1634
1635impl Default for PersonalDictionaryEntry {
1636 fn default() -> Self {
1637 Self {
1638 kind: PersonalDictionaryKind::Pinyin,
1639 key: String::new(),
1640 value: String::new(),
1641 weight: Self::DEFAULT_WEIGHT,
1642 }
1643 }
1644}
1645
1646#[cfg(test)]
1647mod tests {
1648 use super::{CandidateSource, SchemeSet, SchemeType, WordItem};
1649
1650 #[test]
1652 fn candidate_sources_round_trip_and_mixed_japanese_matches_no_predicate() {
1653 for value in 0..=u8::MAX {
1654 match CandidateSource::from_u8(value) {
1655 Some(source) => assert_eq!(source as u8, value),
1656 None => assert!(value == 10 || value > 14, "{value}"),
1657 }
1658 }
1659 let japanese = CandidateSource::MixedJapanese;
1660 assert_eq!(CandidateSource::from_u8(13), Some(japanese));
1661 assert!(!japanese.is_dictionary());
1662 assert!(!japanese.is_generated_or_fallback());
1663 assert!(!japanese.is_sentence_learning());
1664 assert!(!japanese.is_online());
1665 assert!(!japanese.takes_online_seat());
1666 assert!(!japanese.holds_ai_seat());
1667 }
1668
1669 #[test]
1671 fn plugin_rows_match_no_predicate() {
1672 let plugin = CandidateSource::Plugin;
1673 assert_eq!(CandidateSource::from_u8(14), Some(plugin));
1674 assert_eq!(plugin as u8, 14);
1675 assert!(!plugin.is_dictionary());
1676 assert!(!plugin.is_generated_or_fallback());
1677 assert!(!plugin.is_sentence_learning());
1678 assert!(!plugin.is_online());
1679 assert!(!plugin.takes_online_seat());
1680 assert!(!plugin.holds_ai_seat());
1681 }
1682
1683 type Row = (&'static str, fn(SchemeType) -> bool, [bool; 5]);
1684 type Named = (&'static str, fn(SchemeType) -> bool);
1685
1686 const SCHEMES: [SchemeType; 5] = [
1687 SchemeType::Quanpin,
1688 SchemeType::Shuangpin,
1689 SchemeType::Wubi,
1690 SchemeType::JapaneseRomaji,
1691 SchemeType::Korean,
1692 ];
1693
1694 #[test]
1696 fn predicates_keep_the_behaviour_of_the_existing_schemes() {
1697 let table: [Row; 26] = [
1698 (
1699 "is_chinese",
1700 SchemeType::is_chinese,
1701 [true, true, true, false, false],
1702 ),
1703 (
1704 "outputs_traditional_natively",
1705 SchemeType::outputs_traditional_natively,
1706 [false; 5],
1707 ),
1708 (
1709 "script_conversion_applies",
1710 SchemeType::script_conversion_applies,
1711 [true, true, true, false, false],
1712 ),
1713 (
1714 "uses_chinese_punctuation",
1715 SchemeType::uses_chinese_punctuation,
1716 [true, true, true, true, false],
1717 ),
1718 (
1719 "host_smart_punctuation",
1720 SchemeType::host_smart_punctuation,
1721 [true, true, true, false, false],
1722 ),
1723 (
1724 "widens_full_width",
1725 SchemeType::widens_full_width,
1726 [true, true, true, true, false],
1727 ),
1728 (
1729 "allows_english_emoji_mixing",
1730 SchemeType::allows_english_emoji_mixing,
1731 [true, true, false, false, false],
1732 ),
1733 (
1734 "opens_local_modes",
1735 SchemeType::opens_local_modes,
1736 [true, true, false, false, false],
1737 ),
1738 (
1739 "learns_into_main_dictionary",
1740 SchemeType::learns_into_main_dictionary,
1741 [true, true, true, false, false],
1742 ),
1743 (
1744 "learns_english_words",
1745 SchemeType::learns_english_words,
1746 [true, true, false, false, false],
1747 ),
1748 (
1749 "cloud_eligible",
1750 SchemeType::cloud_eligible,
1751 [true, true, false, true, false],
1752 ),
1753 (
1754 "shows_glosses",
1755 SchemeType::shows_glosses,
1756 [true, true, true, false, true],
1757 ),
1758 (
1759 "commits_on_blur",
1760 SchemeType::commits_on_blur,
1761 [false, false, false, false, true],
1762 ),
1763 (
1764 "holds_phrase_progress",
1765 SchemeType::holds_phrase_progress,
1766 [true, true, true, true, false],
1767 ),
1768 (
1769 "reranks_with_sentence_model",
1770 SchemeType::reranks_with_sentence_model,
1771 [true, true, false, false, false],
1772 ),
1773 (
1774 "draws_reading",
1775 SchemeType::draws_reading,
1776 [false, false, false, true, true],
1777 ),
1778 (
1779 "has_openable_candidate_list",
1780 SchemeType::has_openable_candidate_list,
1781 [false, false, false, false, true],
1782 ),
1783 (
1784 "cancel_keeps_composition",
1785 SchemeType::cancel_keeps_composition,
1786 [false, false, false, false, true],
1787 ),
1788 (
1789 "selection_completes",
1790 SchemeType::selection_completes,
1791 [false, false, false, true, true],
1792 ),
1793 (
1794 "follows_pinyin_candidates",
1795 SchemeType::follows_pinyin_candidates,
1796 [true, true, false, false, false],
1797 ),
1798 (
1799 "accepts_apostrophe",
1800 SchemeType::accepts_apostrophe,
1801 [true, true, false, true, false],
1802 ),
1803 (
1804 "locks_caret",
1805 SchemeType::locks_caret,
1806 [false, false, false, false, true],
1807 ),
1808 (
1809 "supports_fuzzy",
1810 SchemeType::supports_fuzzy,
1811 [true, true, false, false, false],
1812 ),
1813 (
1814 "supports_autocorrect",
1815 SchemeType::supports_autocorrect,
1816 [true, false, false, false, false],
1817 ),
1818 (
1819 "nine_key",
1820 SchemeType::nine_key,
1821 [true, false, false, false, false],
1822 ),
1823 (
1824 "helpcode",
1825 SchemeType::helpcode,
1826 [true, true, false, false, false],
1827 ),
1828 ];
1829 for (name, predicate, expected) in table {
1830 for (scheme, want) in SCHEMES.into_iter().zip(expected) {
1831 assert_eq!(predicate(scheme), want, "{name} for {scheme:?}");
1832 }
1833 }
1834 }
1835
1836 #[test]
1837 fn only_the_chinese_typing_schemes_detect_urls() {
1838 for code in 0..=9 {
1839 let scheme = SchemeType::from_u8(code).expect("scheme code");
1840 let expected = matches!(
1841 scheme,
1842 SchemeType::Quanpin | SchemeType::Shuangpin | SchemeType::Wubi
1843 );
1844 assert_eq!(scheme.detects_urls(), expected, "{scheme:?}");
1845 }
1846 }
1847
1848 #[test]
1849 fn local_input_mode_names_round_trip() {
1850 use super::LocalInputMode;
1851 fn next(mode: LocalInputMode) -> Option<LocalInputMode> {
1853 Some(match mode {
1854 LocalInputMode::None => LocalInputMode::Unicode,
1855 LocalInputMode::Unicode => LocalInputMode::DateTime,
1856 LocalInputMode::DateTime => LocalInputMode::QuickPhrase,
1857 LocalInputMode::QuickPhrase => LocalInputMode::Emoji,
1858 LocalInputMode::Emoji => LocalInputMode::Kaomoji,
1859 LocalInputMode::Kaomoji => LocalInputMode::SuperJianpin,
1860 LocalInputMode::SuperJianpin => LocalInputMode::TemporaryEnglish,
1861 LocalInputMode::TemporaryEnglish => LocalInputMode::TemporaryJapanese,
1862 LocalInputMode::TemporaryJapanese => LocalInputMode::Expression,
1863 LocalInputMode::Expression => LocalInputMode::Command,
1864 LocalInputMode::Command => LocalInputMode::Mention,
1865 LocalInputMode::Mention => LocalInputMode::Url,
1866 LocalInputMode::Url => LocalInputMode::Chaizi,
1867 LocalInputMode::Chaizi => LocalInputMode::Symbol,
1868 LocalInputMode::Symbol => return None,
1869 })
1870 }
1871 let mut names = Vec::new();
1872 let mut mode = Some(LocalInputMode::None);
1873 while let Some(current) = mode {
1874 assert_eq!(
1875 LocalInputMode::from_name(current.name()),
1876 Some(current),
1877 "{current:?}"
1878 );
1879 names.push(current.name());
1880 mode = next(current);
1881 }
1882 assert_eq!(names.len(), 15);
1883 assert!(names.contains(&"url"));
1884 assert!(names.contains(&"chaizi"));
1885 assert!(names.contains(&"symbol"));
1886 assert_eq!(LocalInputMode::from_name("unknown"), None);
1887 }
1888
1889 #[test]
1890 fn existing_scheme_codes_are_unchanged() {
1891 for (code, scheme) in SCHEMES.into_iter().enumerate() {
1892 assert_eq!(scheme as u8, code as u8);
1893 assert_eq!(SchemeType::from_u8(code as u8), Some(scheme));
1894 }
1895 }
1896
1897 #[test]
1898 fn vietnamese_round_trips_through_code_seven() {
1899 assert_eq!(SchemeType::Vietnamese as u8, 7);
1900 assert_eq!(SchemeType::from_u8(7), Some(SchemeType::Vietnamese));
1901 assert_eq!(SchemeType::Vietnamese.name(), "vietnamese");
1902 assert!(!SchemeType::Vietnamese.is_pinyin());
1903 }
1904
1905 #[test]
1906 fn tibetan_round_trips_through_code_eight() {
1907 assert_eq!(SchemeType::Tibetan as u8, 8);
1908 assert_eq!(SchemeType::from_u8(8), Some(SchemeType::Tibetan));
1909 assert_eq!(SchemeType::Tibetan.name(), "tibetan");
1910 assert!(!SchemeType::Tibetan.is_pinyin());
1911 }
1912
1913 #[test]
1914 fn cantonese_round_trips_through_code_five() {
1915 assert_eq!(SchemeType::Cantonese as u8, 5);
1916 assert_eq!(SchemeType::from_u8(5), Some(SchemeType::Cantonese));
1917 assert_eq!(SchemeType::Cantonese.name(), "cantonese");
1918 assert!(!SchemeType::Cantonese.is_pinyin());
1919 }
1920
1921 #[test]
1922 fn zhuyin_round_trips_through_code_six() {
1923 assert_eq!(SchemeType::Zhuyin as u8, 6);
1924 assert_eq!(SchemeType::from_u8(6), Some(SchemeType::Zhuyin));
1925 assert_eq!(SchemeType::Zhuyin.name(), "zhuyin");
1926 assert!(!SchemeType::Zhuyin.is_pinyin());
1927 }
1928
1929 #[test]
1930 fn stroke_round_trips_through_code_nine() {
1931 assert_eq!(SchemeType::Stroke as u8, 9);
1932 assert_eq!(SchemeType::from_u8(9), Some(SchemeType::Stroke));
1933 assert_eq!(SchemeType::Stroke.name(), "stroke");
1934 assert!(!SchemeType::Stroke.is_pinyin());
1935 assert_eq!(SchemeType::from_u8(10), None);
1936 }
1937
1938 #[test]
1940 fn stroke_predicates() {
1941 let scheme = SchemeType::Stroke;
1942 let on: [Named; 6] = [
1943 ("is_chinese", SchemeType::is_chinese),
1944 (
1945 "outputs_traditional_natively",
1946 SchemeType::outputs_traditional_natively,
1947 ),
1948 (
1949 "uses_chinese_punctuation",
1950 SchemeType::uses_chinese_punctuation,
1951 ),
1952 ("host_smart_punctuation", SchemeType::host_smart_punctuation),
1953 ("widens_full_width", SchemeType::widens_full_width),
1954 ("draws_reading", SchemeType::draws_reading),
1955 ];
1956 let off: [Named; 20] = [
1957 (
1958 "cancel_keeps_composition",
1959 SchemeType::cancel_keeps_composition,
1960 ),
1961 (
1962 "script_conversion_applies",
1963 SchemeType::script_conversion_applies,
1964 ),
1965 (
1966 "allows_english_emoji_mixing",
1967 SchemeType::allows_english_emoji_mixing,
1968 ),
1969 ("opens_local_modes", SchemeType::opens_local_modes),
1970 (
1971 "learns_into_main_dictionary",
1972 SchemeType::learns_into_main_dictionary,
1973 ),
1974 ("learns_english_words", SchemeType::learns_english_words),
1975 ("cloud_eligible", SchemeType::cloud_eligible),
1976 ("shows_glosses", SchemeType::shows_glosses),
1977 ("commits_on_blur", SchemeType::commits_on_blur),
1978 ("holds_phrase_progress", SchemeType::holds_phrase_progress),
1979 (
1980 "reranks_with_sentence_model",
1981 SchemeType::reranks_with_sentence_model,
1982 ),
1983 (
1984 "has_openable_candidate_list",
1985 SchemeType::has_openable_candidate_list,
1986 ),
1987 ("selection_completes", SchemeType::selection_completes),
1988 (
1989 "follows_pinyin_candidates",
1990 SchemeType::follows_pinyin_candidates,
1991 ),
1992 ("accepts_apostrophe", SchemeType::accepts_apostrophe),
1993 ("locks_caret", SchemeType::locks_caret),
1994 ("supports_fuzzy", SchemeType::supports_fuzzy),
1995 ("supports_autocorrect", SchemeType::supports_autocorrect),
1996 ("nine_key", SchemeType::nine_key),
1997 ("helpcode", SchemeType::helpcode),
1998 ];
1999 for (name, predicate) in on {
2000 assert!(predicate(scheme), "{name}");
2001 }
2002 for (name, predicate) in off {
2003 assert!(!predicate(scheme), "{name}");
2004 }
2005 for (name, predicate) in on.into_iter().chain(off) {
2007 if name == "accepts_apostrophe" || name == "draws_reading" {
2008 assert_ne!(
2009 predicate(scheme),
2010 predicate(SchemeType::Cantonese),
2011 "{name}"
2012 );
2013 } else {
2014 assert_eq!(
2015 predicate(scheme),
2016 predicate(SchemeType::Cantonese),
2017 "{name}"
2018 );
2019 }
2020 }
2021 }
2022
2023 #[test]
2025 fn zhuyin_predicates() {
2026 let scheme = SchemeType::Zhuyin;
2027 let on: [Named; 10] = [
2028 ("is_chinese", SchemeType::is_chinese),
2029 (
2030 "cancel_keeps_composition",
2031 SchemeType::cancel_keeps_composition,
2032 ),
2033 (
2034 "outputs_traditional_natively",
2035 SchemeType::outputs_traditional_natively,
2036 ),
2037 (
2038 "uses_chinese_punctuation",
2039 SchemeType::uses_chinese_punctuation,
2040 ),
2041 ("widens_full_width", SchemeType::widens_full_width),
2042 ("commits_on_blur", SchemeType::commits_on_blur),
2043 ("draws_reading", SchemeType::draws_reading),
2044 (
2045 "has_openable_candidate_list",
2046 SchemeType::has_openable_candidate_list,
2047 ),
2048 ("locks_caret", SchemeType::locks_caret),
2049 ("nine_key", SchemeType::nine_key),
2050 ];
2051 let off: [Named; 16] = [
2052 (
2053 "script_conversion_applies",
2054 SchemeType::script_conversion_applies,
2055 ),
2056 ("host_smart_punctuation", SchemeType::host_smart_punctuation),
2057 (
2058 "allows_english_emoji_mixing",
2059 SchemeType::allows_english_emoji_mixing,
2060 ),
2061 ("opens_local_modes", SchemeType::opens_local_modes),
2062 (
2063 "learns_into_main_dictionary",
2064 SchemeType::learns_into_main_dictionary,
2065 ),
2066 ("learns_english_words", SchemeType::learns_english_words),
2067 ("cloud_eligible", SchemeType::cloud_eligible),
2068 ("shows_glosses", SchemeType::shows_glosses),
2069 ("holds_phrase_progress", SchemeType::holds_phrase_progress),
2070 (
2071 "reranks_with_sentence_model",
2072 SchemeType::reranks_with_sentence_model,
2073 ),
2074 ("selection_completes", SchemeType::selection_completes),
2075 (
2076 "follows_pinyin_candidates",
2077 SchemeType::follows_pinyin_candidates,
2078 ),
2079 ("accepts_apostrophe", SchemeType::accepts_apostrophe),
2080 ("supports_fuzzy", SchemeType::supports_fuzzy),
2081 ("supports_autocorrect", SchemeType::supports_autocorrect),
2082 ("helpcode", SchemeType::helpcode),
2083 ];
2084 for (name, predicate) in on {
2085 assert!(predicate(scheme), "{name}");
2086 }
2087 for (name, predicate) in off {
2088 assert!(!predicate(scheme), "{name}");
2089 }
2090 }
2091
2092 #[test]
2094 fn cantonese_predicates() {
2095 let scheme = SchemeType::Cantonese;
2096 let on: [Named; 6] = [
2097 ("is_chinese", SchemeType::is_chinese),
2098 (
2099 "outputs_traditional_natively",
2100 SchemeType::outputs_traditional_natively,
2101 ),
2102 (
2103 "uses_chinese_punctuation",
2104 SchemeType::uses_chinese_punctuation,
2105 ),
2106 ("host_smart_punctuation", SchemeType::host_smart_punctuation),
2107 ("widens_full_width", SchemeType::widens_full_width),
2108 ("accepts_apostrophe", SchemeType::accepts_apostrophe),
2109 ];
2110 let off: [Named; 20] = [
2111 (
2112 "cancel_keeps_composition",
2113 SchemeType::cancel_keeps_composition,
2114 ),
2115 (
2116 "script_conversion_applies",
2117 SchemeType::script_conversion_applies,
2118 ),
2119 (
2120 "allows_english_emoji_mixing",
2121 SchemeType::allows_english_emoji_mixing,
2122 ),
2123 ("opens_local_modes", SchemeType::opens_local_modes),
2124 (
2125 "learns_into_main_dictionary",
2126 SchemeType::learns_into_main_dictionary,
2127 ),
2128 ("learns_english_words", SchemeType::learns_english_words),
2129 ("cloud_eligible", SchemeType::cloud_eligible),
2130 ("shows_glosses", SchemeType::shows_glosses),
2131 ("commits_on_blur", SchemeType::commits_on_blur),
2132 ("holds_phrase_progress", SchemeType::holds_phrase_progress),
2133 (
2134 "reranks_with_sentence_model",
2135 SchemeType::reranks_with_sentence_model,
2136 ),
2137 ("draws_reading", SchemeType::draws_reading),
2138 (
2139 "has_openable_candidate_list",
2140 SchemeType::has_openable_candidate_list,
2141 ),
2142 ("selection_completes", SchemeType::selection_completes),
2143 (
2144 "follows_pinyin_candidates",
2145 SchemeType::follows_pinyin_candidates,
2146 ),
2147 ("locks_caret", SchemeType::locks_caret),
2148 ("supports_fuzzy", SchemeType::supports_fuzzy),
2149 ("supports_autocorrect", SchemeType::supports_autocorrect),
2150 ("nine_key", SchemeType::nine_key),
2151 ("helpcode", SchemeType::helpcode),
2152 ];
2153 for (name, predicate) in on {
2154 assert!(predicate(scheme), "{name}");
2155 }
2156 for (name, predicate) in off {
2157 assert!(!predicate(scheme), "{name}");
2158 }
2159 }
2160
2161 #[test]
2163 fn vietnamese_and_tibetan_predicates() {
2164 let on: [Named; 4] = [
2165 (
2166 "cancel_keeps_composition",
2167 SchemeType::cancel_keeps_composition,
2168 ),
2169 ("commits_on_blur", SchemeType::commits_on_blur),
2170 ("locks_caret", SchemeType::locks_caret),
2171 ("selection_completes", SchemeType::selection_completes),
2172 ];
2173 let off: [Named; 22] = [
2174 ("is_chinese", SchemeType::is_chinese),
2175 (
2176 "outputs_traditional_natively",
2177 SchemeType::outputs_traditional_natively,
2178 ),
2179 (
2180 "script_conversion_applies",
2181 SchemeType::script_conversion_applies,
2182 ),
2183 (
2184 "uses_chinese_punctuation",
2185 SchemeType::uses_chinese_punctuation,
2186 ),
2187 ("host_smart_punctuation", SchemeType::host_smart_punctuation),
2188 ("widens_full_width", SchemeType::widens_full_width),
2189 (
2190 "allows_english_emoji_mixing",
2191 SchemeType::allows_english_emoji_mixing,
2192 ),
2193 ("opens_local_modes", SchemeType::opens_local_modes),
2194 (
2195 "learns_into_main_dictionary",
2196 SchemeType::learns_into_main_dictionary,
2197 ),
2198 ("learns_english_words", SchemeType::learns_english_words),
2199 ("cloud_eligible", SchemeType::cloud_eligible),
2200 ("shows_glosses", SchemeType::shows_glosses),
2201 ("holds_phrase_progress", SchemeType::holds_phrase_progress),
2202 (
2203 "reranks_with_sentence_model",
2204 SchemeType::reranks_with_sentence_model,
2205 ),
2206 ("draws_reading", SchemeType::draws_reading),
2207 (
2208 "has_openable_candidate_list",
2209 SchemeType::has_openable_candidate_list,
2210 ),
2211 (
2212 "follows_pinyin_candidates",
2213 SchemeType::follows_pinyin_candidates,
2214 ),
2215 ("accepts_apostrophe", SchemeType::accepts_apostrophe),
2216 ("supports_fuzzy", SchemeType::supports_fuzzy),
2217 ("supports_autocorrect", SchemeType::supports_autocorrect),
2218 ("nine_key", SchemeType::nine_key),
2219 ("helpcode", SchemeType::helpcode),
2220 ];
2221 for scheme in [SchemeType::Vietnamese, SchemeType::Tibetan] {
2222 for (name, predicate) in on {
2223 assert!(predicate(scheme), "{name} for {scheme:?}");
2224 }
2225 for (name, predicate) in off {
2226 assert!(!predicate(scheme), "{name} for {scheme:?}");
2227 }
2228 }
2229 }
2230
2231 #[test]
2233 fn scheme_set_all_covers_every_scheme() {
2234 for value in 0..=u8::MAX {
2235 match SchemeType::from_u8(value) {
2236 Some(scheme) => assert!(
2237 SchemeSet::ALL.contains(scheme),
2238 "{scheme:?} is outside SchemeSet::ALL"
2239 ),
2240 None => assert!(
2241 u32::from(value) >= u16::BITS || SchemeSet::ALL.0 & (1 << value) == 0,
2242 "SchemeSet::ALL sets bit {value}, which names no scheme"
2243 ),
2244 }
2245 }
2246 }
2247
2248 #[test]
2249 fn only_pinyin_and_wubi_read_the_main_dictionary() {
2250 assert!(SchemeSet::ALL.reads_main_dictionary());
2251 assert!(!SchemeSet::EMPTY.reads_main_dictionary());
2252 for value in 0..=u8::MAX {
2253 let Some(scheme) = SchemeType::from_u8(value) else {
2254 continue;
2255 };
2256 let reads = matches!(
2257 scheme,
2258 SchemeType::Quanpin | SchemeType::Shuangpin | SchemeType::Wubi
2259 );
2260 assert_eq!(
2261 SchemeSet::of(&[scheme]).reads_main_dictionary(),
2262 reads,
2263 "{scheme:?}"
2264 );
2265 }
2266 assert!(!SchemeSet::of(&[
2267 SchemeType::JapaneseRomaji,
2268 SchemeType::Vietnamese,
2269 SchemeType::Tibetan,
2270 ])
2271 .reads_main_dictionary());
2272 }
2273
2274 #[test]
2275 fn lattice_static_evidence_removes_syllable_bonus_and_counts_unicode_characters() {
2276 let mut item = WordItem::new("xian", "西安", 36000, CandidateSource::Generated, "xi'an");
2277 assert_eq!(item.static_evidence(), None);
2278 item.sentence_association = true;
2279 let expected = Some(crate::ordering::StaticEvidence {
2280 log_prob_milli_nats: -4000,
2281 characters: 2,
2282 });
2283 assert_eq!(item.static_evidence(), expected);
2284 item.source = CandidateSource::NeuralKeyboard;
2285 assert_eq!(item.static_evidence(), expected);
2286 item.source = CandidateSource::Database;
2287 assert_eq!(item.static_evidence(), None);
2288 item.source = CandidateSource::Generated;
2289 item.canonical_pinyin.clear();
2290 assert_eq!(item.static_evidence(), None);
2291 item.canonical_pinyin = "xi'an".into();
2292 item.word.clear();
2293 assert_eq!(item.static_evidence(), None);
2294 }
2295}