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
161impl Engine {
162 pub fn new(input_method: InputMethod) -> Self {
164 Self::with_config(input_method, Config::default())
165 }
166
167 pub fn with_config(input_method: InputMethod, config: Config) -> Self {
169 let mut rules_by_key: std::collections::BTreeMap<char, Vec<Rule>> =
170 std::collections::BTreeMap::new();
171 for rule in &input_method.rules {
172 let key = lower(rule.key);
173 rules_by_key.entry(key).or_default().push(*rule);
174 }
175
176 let total_rules: usize = rules_by_key.values().map(|v| v.len()).sum();
177 let mut all_rules_vec = Vec::with_capacity(total_rules);
178 let mut ascii_rule_indices = [(0u16, 0u16); 128];
179 let mut non_ascii_indices_vec = Vec::new();
180
181 for (key, rules) in rules_by_key {
182 let start = all_rules_vec.len() as u16;
183 all_rules_vec.extend(rules);
184 let end = all_rules_vec.len() as u16;
185 if key.is_ascii() {
186 ascii_rule_indices[key as usize] = (start, end);
187 } else {
188 non_ascii_indices_vec.push((key, (start, end)));
189 }
190 }
191
192 let mut ascii_effect_keys = [false; 128];
193 let mut non_ascii_effect_keys: Vec<char> = Vec::new();
194 for key in &input_method.keys {
195 if key.is_ascii() {
196 ascii_effect_keys[*key as usize] = true;
197 } else {
198 non_ascii_effect_keys.push(*key);
199 }
200 }
201 non_ascii_effect_keys.sort_unstable();
202 non_ascii_effect_keys.dedup();
203
204 Self {
205 committed_text: String::with_capacity(256),
206 active_buffer: [Transformation::default(); MAX_ACTIVE_TRANS],
207 active_len: 0,
208 input_method,
209 all_rules: all_rules_vec.into_boxed_slice(),
210 ascii_rule_indices,
211 non_ascii_rule_indices: non_ascii_indices_vec.into_boxed_slice(),
212 ascii_effect_keys,
213 non_ascii_effect_keys,
214 config,
215
216 work_comp: TransformationStack::new(),
217 scratch_comp: TransformationStack::new(),
218
219 prev_preedit: String::with_capacity(64),
220 delta_buf: String::with_capacity(64),
221 dfa: crate::dfa::Dfa::new(),
222 current_state_id: 0,
223
224 snapshots: [Snapshot {
225 active_buffer: [Transformation::default(); MAX_ACTIVE_TRANS],
226 active_len: 0,
227 current_state_id: 0,
228 }; MAX_ACTIVE_TRANS],
229 snapshot_len: 0,
230 }
231 }
232
233 #[inline]
234 pub(crate) fn active_slice(&self) -> &[Transformation] {
235 &self.active_buffer[..self.active_len]
236 }
237
238 fn take_active_into(&mut self, out: &mut TransformationStack) {
239 out.clear();
240 out.extend_from_slice(self.active_slice());
241 self.active_len = 0;
242 }
243
244 fn set_active_from_stack(&mut self, src: &mut TransformationStack) {
245 self.active_len = src.len().min(MAX_ACTIVE_TRANS);
246 self.active_buffer[..self.active_len].copy_from_slice(src.as_slice());
247 src.clear();
248 }
249
250 #[inline]
251 fn push_snapshot(&mut self) {
252 if self.snapshot_len < MAX_ACTIVE_TRANS {
253 let snap = &mut self.snapshots[self.snapshot_len];
254 snap.active_buffer[..self.active_len].copy_from_slice(&self.active_buffer[..self.active_len]);
255 snap.active_len = self.active_len;
256 snap.current_state_id = self.current_state_id;
257 self.snapshot_len += 1;
258 }
259 }
260
261 #[inline]
262 fn pop_snapshot(&mut self) -> Option<()> {
263 if self.snapshot_len == 0 {
264 return None;
265 }
266 self.snapshot_len -= 1;
267 let snap = &self.snapshots[self.snapshot_len];
268 self.active_len = snap.active_len;
269 self.active_buffer[..self.active_len].copy_from_slice(&snap.active_buffer[..self.active_len]);
270 self.current_state_id = snap.current_state_id;
271 Some(())
272 }
273
274 pub fn config(&self) -> Config {
276 self.config
277 }
278
279 pub fn set_config(&mut self, config: Config) {
281 self.config = config;
282 }
283
284 pub fn input_method(&self) -> InputMethod {
286 self.input_method.clone()
287 }
288
289 #[deprecated(
298 since = "0.3.4",
299 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."
300 )]
301 pub fn warm_up(&mut self) {
302 let mut compiler = crate::dfa::DfaCompiler::new(&self.input_method, self.config.to_flags());
303 compiler.compile_common();
304 self.dfa = compiler.dfa;
305 self.current_state_id = 0;
306 }
307
308 fn get_applicable_rules(&self, key: char) -> &[Rule] {
309 let key = lower(key);
310 if key.is_ascii() {
311 let (start, end) = self.ascii_rule_indices[key as usize];
312 &self.all_rules[start as usize..end as usize]
313 } else {
314 self.non_ascii_rule_indices
315 .binary_search_by_key(&key, |(k, _)| *k)
316 .map(|idx| {
317 let (start, end) = self.non_ascii_rule_indices[idx].1;
318 &self.all_rules[start as usize..end as usize]
319 })
320 .unwrap_or(&[])
321 }
322 }
323
324 fn can_process_key_raw(&self, lower_key: char) -> bool {
325 if crate::utils::is_alpha(lower_key)
326 || (lower_key.is_ascii() && self.ascii_effect_keys[lower_key as usize])
327 || self.non_ascii_effect_keys.binary_search(&lower_key).is_ok()
328 {
329 return true;
330 }
331 if crate::utils::is_word_break_symbol(lower_key) {
332 return false;
333 }
334 crate::utils::is_vietnamese_rune(lower_key)
335 }
336
337 fn generate_transformations(
338 &self,
339 composition: &mut TransformationStack,
340 key: char,
341 is_upper_case: bool,
342 ) {
343 let lower_key = lower(key);
344 let mut trans_buf = TransformationStack::new();
345
346 crate::bamboo_util::generate_transformations(
347 composition.as_slice(),
348 self.get_applicable_rules(lower_key),
349 self.config.to_flags(),
350 lower_key,
351 is_upper_case,
352 &mut trans_buf,
353 );
354
355 if trans_buf.is_empty() {
356 crate::bamboo_util::generate_fallback_transformations(
357 self.get_applicable_rules(lower_key),
358 lower_key,
359 is_upper_case,
360 &mut trans_buf,
361 );
362
363 let combined_len = composition.len() + trans_buf.len();
365 if combined_len <= MAX_ACTIVE_TRANS {
366 let mut tmp_data = [Transformation::default(); MAX_ACTIVE_TRANS];
367 tmp_data[..composition.len()].copy_from_slice(composition.as_slice());
368 tmp_data[composition.len()..combined_len].copy_from_slice(trans_buf.as_slice());
369
370 if !self.input_method.super_keys.is_empty() {
371 let current_str = crate::flattener::flatten_slice(
372 &tmp_data[..combined_len],
373 OutputOptions::TONE_LESS | OutputOptions::LOWER_CASE,
374 );
375 if uoh_tail_match(¤t_str) {
376 let (target, rule) = crate::bamboo_util::find_target(
377 &tmp_data[..combined_len],
378 self.get_applicable_rules(self.input_method.super_keys[0]),
379 self.config.to_flags(),
380 );
381 if let (Some(target), Some(mut rule)) = (target, rule) {
382 rule.key = '\0';
383 trans_buf.push(Transformation {
384 rule,
385 target: Some(target),
386 is_upper_case: false,
387 });
388 }
389 }
390 }
391 }
392 }
393 composition.extend_from_slice(trans_buf.as_slice());
394 if self.config.to_flags() & crate::bamboo_util::EFREE_TONE_MARKING != 0
395 && self.is_valid_internal(composition.as_slice(), false)
396 {
397 let mut extra = TransformationStack::new();
398 crate::bamboo_util::refresh_last_tone_target_into(
399 composition.as_mut_slice(),
400 self.config.to_flags() & crate::bamboo_util::ESTD_TONE_STYLE != 0,
401 &mut extra,
402 );
403 composition.extend_from_slice(extra.as_slice());
404 }
405 }
406
407 fn last_syllable_start(composition: &[Transformation]) -> usize {
408 let mut idx = composition.len();
409 let mut last_is_vowel = false;
410 let mut found_vowel = false;
411
412 while idx > 0 {
413 let tmp = &composition[idx - 1];
414 if tmp.target.is_none() {
415 let is_v = crate::utils::is_vowel(tmp.rule.result);
416 if found_vowel && !is_v && !last_is_vowel {
417 break;
418 }
419 if is_v {
420 found_vowel = true;
421 }
422 last_is_vowel = is_v;
423 }
424 idx -= 1;
425 }
426
427 idx
428 }
429
430 fn new_composition_in_place(
431 &self,
432 composition: &mut TransformationStack,
433 scratch: &mut TransformationStack,
434 key: char,
435 is_upper_case: bool,
436 ) {
437 let syllable_abs_start = Self::last_syllable_start(composition.as_slice());
438
439 composition.drain_to(syllable_abs_start, scratch);
440
441 let offset = syllable_abs_start;
442 if offset != 0 {
443 for t in scratch.as_mut_slice().iter_mut() {
444 if let Some(target) = t.target {
445 t.target = Some(target.saturating_sub(offset as u8));
446 }
447 }
448 }
449
450 self.generate_transformations(scratch, key, is_upper_case);
451
452 if offset != 0 {
453 for t in scratch.as_mut_slice().iter_mut() {
454 if let Some(target) = t.target {
455 t.target = Some(target + offset as u8);
456 }
457 }
458 }
459
460 composition.extend_from_slice(scratch.as_slice());
461 }
462
463 pub fn process(&mut self, s: &str, mode: Mode) -> String {
467 self.process_str(s, mode).output()
468 }
469
470 pub fn process_str(&mut self, s: &str, mode: Mode) -> &Self {
472 for key in s.chars() {
473 self.process_key(key, mode);
474 }
475 self
476 }
477
478 fn lcp_chars_and_bytes(a: &str, b: &str) -> (usize, usize) {
479 let mut lcp_chars = 0usize;
480 let mut lcp_bytes = 0usize;
481 for (ac, bc) in a.chars().zip(b.chars()) {
482 if ac == bc {
483 lcp_chars += 1;
484 lcp_bytes += ac.len_utf8();
485 } else {
486 break;
487 }
488 }
489 (lcp_chars, lcp_bytes)
490 }
491
492 pub fn process_key_delta(&mut self, key: char, mode: Mode) -> (usize, usize, &str) {
502 self.process_key(key, mode);
503
504 let active_len = self.active_len;
505 let active = &self.active_buffer[..active_len];
506 crate::flattener::flatten_slice_into(active, OutputOptions::NONE, &mut self.delta_buf);
507
508 let (_lcp_chars, lcp_bytes) =
509 Self::lcp_chars_and_bytes(&self.prev_preedit, &self.delta_buf);
510
511 let prev_bytes = self.prev_preedit.len();
512
513 let backspaces_chars = self.prev_preedit[lcp_bytes..].chars().count();
515 let backspaces_bytes = prev_bytes.saturating_sub(lcp_bytes);
516
517 std::mem::swap(&mut self.prev_preedit, &mut self.delta_buf);
518 let inserted = &self.prev_preedit[lcp_bytes..];
519 (backspaces_chars, backspaces_bytes, inserted)
520 }
521
522 pub fn process_key_delta_into(
527 &mut self,
528 key: char,
529 mode: Mode,
530 inserted: &mut String,
531 ) -> usize {
532 let (backspaces_chars, _backspaces_bytes, ins) = self.process_key_delta(key, mode);
533 inserted.clear();
534 inserted.push_str(ins);
535 backspaces_chars
536 }
537
538 pub fn process_key(&mut self, key: char, mode: Mode) {
542 let lower_key = lower(key);
543 let is_upper_case = is_upper(key);
544
545 if mode == Mode::English {
548 if crate::utils::is_word_break_symbol(lower_key) && self.active_len > 0 {
549 self.commit();
550 }
551 if self.active_len >= MAX_ACTIVE_TRANS {
552 self.commit();
553 }
554 self.active_buffer[self.active_len] =
555 crate::bamboo_util::new_appending_trans(lower_key, is_upper_case);
556 self.active_len += 1;
557 if crate::utils::is_word_break_symbol(lower_key) {
558 self.commit();
559 }
560 self.current_state_id = 0;
561 return;
562 }
563
564 if lower_key.is_ascii() && !is_upper_case {
567 let next_state_id =
568 self.dfa.get_state(self.current_state_id).transitions[lower_key as usize];
569 if next_state_id != 0 {
570 if self.active_len > 0 {
572 self.push_snapshot();
573 }
574 self.current_state_id = next_state_id;
575 let comp = self.dfa.get_composition(next_state_id);
576 self.active_len = comp.len().min(MAX_ACTIVE_TRANS);
577 self.active_buffer[..self.active_len].copy_from_slice(comp);
578 return;
579 }
580 }
581
582 if !self.can_process_key_raw(lower_key) {
584 if crate::utils::is_word_break_symbol(lower_key) {
585 self.commit();
586 }
587 if self.active_len > 0 {
591 self.push_snapshot();
592 }
593 let trans = crate::bamboo_util::new_appending_trans(lower_key, is_upper_case);
594 self.push_active(trans);
595 if crate::utils::is_word_break_symbol(lower_key) {
596 self.commit();
597 }
598 self.current_state_id = 0;
599 return;
600 }
601
602 self.push_snapshot();
604
605 let mut work = self.work_comp;
606 let mut scratch = self.scratch_comp;
607
608 self.take_active_into(&mut work);
609 self.new_composition_in_place(&mut work, &mut scratch, lower_key, is_upper_case);
610
611 if lower_key.is_ascii() && !is_upper_case && work.len() <= MAX_ACTIVE_TRANS {
613 let next_id = self.dfa.add_state(work.as_slice());
614 self.dfa.states[self.current_state_id as usize].transitions[lower_key as usize] =
615 next_id;
616 self.current_state_id = next_id;
617 } else {
618 self.current_state_id = self.dfa.find_state(work.as_slice()).unwrap_or(0);
619 }
620
621 self.set_active_from_stack(&mut work);
622
623 self.work_comp = work;
624 self.scratch_comp = scratch;
625 }
626
627 fn push_active(&mut self, trans: Transformation) {
628 if self.active_len >= MAX_ACTIVE_TRANS {
629 self.commit();
631 }
632 self.active_buffer[self.active_len] = trans;
633 self.active_len += 1;
634 self.current_state_id = self.dfa.find_state(self.active_slice()).unwrap_or(0);
635 }
636
637 pub fn commit(&mut self) {
639 if self.active_len == 0 {
640 return;
641 }
642 let word = self.output();
643 self.committed_text.push_str(&word);
644 self.active_len = 0;
645 self.current_state_id = 0;
646 self.snapshot_len = 0;
647 }
648
649 pub fn output(&self) -> String {
651 crate::flattener::flatten_slice(self.active_slice(), OutputOptions::NONE)
652 }
653
654 pub fn get_processed_str(&self, options: OutputOptions) -> String {
658 let active = self.active_slice();
659 if options.contains(OutputOptions::FULL_TEXT) {
660 let mut result = self.committed_text.clone();
661 result.push_str(&crate::flattener::flatten_slice(active, options));
662 return result;
663 }
664 if options.contains(OutputOptions::PUNCTUATION_MODE) {
665 if active.is_empty() {
666 return String::new();
667 }
668 let (_, tail) = crate::bamboo_util::extract_last_word_with_punctuation_marks(
669 active,
670 &self.input_method.keys,
671 );
672 return crate::flattener::flatten_slice(tail, OutputOptions::NONE);
673 }
674 crate::flattener::flatten_slice(active, options)
675 }
676
677 pub fn is_valid(&self, input_is_full_complete: bool) -> bool {
679 self.is_valid_internal(self.active_slice(), input_is_full_complete)
680 }
681
682 fn is_valid_internal(
683 &self,
684 composition: &[Transformation],
685 input_is_full_complete: bool,
686 ) -> bool {
687 crate::bamboo_util::is_valid(composition, input_is_full_complete)
688 }
689
690 pub fn restore_last_word(&mut self, to_vietnamese: bool) {
694 let mut work = self.work_comp;
695
696 self.take_active_into(&mut work);
697 if work.is_empty() {
698 self.set_active_from_stack(&mut work);
699 self.current_state_id = 0;
700 return;
701 }
702
703 let (prev_slice, last) =
704 crate::bamboo_util::extract_last_word(work.as_slice(), Some(&self.input_method.keys));
705
706 let mut previous = TransformationStack::new();
707 previous.extend_from_slice(prev_slice);
708
709 if last.is_empty() {
710 self.set_active_from_stack(&mut work);
711 self.current_state_id = 0;
712 return;
713 }
714 if !to_vietnamese {
715 previous.extend_from_slice(&crate::bamboo_util::break_composition_slice(last));
716 self.set_active_from_stack(&mut previous);
717 self.current_state_id = 0;
718 return;
719 }
720
721 let mut new_comp = TransformationStack::new();
722 let mut temp_engine = Self::with_config(self.input_method.clone(), self.config);
723
724 for t in last {
725 if t.rule.key == '\0' {
726 continue;
727 }
728 temp_engine.process_key(t.rule.key, Mode::Vietnamese);
729 }
730 new_comp.extend_from_slice(temp_engine.active_slice());
731
732 previous.extend_from_slice(new_comp.as_slice());
733
734 self.set_active_from_stack(&mut previous);
735 self.current_state_id = 0;
736 }
737
738 pub fn remove_last_char(&mut self, refresh_last_tone_target: bool) {
740 if self.pop_snapshot().is_none() {
741 return;
742 }
743
744 if refresh_last_tone_target && self.active_len > 0 {
745 let mut extra = TransformationStack::new();
746 crate::bamboo_util::refresh_last_tone_target_into(
747 &mut self.active_buffer[..self.active_len],
748 self.config.to_flags() & crate::bamboo_util::ESTD_TONE_STYLE != 0,
749 &mut extra,
750 );
751 for t in extra.as_slice() {
752 if self.active_len < MAX_ACTIVE_TRANS {
753 self.active_buffer[self.active_len] = *t;
754 self.active_len += 1;
755 }
756 }
757 }
758 }
759
760 pub fn reset(&mut self) {
762 self.committed_text.clear();
763 self.active_len = 0;
764 self.prev_preedit.clear();
765 self.delta_buf.clear();
766 self.current_state_id = 0;
767 self.snapshot_len = 0;
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774
775 #[test]
776 fn delta_backspaces_and_inserted() {
777 let telex = InputMethod::telex();
778 let mut e = Engine::new(telex);
779
780 let (bs1, _bb1, ins1) = e.process_key_delta('a', Mode::Vietnamese);
781 assert_eq!(bs1, 0, "First 'a' should have 0 backspaces");
782 assert_eq!(ins1, "a");
783
784 let (bs2, _bb2, ins2) = e.process_key_delta('s', Mode::Vietnamese);
785 assert_eq!(bs2, 1, "Adding 's' to 'a' should have 1 backspace for 'á'");
786 assert_eq!(ins2, "á");
787
788 let (bs3, _bb3, ins3) = e.process_key_delta(' ', Mode::Vietnamese);
789 assert_eq!(bs3, 1, "Space should clear the preedit 'á'");
790 assert_eq!(ins3, "");
791 }
792}