1use crate::config::Config;
4use crate::input_method::{InputMethod, Rule};
5use crate::mode::{Mode, OutputOptions};
6use crate::utils::{is_upper, lower};
7
8pub const MAX_ACTIVE_TRANS: usize = 16;
10
11#[derive(Clone, Copy)]
13struct Snapshot {
14 active_buffer: [Transformation; MAX_ACTIVE_TRANS],
15 active_len: usize,
16 current_state_id: u32,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Hash)]
21pub struct Transformation {
22 pub rule: Rule,
24 pub target: Option<u8>,
28 pub is_upper_case: bool,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Hash)]
36pub struct TransformationStack {
37 data: [Transformation; MAX_ACTIVE_TRANS],
38 len: usize,
39}
40
41impl TransformationStack {
42 pub fn new() -> Self {
44 Self { data: [Transformation::default(); MAX_ACTIVE_TRANS], len: 0 }
45 }
46
47 pub fn push(&mut self, t: Transformation) {
50 debug_assert!(
51 self.len < MAX_ACTIVE_TRANS,
52 "TransformationStack overflow: max {MAX_ACTIVE_TRANS} reached"
53 );
54 if self.len < MAX_ACTIVE_TRANS {
55 self.data[self.len] = t;
56 self.len += 1;
57 }
58 }
59
60 #[allow(dead_code)]
62 pub fn pop(&mut self) -> Option<Transformation> {
63 if self.len > 0 {
64 self.len -= 1;
65 Some(self.data[self.len])
66 } else {
67 None
68 }
69 }
70
71 pub fn clear(&mut self) {
73 self.len = 0;
74 }
75
76 pub fn len(&self) -> usize {
78 self.len
79 }
80
81 pub fn is_empty(&self) -> bool {
83 self.len == 0
84 }
85
86 pub fn as_slice(&self) -> &[Transformation] {
88 &self.data[..self.len]
89 }
90
91 pub fn as_mut_slice(&mut self) -> &mut [Transformation] {
93 &mut self.data[..self.len]
94 }
95
96 pub fn extend_from_slice(&mut self, other: &[Transformation]) {
98 let to_copy = other.len().min(MAX_ACTIVE_TRANS - self.len);
99 if to_copy > 0 {
100 self.data[self.len..self.len + to_copy].copy_from_slice(&other[..to_copy]);
101 self.len += to_copy;
102 }
103 }
104
105 pub fn drain_to(&mut self, start: usize, target: &mut TransformationStack) {
107 target.clear();
108 if start < self.len {
109 target.extend_from_slice(&self.data[start..self.len]);
110 self.len = start;
111 }
112 }
113}
114
115#[inline]
116fn uoh_tail_match(s: &str) -> bool {
117 for pat in ["uơ", "ưo"] {
118 if let Some(idx) = s.find(pat) {
119 let after = &s[idx + pat.len()..];
120 if after.chars().next().is_some_and(|c| c.is_alphabetic()) {
121 return true;
122 }
123 }
124 }
125 false
126}
127
128pub struct Engine {
133 committed_text: String,
134 active_buffer: [Transformation; MAX_ACTIVE_TRANS],
136 active_len: usize,
137
138 input_method: InputMethod,
139 all_rules: Box<[Rule]>,
140 ascii_rule_indices: [(u16, u16); 128],
141 non_ascii_rule_indices: Box<[(char, (u16, u16))]>,
142 ascii_effect_keys: [bool; 128],
143 non_ascii_effect_keys: Vec<char>,
144 config: Config,
145
146 work_comp: TransformationStack,
148 scratch_comp: TransformationStack,
149
150 prev_preedit: String,
151 delta_buf: String,
152
153 dfa: crate::dfa::Dfa,
154 current_state_id: u32,
155
156 snapshots: [Snapshot; MAX_ACTIVE_TRANS],
158 snapshot_len: usize,
159
160 scratch_engine: Option<Box<Engine>>,
162}
163
164impl Engine {
165 pub fn new(input_method: InputMethod) -> Self {
167 Self::with_config(input_method, Config::default())
168 }
169
170 pub fn with_config(input_method: InputMethod, config: Config) -> Self {
172 let mut rules_by_key: std::collections::BTreeMap<char, Vec<Rule>> =
173 std::collections::BTreeMap::new();
174 for rule in &input_method.rules {
175 let key = lower(rule.key);
176 rules_by_key.entry(key).or_default().push(*rule);
177 }
178
179 let total_rules: usize = rules_by_key.values().map(|v| v.len()).sum();
180 let mut all_rules_vec = Vec::with_capacity(total_rules);
181 let mut ascii_rule_indices = [(0u16, 0u16); 128];
182 let mut non_ascii_indices_vec = Vec::new();
183
184 for (key, rules) in rules_by_key {
185 let start = all_rules_vec.len() as u16;
186 all_rules_vec.extend(rules);
187 let end = all_rules_vec.len() as u16;
188 if key.is_ascii() {
189 ascii_rule_indices[key as usize] = (start, end);
190 } else {
191 non_ascii_indices_vec.push((key, (start, end)));
192 }
193 }
194
195 let mut ascii_effect_keys = [false; 128];
196 let mut non_ascii_effect_keys: Vec<char> = Vec::new();
197 for key in &input_method.keys {
198 if key.is_ascii() {
199 ascii_effect_keys[*key as usize] = true;
200 } else {
201 non_ascii_effect_keys.push(*key);
202 }
203 }
204 non_ascii_effect_keys.sort_unstable();
205 non_ascii_effect_keys.dedup();
206
207 Self {
208 committed_text: String::with_capacity(256),
209 active_buffer: [Transformation::default(); MAX_ACTIVE_TRANS],
210 active_len: 0,
211 input_method,
212 all_rules: all_rules_vec.into_boxed_slice(),
213 ascii_rule_indices,
214 non_ascii_rule_indices: non_ascii_indices_vec.into_boxed_slice(),
215 ascii_effect_keys,
216 non_ascii_effect_keys,
217 config,
218
219 work_comp: TransformationStack::new(),
220 scratch_comp: TransformationStack::new(),
221
222 prev_preedit: String::with_capacity(64),
223 delta_buf: String::with_capacity(64),
224 dfa: crate::dfa::Dfa::new(),
225 current_state_id: 0,
226
227 snapshots: [Snapshot {
228 active_buffer: [Transformation::default(); MAX_ACTIVE_TRANS],
229 active_len: 0,
230 current_state_id: 0,
231 }; MAX_ACTIVE_TRANS],
232 snapshot_len: 0,
233 scratch_engine: None,
234 }
235 }
236
237 #[inline]
238 pub(crate) fn active_slice(&self) -> &[Transformation] {
239 &self.active_buffer[..self.active_len]
240 }
241
242 fn take_active_into(&mut self, out: &mut TransformationStack) {
243 out.clear();
244 out.extend_from_slice(self.active_slice());
245 self.active_len = 0;
246 }
247
248 fn set_active_from_stack(&mut self, src: &mut TransformationStack) {
249 self.active_len = src.len().min(MAX_ACTIVE_TRANS);
250 self.active_buffer[..self.active_len].copy_from_slice(src.as_slice());
251 src.clear();
252 }
253
254 #[inline]
255 fn push_snapshot(&mut self) {
256 if self.snapshot_len < MAX_ACTIVE_TRANS {
257 let snap = &mut self.snapshots[self.snapshot_len];
258 snap.active_buffer[..self.active_len]
259 .copy_from_slice(&self.active_buffer[..self.active_len]);
260 snap.active_len = self.active_len;
261 snap.current_state_id = self.current_state_id;
262 self.snapshot_len += 1;
263 }
264 }
265
266 #[inline]
267 fn pop_snapshot(&mut self) -> Option<()> {
268 if self.snapshot_len == 0 {
269 return None;
270 }
271 self.snapshot_len -= 1;
272 let snap = &self.snapshots[self.snapshot_len];
273 self.active_len = snap.active_len;
274 self.active_buffer[..self.active_len]
275 .copy_from_slice(&snap.active_buffer[..self.active_len]);
276 self.current_state_id = snap.current_state_id;
277 Some(())
278 }
279
280 pub fn config(&self) -> Config {
282 self.config
283 }
284
285 pub fn set_config(&mut self, config: Config) {
287 self.config = config;
288 }
289
290 pub fn input_method(&self) -> InputMethod {
292 self.input_method.clone()
293 }
294
295 #[deprecated(
304 since = "0.3.4",
305 note = "Engine::warm_up() is unstable and may be removed. It uses a Telex-biased heuristic and may regress cold-start or non-Telex workloads."
306 )]
307 pub fn warm_up(&mut self) {
308 let mut compiler = crate::dfa::DfaCompiler::new(&self.input_method, self.config.to_flags());
309 compiler.compile_common();
310 self.dfa = compiler.dfa;
311 self.current_state_id = 0;
312 }
313
314 fn get_applicable_rules(&self, key: char) -> &[Rule] {
315 let key = lower(key);
316 if key.is_ascii() {
317 let (start, end) = self.ascii_rule_indices[key as usize];
318 &self.all_rules[start as usize..end as usize]
319 } else {
320 self.non_ascii_rule_indices
321 .binary_search_by_key(&key, |(k, _)| *k)
322 .map(|idx| {
323 let (start, end) = self.non_ascii_rule_indices[idx].1;
324 &self.all_rules[start as usize..end as usize]
325 })
326 .unwrap_or(&[])
327 }
328 }
329
330 fn can_process_key_raw(&self, lower_key: char) -> bool {
331 if crate::utils::is_alpha(lower_key)
332 || (lower_key.is_ascii() && self.ascii_effect_keys[lower_key as usize])
333 || self.non_ascii_effect_keys.binary_search(&lower_key).is_ok()
334 {
335 return true;
336 }
337 if crate::utils::is_word_break_symbol(lower_key) {
338 return false;
339 }
340 crate::utils::is_vietnamese_rune(lower_key)
341 }
342
343 fn generate_transformations(
344 &self,
345 composition: &mut TransformationStack,
346 key: char,
347 is_upper_case: bool,
348 ) {
349 let lower_key = lower(key);
350 let mut trans_buf = TransformationStack::new();
351
352 crate::bamboo_util::generate_transformations(
353 composition.as_slice(),
354 self.get_applicable_rules(lower_key),
355 self.config.to_flags(),
356 lower_key,
357 is_upper_case,
358 &mut trans_buf,
359 );
360
361 if trans_buf.is_empty() {
362 crate::bamboo_util::generate_fallback_transformations(
363 self.get_applicable_rules(lower_key),
364 lower_key,
365 is_upper_case,
366 &mut trans_buf,
367 );
368
369 let combined_len = composition.len() + trans_buf.len();
371 if combined_len <= MAX_ACTIVE_TRANS {
372 let mut tmp_data = [Transformation::default(); MAX_ACTIVE_TRANS];
373 tmp_data[..composition.len()].copy_from_slice(composition.as_slice());
374 tmp_data[composition.len()..combined_len].copy_from_slice(trans_buf.as_slice());
375
376 if !self.input_method.super_keys.is_empty() {
377 let current_str = crate::flattener::flatten_slice(
378 &tmp_data[..combined_len],
379 OutputOptions::TONE_LESS | OutputOptions::LOWER_CASE,
380 );
381 if uoh_tail_match(¤t_str) {
382 let (target, rule) = crate::bamboo_util::find_target(
383 &tmp_data[..combined_len],
384 self.get_applicable_rules(self.input_method.super_keys[0]),
385 self.config.to_flags(),
386 );
387 if let (Some(target), Some(mut rule)) = (target, rule) {
388 rule.key = '\0';
389 trans_buf.push(Transformation {
390 rule,
391 target: Some(target),
392 is_upper_case: false,
393 });
394 }
395 }
396 }
397 }
398 }
399 composition.extend_from_slice(trans_buf.as_slice());
400 if self.config.to_flags() & crate::bamboo_util::EFREE_TONE_MARKING != 0
401 && self.is_valid_internal(composition.as_slice(), false)
402 {
403 let mut extra = TransformationStack::new();
404 crate::bamboo_util::refresh_last_tone_target_into(
405 composition.as_mut_slice(),
406 self.config.to_flags() & crate::bamboo_util::ESTD_TONE_STYLE != 0,
407 &mut extra,
408 );
409 composition.extend_from_slice(extra.as_slice());
410 }
411 }
412
413 fn last_syllable_start(composition: &[Transformation]) -> usize {
414 let mut idx = composition.len();
415 let mut last_is_vowel = false;
416 let mut found_vowel = false;
417
418 while idx > 0 {
419 let tmp = &composition[idx - 1];
420 if tmp.target.is_none() {
421 let is_v = crate::utils::is_vowel(tmp.rule.result);
422 if found_vowel && !is_v && !last_is_vowel {
423 break;
424 }
425 if is_v {
426 found_vowel = true;
427 }
428 last_is_vowel = is_v;
429 }
430 idx -= 1;
431 }
432
433 idx
434 }
435
436 fn new_composition_in_place(
437 &self,
438 composition: &mut TransformationStack,
439 scratch: &mut TransformationStack,
440 key: char,
441 is_upper_case: bool,
442 ) {
443 let syllable_abs_start = Self::last_syllable_start(composition.as_slice());
444
445 composition.drain_to(syllable_abs_start, scratch);
446
447 let offset = syllable_abs_start;
448 if offset != 0 {
449 for t in scratch.as_mut_slice().iter_mut() {
450 if let Some(target) = t.target {
451 t.target = Some(target.saturating_sub(offset as u8));
452 }
453 }
454 }
455
456 self.generate_transformations(scratch, key, is_upper_case);
457
458 if offset != 0 {
459 for t in scratch.as_mut_slice().iter_mut() {
460 if let Some(target) = t.target {
461 t.target = Some(target + offset as u8);
462 }
463 }
464 }
465
466 composition.extend_from_slice(scratch.as_slice());
467 }
468
469 pub fn process(&mut self, s: &str, mode: Mode) -> String {
473 self.process_str(s, mode).output()
474 }
475
476 pub fn process_str(&mut self, s: &str, mode: Mode) -> &Self {
478 for key in s.chars() {
479 self.process_key(key, mode);
480 }
481 self
482 }
483
484 fn lcp_chars_and_bytes(a: &str, b: &str) -> (usize, usize) {
485 let mut lcp_chars = 0usize;
486 let mut lcp_bytes = 0usize;
487 for (ac, bc) in a.chars().zip(b.chars()) {
488 if ac == bc {
489 lcp_chars += 1;
490 lcp_bytes += ac.len_utf8();
491 } else {
492 break;
493 }
494 }
495 (lcp_chars, lcp_bytes)
496 }
497
498 pub fn process_key_delta(&mut self, key: char, mode: Mode) -> (usize, usize, &str) {
538 self.process_key(key, mode);
539
540 let active_len = self.active_len;
541 let active = &self.active_buffer[..active_len];
542 crate::flattener::flatten_slice_into(active, OutputOptions::NONE, &mut self.delta_buf);
543
544 let (_prefix_len, lcp_bytes) =
545 Self::lcp_chars_and_bytes(&self.prev_preedit, &self.delta_buf);
546
547 let prev_bytes = self.prev_preedit.len();
548
549 let backspace_count = self.prev_preedit[lcp_bytes..].chars().count();
551 let backspaces_bytes = prev_bytes.saturating_sub(lcp_bytes);
552
553 std::mem::swap(&mut self.prev_preedit, &mut self.delta_buf);
554 let inserted_suffix = &self.prev_preedit[lcp_bytes..];
555 (backspace_count, backspaces_bytes, inserted_suffix)
556 }
557
558 pub fn process_key_delta_into(
563 &mut self,
564 key: char,
565 mode: Mode,
566 inserted: &mut String,
567 ) -> usize {
568 let (backspace_count, _backspaces_bytes, ins) = self.process_key_delta(key, mode);
569 inserted.clear();
570 inserted.push_str(ins);
571 backspace_count
572 }
573
574 pub fn process_key(&mut self, key: char, mode: Mode) {
578 let lower_key = lower(key);
579 let is_upper_case = is_upper(key);
580
581 if mode == Mode::English {
584 if crate::utils::is_word_break_symbol(lower_key) && self.active_len > 0 {
585 self.commit();
586 }
587 if self.active_len >= MAX_ACTIVE_TRANS {
588 self.commit();
589 }
590 self.active_buffer[self.active_len] =
591 crate::bamboo_util::new_appending_trans(lower_key, is_upper_case);
592 self.active_len += 1;
593 if crate::utils::is_word_break_symbol(lower_key) {
594 self.commit();
595 }
596 self.current_state_id = 0;
597 return;
598 }
599
600 if lower_key.is_ascii() && !is_upper_case {
603 let next_state_id =
604 self.dfa.get_state(self.current_state_id).transitions[lower_key as usize];
605 if next_state_id != 0 {
606 if self.active_len > 0 {
608 self.push_snapshot();
609 }
610 self.current_state_id = next_state_id;
611 let comp = self.dfa.get_composition(next_state_id);
612 self.active_len = comp.len().min(MAX_ACTIVE_TRANS);
613 self.active_buffer[..self.active_len].copy_from_slice(comp);
614 return;
615 }
616 }
617
618 if !self.can_process_key_raw(lower_key) {
620 if crate::utils::is_word_break_symbol(lower_key) {
621 self.commit();
622 }
623 if self.active_len > 0 {
627 self.push_snapshot();
628 }
629 let trans = crate::bamboo_util::new_appending_trans(lower_key, is_upper_case);
630 self.push_active(trans);
631 if crate::utils::is_word_break_symbol(lower_key) {
632 self.commit();
633 }
634 self.current_state_id = 0;
635 return;
636 }
637
638 self.push_snapshot();
640
641 let mut work = self.work_comp;
642 let mut scratch = self.scratch_comp;
643
644 self.take_active_into(&mut work);
645 self.new_composition_in_place(&mut work, &mut scratch, lower_key, is_upper_case);
646
647 if lower_key.is_ascii() && !is_upper_case && work.len() <= MAX_ACTIVE_TRANS {
649 let next_id = self.dfa.add_state(work.as_slice());
650 self.dfa.states[self.current_state_id as usize].transitions[lower_key as usize] =
651 next_id;
652 self.current_state_id = next_id;
653 } else {
654 self.current_state_id = self.dfa.find_state(work.as_slice()).unwrap_or(0);
655 }
656
657 self.set_active_from_stack(&mut work);
658
659 self.work_comp = work;
660 self.scratch_comp = scratch;
661 }
662
663 fn push_active(&mut self, trans: Transformation) {
664 if self.active_len >= MAX_ACTIVE_TRANS {
665 self.commit();
667 }
668 self.active_buffer[self.active_len] = trans;
669 self.active_len += 1;
670 self.current_state_id = self.dfa.find_state(self.active_slice()).unwrap_or(0);
671 }
672
673 pub fn commit(&mut self) {
675 if self.active_len == 0 {
676 return;
677 }
678 let word = self.output();
679 self.committed_text.push_str(&word);
680 self.active_len = 0;
681 self.current_state_id = 0;
682 self.snapshot_len = 0;
683 }
684
685 pub fn output(&self) -> String {
687 crate::flattener::flatten_slice(self.active_slice(), OutputOptions::NONE)
688 }
689
690 pub fn get_processed_str(&self, options: OutputOptions) -> String {
694 let active = self.active_slice();
695 if options.contains(OutputOptions::FULL_TEXT) {
696 let mut result = self.committed_text.clone();
697 result.push_str(&crate::flattener::flatten_slice(active, options));
698 return result;
699 }
700 if options.contains(OutputOptions::PUNCTUATION_MODE) {
701 if active.is_empty() {
702 return String::new();
703 }
704 let (_, tail) = crate::bamboo_util::extract_last_word_with_punctuation_marks(
705 active,
706 &self.input_method.keys,
707 );
708 return crate::flattener::flatten_slice(tail, OutputOptions::NONE);
709 }
710 crate::flattener::flatten_slice(active, options)
711 }
712
713 pub fn is_valid(&self, input_is_full_complete: bool) -> bool {
715 self.is_valid_internal(self.active_slice(), input_is_full_complete)
716 }
717
718 fn is_valid_internal(
719 &self,
720 composition: &[Transformation],
721 input_is_full_complete: bool,
722 ) -> bool {
723 crate::bamboo_util::is_valid(composition, input_is_full_complete)
724 }
725
726 pub fn restore_last_word(&mut self, to_vietnamese: bool) {
730 let mut work = self.work_comp;
731
732 self.take_active_into(&mut work);
733 if work.is_empty() {
734 self.set_active_from_stack(&mut work);
735 self.current_state_id = 0;
736 return;
737 }
738
739 let (prev_slice, last) =
740 crate::bamboo_util::extract_last_word(work.as_slice(), Some(&self.input_method.keys));
741
742 let mut previous = TransformationStack::new();
743 previous.extend_from_slice(prev_slice);
744
745 if last.is_empty() {
746 self.set_active_from_stack(&mut work);
747 self.current_state_id = 0;
748 return;
749 }
750 if !to_vietnamese {
751 previous.extend_from_slice(&crate::bamboo_util::break_composition_slice(last));
752 self.set_active_from_stack(&mut previous);
753 self.current_state_id = 0;
754 return;
755 }
756
757 let mut new_comp = TransformationStack::new();
758 if self.scratch_engine.is_none() {
759 self.scratch_engine =
760 Some(Box::new(Self::with_config(self.input_method.clone(), self.config)));
761 }
762 let temp_engine = self.scratch_engine.as_mut().unwrap();
763 temp_engine.reset();
764
765 for t in last {
766 if t.rule.key == '\0' {
767 continue;
768 }
769 temp_engine.process_key(t.rule.key, Mode::Vietnamese);
770 }
771 new_comp.extend_from_slice(temp_engine.active_slice());
772
773 previous.extend_from_slice(new_comp.as_slice());
774
775 self.set_active_from_stack(&mut previous);
776 self.current_state_id = 0;
777 }
778
779 pub fn remove_last_char(&mut self, refresh_last_tone_target: bool) {
781 if self.pop_snapshot().is_none() {
782 return;
783 }
784
785 if refresh_last_tone_target && self.active_len > 0 {
786 let mut extra = TransformationStack::new();
787 crate::bamboo_util::refresh_last_tone_target_into(
788 &mut self.active_buffer[..self.active_len],
789 self.config.to_flags() & crate::bamboo_util::ESTD_TONE_STYLE != 0,
790 &mut extra,
791 );
792 for t in extra.as_slice() {
793 if self.active_len < MAX_ACTIVE_TRANS {
794 self.active_buffer[self.active_len] = *t;
795 self.active_len += 1;
796 }
797 }
798 }
799 }
800
801 pub fn reset(&mut self) {
803 self.committed_text.clear();
804 self.active_len = 0;
805 self.prev_preedit.clear();
806 self.delta_buf.clear();
807 self.current_state_id = 0;
808 self.snapshot_len = 0;
809 }
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815
816 #[test]
817 fn delta_backspaces_and_inserted() {
818 let telex = InputMethod::telex();
819 let mut e = Engine::new(telex);
820
821 let (bs1, _bb1, ins1) = e.process_key_delta('a', Mode::Vietnamese);
822 assert_eq!(bs1, 0, "First 'a' should have 0 backspaces");
823 assert_eq!(ins1, "a");
824
825 let (bs2, _bb2, ins2) = e.process_key_delta('s', Mode::Vietnamese);
826 assert_eq!(bs2, 1, "Adding 's' to 'a' should have 1 backspace for 'á'");
827 assert_eq!(ins2, "á");
828
829 let (bs3, _bb3, ins3) = e.process_key_delta(' ', Mode::Vietnamese);
830 assert_eq!(bs3, 1, "Space should clear the preedit 'á'");
831 assert_eq!(ins3, "");
832 }
833}