1use crate::config::{BkspUnit, Layout};
12use crate::expr::{Ctx, Value};
13use crate::ngs_seq::ngs_seq;
14use crate::unit::{self, Category, Jamo, Unit};
15
16#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
18struct Syllable {
19 cho: Option<u32>,
20 jung: Option<u32>,
21 jong: Option<u32>,
22}
23
24impl Syllable {
25 fn is_empty(&self) -> bool {
26 self.cho.is_none() && self.jung.is_none() && self.jong.is_none()
27 }
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Default)]
32pub struct KeyOutcome {
33 pub commit: String,
35 pub preedit: String,
37 pub consumed: bool,
39 pub delete_before: u32,
43}
44
45#[derive(Debug, Clone)]
47pub struct Engine {
48 layout: Layout,
49 cur: Syllable,
50 history: Vec<Unit>,
52 auto_state: i64,
54 bksp_streak: Option<BkspUnit>,
57 prev_syllables: Vec<Vec<Unit>>,
62 last_overwrite: Option<(Category, u32, u32)>,
66 replaying: bool,
69 surrounding_ok: bool,
73}
74
75impl Engine {
76 pub fn new(layout: Layout) -> Self {
77 let auto_state = layout.automata_start;
78 Self {
79 layout,
80 cur: Syllable::default(),
81 history: Vec::new(),
82 auto_state,
83 bksp_streak: None,
84 prev_syllables: Vec::new(),
85 last_overwrite: None,
86 replaying: false,
87 surrounding_ok: false,
88 }
89 }
90
91 pub fn layout(&self) -> &Layout {
92 &self.layout
93 }
94
95 pub fn set_surrounding_ok(&mut self, ok: bool) {
97 self.surrounding_ok = ok;
98 }
99
100 pub fn is_empty(&self) -> bool {
102 self.cur.is_empty()
103 }
104
105 fn t_state(&self) -> i64 {
107 if self.cur.is_empty() {
108 0
109 } else if self.cur.jung.is_some() {
110 2
111 } else {
112 1
113 }
114 }
115
116 fn render(&self, syl: &Syllable) -> String {
118 if syl.is_empty() {
119 return String::new();
120 }
121 if let (Some(cho), Some(jung)) = (syl.cho, syl.jung) {
123 if let Some(ch) = hanmo::compose(cho, jung, syl.jong) {
124 return ch.to_string(); }
126 let mut s = String::new();
128 for cp in [Some(cho), Some(jung), syl.jong].into_iter().flatten() {
129 if let Some(c) = char::from_u32(cp) {
130 s.push(c);
131 }
132 }
133 return s;
134 }
135 let count = [syl.cho, syl.jung, syl.jong]
140 .iter()
141 .filter(|x| x.is_some())
142 .count();
143 if count >= 2 {
144 const CHOSEONG_FILLER: u32 = 0x115F;
145 const JUNGSEONG_FILLER: u32 = 0x1160;
146 let mut s = String::new();
147 for cp in [
148 Some(syl.cho.unwrap_or(CHOSEONG_FILLER)),
149 Some(syl.jung.unwrap_or(JUNGSEONG_FILLER)),
150 syl.jong,
151 ]
152 .into_iter()
153 .flatten()
154 {
155 if let Some(c) = char::from_u32(cp) {
156 s.push(c);
157 }
158 }
159 return s;
160 }
161 let mut s = String::new();
163 for (cat, cp) in [
164 (Category::Cho, syl.cho),
165 (Category::Jung, syl.jung),
166 (Category::Jong, syl.jong),
167 ] {
168 if let Some(cp) = cp {
169 if let Some(ch) = self.layout.standalone(Jamo::new(cat, cp)) {
170 s.push(ch);
171 } else if let Some(ch) = char::from_u32(cp) {
172 s.push(ch);
173 }
174 }
175 }
176 s
177 }
178
179 pub fn preedit(&self) -> String {
181 self.render(&self.cur)
182 }
183
184 const MAX_PREV_SYLLABLES: usize = 32;
186
187 fn commit_current(&mut self) -> String {
190 let s = self.render(&self.cur);
191 if !self.cur.is_empty() && !self.history.is_empty() {
192 self.prev_syllables.push(self.history.clone());
195 if self.prev_syllables.len() > Self::MAX_PREV_SYLLABLES {
196 self.prev_syllables.remove(0);
197 }
198 }
199 self.cur = Syllable::default();
200 self.last_overwrite = None; s
202 }
203
204 fn commit_and_clear(&mut self) -> String {
209 let s = self.commit_current();
210 self.history.clear();
211 self.prev_syllables.clear();
212 s
213 }
214
215 pub fn flush(&mut self) -> String {
217 let s = self.commit_current();
218 self.history.clear();
219 self.prev_syllables.clear();
220 self.auto_state = self.layout.automata_start;
221 self.bksp_streak = None;
222 s
223 }
224
225 pub fn reset(&mut self) {
227 self.cur = Syllable::default();
228 self.history.clear();
229 self.prev_syllables.clear();
230 self.auto_state = self.layout.automata_start;
231 self.bksp_streak = None;
232 self.last_overwrite = None;
233 }
234
235 fn feed_cho(&mut self, cp: u32) -> String {
238 if self.cur.is_empty() {
239 self.cur.cho = Some(cp);
240 return String::new();
241 }
242 if self.cur.cho.is_some() && self.cur.jung.is_none() && self.cur.jong.is_none() {
244 if let Some(r) = self
245 .layout
246 .combine(Category::Cho, self.cur.cho.unwrap(), cp)
247 {
248 self.cur.cho = Some(r);
249 return String::new();
250 }
251 }
252 let out = self.commit_current();
254 self.cur.cho = Some(cp);
255 out
256 }
257
258 fn feed_jung(&mut self, cp: u32) -> String {
259 if self.cur.jung.is_none() && self.cur.jong.is_none() {
261 self.cur.jung = Some(cp);
262 return String::new();
263 }
264 if self.cur.jung.is_some() && self.cur.jong.is_none() {
266 if let Some(r) = self
267 .layout
268 .combine(Category::Jung, self.cur.jung.unwrap(), cp)
269 {
270 self.cur.jung = Some(r);
271 return String::new();
272 }
273 }
274 let out = self.commit_current();
276 self.cur.jung = Some(cp);
277 out
278 }
279
280 fn feed_jong(&mut self, cp: u32) -> String {
281 if self.cur.cho.is_some() && self.cur.jung.is_some() && self.cur.jong.is_none() {
283 self.cur.jong = Some(cp);
284 return String::new();
285 }
286 if self.cur.jong.is_some() {
288 if let Some(r) = self
289 .layout
290 .combine(Category::Jong, self.cur.jong.unwrap(), cp)
291 {
292 self.cur.jong = Some(r);
293 return String::new();
294 }
295 }
296 let out = self.commit_current();
298 self.cur = Syllable {
299 jong: Some(cp),
300 ..Syllable::default()
301 };
302 out
303 }
304
305 fn feed_toggle(&mut self) -> String {
306 if let Some(cho) = self.cur.cho {
308 if let Some(r) = self.layout.combine(Category::Cho, cho, unit::TOGGLE) {
309 self.cur.cho = Some(r);
310 }
311 }
312 String::new()
313 }
314
315 fn feed_jamo(&mut self, j: Jamo) -> String {
316 match j.category {
317 Category::Cho => self.feed_cho(j.cp),
318 Category::Jung => self.feed_jung(j.cp),
319 Category::Jong => self.feed_jong(j.cp),
320 }
321 }
322
323 fn feed_unit(&mut self, u: Unit) -> String {
324 if !self.layout.automata.is_empty() {
327 let jamo = match u {
328 Unit::Jamo(j) => Some(j),
329 Unit::Virtual(id) => self.layout.virtual_units.get(&id).copied(),
330 Unit::Toggle => None,
331 };
332 if let Some(j) = jamo {
333 if ngs_seq(j.category, j.cp).is_some() {
335 return self.automaton_feed(j);
336 }
337 }
338 }
339 let out = match u {
341 Unit::Jamo(j) => self.feed_jamo(j),
342 Unit::Toggle => self.feed_toggle(),
343 Unit::Virtual(id) => match self.layout.virtual_units.get(&id).copied() {
344 Some(j) => self.feed_jamo(j),
345 None => String::new(),
346 },
347 };
348 if out.is_empty() {
351 self.history.push(u);
352 } else if self.cur.is_empty() {
353 self.history.clear();
354 } else {
355 self.history = vec![u];
356 }
357 out
358 }
359
360 fn slot_seq(&self, cat: Category) -> i64 {
364 let cp = match cat {
365 Category::Cho => self.cur.cho,
366 Category::Jung => self.cur.jung,
367 Category::Jong => self.cur.jong,
368 };
369 cp.and_then(|c| ngs_seq(cat, c))
370 .map(|s| s as i64)
371 .unwrap_or(0)
372 }
373
374 fn put_modify(&mut self, j: Jamo) -> bool {
378 let existing = match j.category {
379 Category::Cho => self.cur.cho,
380 Category::Jung => self.cur.jung,
381 Category::Jong => self.cur.jong,
382 };
383 let (newcp, replaced) = match existing {
385 None => (j.cp, false),
386 Some(e) => match self.layout.combine(j.category, e, j.cp) {
387 Some(r) => (r, false),
388 None => (j.cp, true),
389 },
390 };
391 match j.category {
392 Category::Cho => self.cur.cho = Some(newcp),
393 Category::Jung => self.cur.jung = Some(newcp),
394 Category::Jong => self.cur.jong = Some(newcp),
395 }
396 if !self.replaying {
399 self.last_overwrite = match existing {
400 Some(e) if replaced => Some((j.category, e, j.cp)),
401 _ => None,
402 };
403 }
404 replaced
405 }
406
407 fn unit_cat(layout: &Layout, u: &Unit) -> Option<Category> {
409 match u {
410 Unit::Jamo(j) => Some(j.category),
411 Unit::Virtual(id) => layout.virtual_units.get(id).map(|j| j.category),
412 Unit::Toggle => Some(Category::Cho),
413 }
414 }
415
416 fn record_unit(&mut self, u: Unit, replaced: bool, cat: Category) {
420 if replaced {
421 let pos = {
422 let layout = &self.layout;
423 self.history
424 .iter()
425 .rposition(|h| Self::unit_cat(layout, h) == Some(cat))
426 };
427 if let Some(p) = pos {
428 self.history.remove(p);
429 }
430 }
431 self.history.push(u);
432 }
433
434 fn fresh_state(&self, j: Jamo) -> i64 {
436 let seq = ngs_seq(j.category, j.cp).map(|s| s as i64).unwrap_or(0);
437 let (a, b, c) = match j.category {
438 Category::Cho => (seq, 0, 0),
439 Category::Jung => (0, seq, 0),
440 Category::Jong => (0, 0, seq),
441 };
442 let ctx = Ctx {
443 a,
444 b,
445 c,
446 ..Default::default()
447 };
448 match self.layout.automata.get(&self.layout.automata_start) {
449 Some(st) => match st.value.eval(&ctx) {
450 Ok(Value::Int(n)) if n > 0 => n,
451 _ => self.layout.automata_start,
452 },
453 None => self.layout.automata_start,
454 }
455 }
456
457 fn automaton_feed(&mut self, j: Jamo) -> String {
459 let seq = ngs_seq(j.category, j.cp).map(|s| s as i64).unwrap_or(0);
460 let (a, b, c) = match j.category {
461 Category::Cho => (seq, 0, 0),
462 Category::Jung => (0, seq, 0),
463 Category::Jong => (0, 0, seq),
464 };
465 let ctx = Ctx {
466 a,
467 b,
468 c,
469 d: self.slot_seq(Category::Cho),
470 e: self.slot_seq(Category::Jung),
471 f: self.slot_seq(Category::Jong),
472 ..Default::default() };
474 let r = match self.layout.automata.get(&self.auto_state) {
476 Some(st) => match st.value.eval(&ctx) {
477 Ok(Value::Int(n)) => n,
478 _ => match st.default.eval(&ctx) {
479 Ok(Value::Int(n)) => n,
480 _ => return self.feed_jamo_tracked(j),
481 },
482 },
483 None => return self.feed_jamo_tracked(j),
484 };
485 self.apply_result(r, j)
486 }
487
488 fn apply_result(&mut self, r: i64, j: Jamo) -> String {
492 match r {
493 n if n > 0 => {
495 let replaced = self.put_modify(j);
496 self.auto_state = n;
497 self.record_unit(Unit::Jamo(j), replaced, j.category);
498 String::new()
499 }
500 -2 => {
502 let replaced = self.put_modify(j);
503 self.record_unit(Unit::Jamo(j), replaced, j.category);
504 String::new()
505 }
506 -1 => String::new(),
508 _ => {
510 let commit = self.commit_current();
511 self.history.clear();
512 self.put_modify(j);
513 self.auto_state = self.fresh_state(j);
514 self.history.push(Unit::Jamo(j));
515 commit
516 }
517 }
518 }
519
520 fn feed_jamo_tracked(&mut self, j: Jamo) -> String {
522 let out = self.feed_jamo(j);
523 if out.is_empty() {
524 self.history.push(Unit::Jamo(j));
525 } else if self.cur.is_empty() {
526 self.history.clear();
527 } else {
528 self.history = vec![Unit::Jamo(j)];
529 }
530 out
531 }
532
533 pub fn press(&mut self, ascii: u8, caps: bool) -> KeyOutcome {
538 self.bksp_streak = None;
540 let expr = match self.layout.keys.get(&(ascii as u32)) {
541 Some(e) => e.clone(),
542 None => {
543 let mut commit = self.commit_and_clear();
545 if commit.is_empty() {
546 return KeyOutcome {
548 commit,
549 preedit: String::new(),
550 consumed: false,
551 delete_before: 0,
552 };
553 }
554 if let Some(ch) = char::from_u32(ascii as u32) {
559 commit.push(ch);
560 }
561 return KeyOutcome {
562 commit,
563 preedit: String::new(),
564 consumed: true,
565 delete_before: 0,
566 };
567 }
568 };
569 let ctx = Ctx {
570 t: self.t_state(),
571 p: caps as i64,
572 ..Default::default()
573 };
574 let val = match expr.eval(&ctx) {
575 Ok(v) => v,
576 Err(_) => {
577 let commit = self.commit_and_clear();
578 return KeyOutcome {
579 commit,
580 preedit: String::new(),
581 consumed: false,
582 delete_before: 0,
583 };
584 }
585 };
586 self.dispatch(val)
587 }
588
589 fn dispatch(&mut self, val: Value) -> KeyOutcome {
590 match val {
591 Value::Unit(u) => {
592 let commit = self.feed_unit(u);
593 KeyOutcome {
594 commit,
595 preedit: self.preedit(),
596 consumed: true,
597 delete_before: 0,
598 }
599 }
600 Value::Int(n) => {
601 let mut commit = self.commit_and_clear();
603 if let Some(ch) = u32::try_from(n).ok().and_then(char::from_u32) {
604 commit.push(ch);
605 }
606 KeyOutcome {
607 commit,
608 preedit: String::new(),
609 consumed: true,
610 delete_before: 0,
611 }
612 }
613 Value::Command(cmd) => self.dispatch_command(cmd),
614 }
615 }
616
617 pub fn backspace(&mut self) -> KeyOutcome {
621 self.last_overwrite = None;
624 if self.cur.is_empty() {
625 if self.layout.bksp.attach {
629 if let Some(hist) = self.prev_syllables.pop() {
630 if !hist.is_empty() {
631 self.history.clear();
633 self.cur = Syllable::default();
634 self.auto_state = self.layout.automata_start;
635 for u in hist {
636 let _ = self.feed_unit(u);
637 }
638 let unit = self.layout.bksp.composing;
640 self.bksp_streak = Some(unit);
641 self.bksp_remove(unit);
642 let hist2 = std::mem::take(&mut self.history);
643 self.cur = Syllable::default();
644 self.auto_state = self.layout.automata_start;
645 for u in hist2 {
646 let _ = self.feed_unit(u);
647 }
648 return KeyOutcome {
649 commit: String::new(),
650 preedit: self.preedit(),
651 consumed: true,
652 delete_before: 1, };
654 }
655 }
656 }
657 self.bksp_streak = None;
659 return KeyOutcome {
660 commit: String::new(),
661 preedit: String::new(),
662 consumed: false,
663 delete_before: 0,
664 };
665 }
666 let unit = match self.bksp_streak {
669 Some(u) => u,
670 None => {
671 let u = self.layout.bksp.composing;
672 self.bksp_streak = Some(u);
673 u
674 }
675 };
676 self.bksp_remove(unit);
677 self.replay_history();
678 KeyOutcome {
679 commit: String::new(),
680 preedit: self.preedit(),
681 consumed: true,
682 delete_before: 0,
683 }
684 }
685
686 fn bksp_remove(&mut self, mode: BkspUnit) {
689 match mode {
690 BkspUnit::Syllable => self.history.clear(),
692 BkspUnit::LastKey => {
694 self.history.pop();
695 }
696 BkspUnit::LowestLastKey | BkspUnit::LowestWhole => {
698 let cat = if self.cur.jong.is_some() {
699 Category::Jong
700 } else if self.cur.jung.is_some() {
701 Category::Jung
702 } else if self.cur.cho.is_some() {
703 Category::Cho
704 } else {
705 self.history.pop();
706 return;
707 };
708 let layout = &self.layout;
709 if mode == BkspUnit::LowestWhole {
710 self.history
712 .retain(|u| Self::unit_cat(layout, u) != Some(cat));
713 } else {
714 if let Some(p) = self
716 .history
717 .iter()
718 .rposition(|u| Self::unit_cat(layout, u) == Some(cat))
719 {
720 self.history.remove(p);
721 }
722 }
723 }
724 }
725 }
726
727 fn replay_history(&mut self) {
741 let hist = std::mem::take(&mut self.history);
742 self.cur = Syllable::default();
743 self.auto_state = self.layout.automata_start;
744 self.replaying = true;
745 for u in hist {
746 let _ = self.feed_unit(u);
747 }
748 self.replaying = false;
749 }
750
751 fn composing_outcome(&self) -> KeyOutcome {
753 KeyOutcome {
754 commit: String::new(),
755 preedit: self.preedit(),
756 consumed: true,
757 delete_before: 0,
758 }
759 }
760
761 fn delete_cats_and_replay(&mut self, remove: &[Category]) {
764 let layout = &self.layout;
765 self.history.retain(|u| match Self::unit_cat(layout, u) {
766 Some(c) => !remove.contains(&c),
767 None => true,
768 });
769 self.replay_history();
770 }
771
772 fn bksp_cmd(&mut self, unit: BkspUnit) -> KeyOutcome {
774 self.bksp_remove(unit);
775 self.replay_history();
776 self.composing_outcome()
777 }
778
779 fn cmd_move_component_back(&mut self, cat: Category) -> KeyOutcome {
783 let has = match cat {
784 Category::Cho => self.cur.cho.is_some(),
785 Category::Jung => self.cur.jung.is_some(),
786 Category::Jong => self.cur.jong.is_some(),
787 };
788 if !has {
789 return self.composing_outcome();
790 }
791 let (keep, moved) = {
793 let layout = &self.layout;
794 let mut keep = Vec::new();
795 let mut moved = Vec::new();
796 for u in &self.history {
797 if Self::unit_cat(layout, u) == Some(cat) {
798 moved.push(*u);
799 } else {
800 keep.push(*u);
801 }
802 }
803 (keep, moved)
804 };
805 self.history = keep;
806 self.replay_history();
807 let commit = self.commit_current();
808 self.commit_then_start_with(commit, moved)
811 }
812
813 fn commit_then_start_with(&mut self, commit: String, moved: Vec<Unit>) -> KeyOutcome {
816 self.history.clear();
817 self.auto_state = self.layout.automata_start;
818 for u in moved {
819 let _ = self.feed_unit(u);
820 }
821 KeyOutcome {
822 commit,
823 preedit: self.preedit(),
824 consumed: true,
825 delete_before: 0,
826 }
827 }
828
829 fn cmd_swap_cho_jong(&mut self) -> KeyOutcome {
832 let cho = self.cur.cho;
833 let jong = self.cur.jong;
834 let jung = self.cur.jung;
835 if cho.is_none() && jong.is_none() {
836 return self.composing_outcome();
837 }
838 let new_cho = jong.and_then(hanmo::jong_to_cho);
839 let new_jong = cho.and_then(hanmo::cho_to_jong);
840 if (jong.is_some() && new_cho.is_none()) || (cho.is_some() && new_jong.is_none()) {
842 return self.composing_outcome();
843 }
844 self.history.clear();
845 self.cur = Syllable::default();
846 self.auto_state = self.layout.automata_start;
847 if let Some(c) = new_cho {
848 let _ = self.feed_unit(Unit::Jamo(Jamo::new(Category::Cho, c)));
849 }
850 if let Some(v) = jung {
851 let _ = self.feed_unit(Unit::Jamo(Jamo::new(Category::Jung, v)));
852 }
853 if let Some(j) = new_jong {
854 let _ = self.feed_unit(Unit::Jamo(Jamo::new(Category::Jong, j)));
855 }
856 self.composing_outcome()
857 }
858
859 fn cmd_dokkaebi(&mut self) -> KeyOutcome {
862 if self.cur.jong.is_none() {
863 return self.composing_outcome();
864 }
865 let pos = {
866 let layout = &self.layout;
867 self.history
868 .iter()
869 .rposition(|u| Self::unit_cat(layout, u) == Some(Category::Jong))
870 };
871 let Some(pos) = pos else {
872 return self.composing_outcome();
873 };
874 let moved = self.history.remove(pos);
875 self.replay_history();
876 let commit = self.commit_current();
877 let moved = match moved {
879 Unit::Jamo(j) => hanmo::jong_to_cho(j.cp)
880 .map(|c| Unit::Jamo(Jamo::new(Category::Cho, c)))
881 .unwrap_or(Unit::Jamo(j)),
882 other => other,
883 };
884 self.commit_then_start_with(commit, vec![moved])
885 }
886
887 fn cmd_split_last_key(&mut self) -> KeyOutcome {
890 let Some(moved) = self.history.pop() else {
891 return self.composing_outcome();
892 };
893 self.replay_history();
894 let commit = self.commit_current();
895 self.commit_then_start_with(commit, vec![moved])
896 }
897
898 fn cmd_swap_last_two_keys(&mut self) -> KeyOutcome {
901 let n = self.history.len();
902 if n < 2 {
903 return self.composing_outcome();
904 }
905 self.history.swap(n - 1, n - 2);
906 self.replay_history();
907 self.composing_outcome()
908 }
909
910 fn restore_overwrite_in_history(&mut self) -> Option<(Category, u32)> {
913 let (cat, old, new) = *self.last_overwrite.as_ref()?;
917 let pos = self
918 .history
919 .iter()
920 .rposition(|u| matches!(u, Unit::Jamo(j) if j.category == cat && j.cp == new))?;
921 self.history[pos] = Unit::Jamo(Jamo::new(cat, old));
922 self.last_overwrite = None;
923 Some((cat, new))
924 }
925
926 fn cmd_infinite_restore(&mut self) -> KeyOutcome {
929 if self.restore_overwrite_in_history().is_some() {
930 self.replay_history();
931 }
932 self.composing_outcome()
933 }
934
935 fn cmd_infinite_split(&mut self) -> KeyOutcome {
938 let Some((cat, new)) = self.restore_overwrite_in_history() else {
939 return self.composing_outcome();
940 };
941 self.replay_history();
942 let commit = self.commit_current();
943 self.commit_then_start_with(commit, vec![Unit::Jamo(Jamo::new(cat, new))])
944 }
945
946 fn cmd_backspace(&mut self, always_consume: bool) -> KeyOutcome {
949 let mut out = self.backspace();
950 if always_consume {
951 out.consumed = true;
952 }
953 out
954 }
955
956 fn dubeol_cho_to_jong(units: Vec<Unit>) -> Vec<Unit> {
959 units
960 .into_iter()
961 .map(|u| match u {
962 Unit::Jamo(j) if j.category == Category::Cho => hanmo::cho_to_jong(j.cp)
963 .map(|c| Unit::Jamo(Jamo::new(Category::Jong, c)))
964 .unwrap_or(u),
965 other => other,
966 })
967 .collect()
968 }
969
970 fn recombine_forward(&mut self, dubeol: bool, first_cat: Option<Category>) -> KeyOutcome {
976 let cho_only = self.cur.cho.is_some() && self.cur.jung.is_none() && self.cur.jong.is_none();
977 if !self.surrounding_ok || self.prev_syllables.is_empty() || self.cur.is_empty() {
978 return self.composing_outcome();
979 }
980 let prev = self.prev_syllables.pop().unwrap();
981 let mut cur_units = std::mem::take(&mut self.history);
982 if let Some(cat) = first_cat {
983 let layout = &self.layout;
984 let (mut first, rest): (Vec<Unit>, Vec<Unit>) = cur_units
985 .into_iter()
986 .partition(|u| Self::unit_cat(layout, u) == Some(cat));
987 first.extend(rest);
988 cur_units = first;
989 }
990 if dubeol && cho_only {
991 cur_units = Self::dubeol_cho_to_jong(cur_units);
992 }
993 self.cur = Syllable::default();
994 self.history.clear();
995 self.auto_state = self.layout.automata_start;
996 self.replaying = true;
997 for u in prev {
998 let _ = self.feed_unit(u);
999 }
1000 for u in cur_units {
1001 let _ = self.feed_unit(u);
1002 }
1003 self.replaying = false;
1004 KeyOutcome {
1005 commit: String::new(),
1006 preedit: self.preedit(),
1007 consumed: true,
1008 delete_before: 1,
1009 }
1010 }
1011
1012 fn dispatch_command(&mut self, cmd: u32) -> KeyOutcome {
1014 use Category::{Cho, Jong, Jung};
1015 if cmd != 0x13 && cmd != 0x14 {
1019 self.last_overwrite = None;
1020 }
1021 match cmd {
1022 0x2 => {
1024 self.delete_cats_and_replay(&[Cho]);
1025 self.composing_outcome()
1026 }
1027 0x3 => {
1028 self.delete_cats_and_replay(&[Jung]);
1029 self.composing_outcome()
1030 }
1031 0x4 => {
1032 self.delete_cats_and_replay(&[Jong]);
1033 self.composing_outcome()
1034 }
1035 0x8 => self.cmd_move_component_back(Cho),
1037 0x9 => self.cmd_move_component_back(Jung),
1038 0xA => self.cmd_move_component_back(Jong),
1039 0x15 => {
1041 self.delete_cats_and_replay(&[Jung, Jong]);
1042 self.composing_outcome()
1043 }
1044 0x16 => {
1045 self.delete_cats_and_replay(&[Cho, Jong]);
1046 self.composing_outcome()
1047 }
1048 0x17 => {
1049 self.delete_cats_and_replay(&[Cho, Jung]);
1050 self.composing_outcome()
1051 }
1052 0x19 => self.bksp_cmd(BkspUnit::LastKey),
1054 0x1A => self.bksp_cmd(BkspUnit::LowestLastKey),
1055 0x1B => self.bksp_cmd(BkspUnit::LowestWhole),
1056 0x1C => self.bksp_cmd(BkspUnit::Syllable),
1057 0x12 => self.cmd_dokkaebi(),
1060 0x13 => self.cmd_infinite_split(),
1061 0x14 => self.cmd_infinite_restore(),
1062 0x18 => self.cmd_swap_cho_jong(),
1063 0x21 => self.cmd_split_last_key(),
1064 0x23 => self.cmd_swap_last_two_keys(),
1065 0x5 => self.recombine_forward(false, Some(Cho)),
1067 0x6 => self.recombine_forward(false, Some(Jung)),
1068 0x7 => self.recombine_forward(false, Some(Jong)),
1069 0x1D => self.recombine_forward(false, None),
1071 0x1E => self.recombine_forward(true, None),
1072 0x88 | 0x89 => self.cmd_backspace(false),
1074 0x8A | 0x8B => self.cmd_backspace(true),
1075 _ => {
1079 let commit = self.commit_and_clear();
1080 KeyOutcome {
1081 commit,
1082 preedit: self.preedit(),
1083 consumed: true,
1084 delete_before: 0,
1085 }
1086 }
1087 }
1088 }
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use super::*;
1094 use crate::config::Config;
1095
1096 const MINI: &str = r#"<?xml version="1.0" encoding="utf-8"?>
1098<EditContextSetting version="0x500">
1099 <EditorLayer flag="0">
1100 <FinalConvTable>
1101 <FinalConv from="0x1100" to="0x3131"/>
1102 <FinalConv from="0x1102" to="0x3134"/>
1103 <FinalConv from="0x1161" to="0x314F"/>
1104 <FinalConv from="0x11A8" to="0x3131"/>
1105 </FinalConvTable>
1106 </EditorLayer>
1107 <InputLayer default="0" current="0">
1108 <InputEntry>
1109 <InputSchemeSetting object="CBasicInputScheme">
1110 <KeyTable name="mini" flag="0" from="33" to="126">
1111 <Key at="0x6B" value="H3|G_"/> <!-- k = 초성 ㄱ -->
1112 <Key at="0x68" value="H3|N_"/> <!-- h = 초성 ㄴ -->
1113 <Key at="0x66" value="H3|A_"/> <!-- f = 중성 ㅏ -->
1114 <Key at="0x2F" value="H3|O_"/> <!-- / = 중성 ㅗ -->
1115 <Key at="0x78" value="H3|_G"/> <!-- x = 종성 ㄱ -->
1116 <Key at="0x73" value="H3|_N"/> <!-- s = 종성 ㄴ -->
1117 <Key at="0x24" value="T ? H3|0x1F4 : 0x24"/> <!-- $ = 갈마 토글 -->
1118 <Key at="0x21" value="0x21"/> <!-- ! = 리터럴 '!' -->
1119 </KeyTable>
1120 </InputSchemeSetting>
1121 <GeneratorSetting object="CNgsImeEx">
1122 <UnitMixTable>
1123 <UnitMix unit="CHO" a="G_" b="500" to="GG"/>
1124 <UnitMix unit="CHO" a="GG" b="500" to="G_"/>
1125 <UnitMix unit="JUNG" a="O_" b="A_" to="WA"/>
1126 </UnitMixTable>
1127 <VirtualUnitTable/>
1128 <AutomataTable default="0"/>
1129 </GeneratorSetting>
1130 </InputEntry>
1131 </InputLayer>
1132</EditContextSetting>"#;
1133
1134 fn engine() -> Engine {
1135 let cfg = Config::parse(MINI).unwrap();
1136 Engine::new(cfg.compile(0).unwrap())
1137 }
1138
1139 fn typ(e: &mut Engine, keys: &str) -> (String, String) {
1141 let mut committed = String::new();
1142 let mut preedit = String::new();
1143 for ch in keys.chars() {
1144 let out = e.press(ch as u8, false);
1145 committed.push_str(&out.commit);
1146 preedit = out.preedit;
1147 }
1148 (committed, preedit)
1149 }
1150
1151 #[test]
1152 fn simple_syllable() {
1153 let mut e = engine();
1154 let (c, p) = typ(&mut e, "kf"); assert_eq!(c, "");
1156 assert_eq!(p, "가");
1157 assert_eq!(e.flush(), "가");
1158 }
1159
1160 #[test]
1161 fn syllable_with_jong() {
1162 let mut e = engine();
1163 let (_c, p) = typ(&mut e, "kfx"); assert_eq!(p, "각");
1165 }
1166
1167 #[test]
1168 fn new_cho_commits_previous() {
1169 let mut e = engine();
1170 let (c, p) = typ(&mut e, "kfhf");
1172 assert_eq!(c, "가");
1173 assert_eq!(p, "나");
1174 }
1175
1176 #[test]
1177 fn compound_vowel() {
1178 let mut e = engine();
1179 let (_c, p) = typ(&mut e, "k/f"); assert_eq!(p, "과");
1181 }
1182
1183 #[test]
1184 fn galma_toggle_tense() {
1185 let mut e = engine();
1186 let (_c, p) = typ(&mut e, "k$f"); assert_eq!(p, "까");
1188 let mut e2 = engine();
1190 let (_c2, p2) = typ(&mut e2, "k$$f"); assert_eq!(p2, "가");
1192 }
1193
1194 #[test]
1195 fn lone_jamo_finalconv() {
1196 let mut e = engine();
1197 let (_c, p) = typ(&mut e, "k"); assert_eq!(p, "ㄱ");
1199 assert_eq!(e.flush(), "ㄱ");
1200 }
1201
1202 #[test]
1203 fn literal_commits_and_breaks() {
1204 let mut e = engine();
1205 e.press(b'k', false); let out = e.press(b'!', false); assert_eq!(out.commit, "ㄱ!");
1208 assert_eq!(out.preedit, "");
1209 assert!(out.consumed);
1210 }
1211
1212 #[test]
1213 fn backspace_unit_step() {
1214 let mut e = engine();
1215 typ(&mut e, "kfx"); let o1 = e.backspace(); assert_eq!(o1.preedit, "가");
1218 let o2 = e.backspace(); assert_eq!(o2.preedit, "ㄱ");
1220 let o3 = e.backspace(); assert_eq!(o3.preedit, "");
1222 let o4 = e.backspace(); assert!(!o4.consumed);
1224 }
1225
1226 #[test]
1227 fn backspace_decomposes_compound() {
1228 let mut e = engine();
1229 typ(&mut e, "k$"); assert_eq!(e.preedit(), "ㄲ");
1231 let o = e.backspace(); assert_eq!(o.preedit, "ㄱ");
1233 }
1234
1235 const OLD: &str = r#"<?xml version="1.0" encoding="utf-8"?>
1238<EditContextSetting version="0x500">
1239 <EditorLayer flag="0">
1240 <FinalConvTable>
1241 <FinalConv from="0x114C" to="0x3181"/>
1242 <FinalConv from="0x1161" to="0x314F"/>
1243 </FinalConvTable>
1244 </EditorLayer>
1245 <InputLayer default="0" current="0">
1246 <InputEntry>
1247 <InputSchemeSetting object="CBasicInputScheme">
1248 <KeyTable name="old" flag="0" from="33" to="126">
1249 <Key at="0x61" value="H3|0x114C"/> <!-- a = 옛이응 초성 (현대 밖) -->
1250 <Key at="0x62" value="H3|A_"/> <!-- b = 중성 ㅏ -->
1251 </KeyTable>
1252 </InputSchemeSetting>
1253 <GeneratorSetting object="CNgsImeEx">
1254 <UnitMixTable/><VirtualUnitTable/><AutomataTable default="0"/>
1255 </GeneratorSetting>
1256 </InputEntry>
1257 </InputLayer>
1258</EditContextSetting>"#;
1259
1260 fn old_engine() -> Engine {
1261 let cfg = Config::parse(OLD).unwrap();
1262 Engine::new(cfg.compile(0).unwrap())
1263 }
1264
1265 #[test]
1266 fn old_hangul_lone_jamo_via_finalconv() {
1267 let mut e = old_engine();
1268 let (_c, p) = typ(&mut e, "a"); assert_eq!(p, "\u{3181}");
1270 }
1271
1272 #[test]
1273 fn old_hangul_syllable_emits_conjoining() {
1274 let mut e = old_engine();
1275 let (_c, p) = typ(&mut e, "ab"); assert_eq!(p, "\u{114C}\u{1161}");
1277 assert_eq!(p.chars().count(), 2);
1278 }
1279
1280 #[test]
1281 fn space_not_in_table_commits_and_consumes() {
1282 let mut e = engine();
1283 typ(&mut e, "kf"); let out = e.press(b' ', false); assert_eq!(out.commit, "가 ");
1286 assert!(out.consumed); }
1288
1289 const AUTO: &str = r#"<?xml version="1.0" encoding="utf-8"?>
1292<EditContextSetting version="0x500">
1293 <EditorLayer flag="0"><FinalConvTable/></EditorLayer>
1294 <InputLayer default="0" current="0">
1295 <InputEntry>
1296 <InputSchemeSetting object="CBasicInputScheme">
1297 <KeyTable name="auto" flag="0" from="33" to="126">
1298 <Key at="0x67" value="H3|G_"/> <!-- g 초 ㄱ -->
1299 <Key at="0x6E" value="H3|N_"/> <!-- n 초 ㄴ -->
1300 <Key at="0x63" value="H3|S_"/> <!-- c 초 ㅅ -->
1301 <Key at="0x6B" value="H3|K_"/> <!-- k 초 ㅋ -->
1302 <Key at="0x68" value="H3|H_"/> <!-- h 초 ㅎ -->
1303 <Key at="0x61" value="H3|A_"/> <!-- a 중 ㅏ -->
1304 <Key at="0x65" value="H3|EO"/> <!-- e 중 ㅓ -->
1305 <Key at="0x6F" value="H3|O_"/> <!-- o 중 ㅗ -->
1306 <Key at="0x6D" value="H3|_N"/> <!-- m 종 ㄴ -->
1307 <Key at="0x69" value="H3|AE"/> <!-- i 중 ㅐ -->
1308 <!-- C0 특수글쇠(테스트용): 대문자 자리에 배당 -->
1309 <Key at="0x41" value="C0|0x2"/> <!-- A 초성 삭제 -->
1310 <Key at="0x42" value="C0|0x3"/> <!-- B 중성 삭제 -->
1311 <Key at="0x43" value="C0|0x4"/> <!-- C 종성 삭제 -->
1312 <Key at="0x44" value="C0|0xA"/> <!-- D 종성 뒤로 빼기 -->
1313 <Key at="0x45" value="C0|0x8"/> <!-- E 초성 뒤로 빼기 -->
1314 <Key at="0x46" value="C0|0x9"/> <!-- F 중성 뒤로 빼기 -->
1315 <Key at="0x47" value="C0|0x15"/> <!-- G 초성만 남기기 -->
1316 <Key at="0x48" value="C0|0x16"/> <!-- H 중성만 남기기 -->
1317 <Key at="0x49" value="C0|0x17"/> <!-- I 종성만 남기기 -->
1318 <Key at="0x4A" value="C0|0x19"/> <!-- J 마지막 한 타 -->
1319 <Key at="0x4B" value="C0|0x1A"/> <!-- K 최하위 직전 한 타 -->
1320 <Key at="0x4C" value="C0|0x1B"/> <!-- L 최하위 낱자 전체 -->
1321 <Key at="0x4D" value="C0|0x1C"/> <!-- M 글자 전체 -->
1322 <Key at="0x4E" value="C0|0x18"/> <!-- N 초·종성 맞바꾸기 -->
1323 <Key at="0x4F" value="C0|0x12"/> <!-- O 도깨비불 -->
1324 <Key at="0x50" value="C0|0x13"/> <!-- P 무한낱자수정 분리 -->
1325 <Key at="0x51" value="C0|0x14"/> <!-- Q 무한낱자수정 복원 -->
1326 <Key at="0x52" value="C0|0x21"/> <!-- R 마지막 한 타 분리 -->
1327 <Key at="0x53" value="C0|0x23"/> <!-- S 직전 두 글쇠 교환 -->
1328 <Key at="0x54" value="C0|0x1D"/> <!-- T 낱자 재결합(앞으로) -->
1329 <Key at="0x55" value="C0|0x1E"/> <!-- U 낱자 재결합(앞으로, 두벌식) -->
1330 <Key at="0x56" value="C0|0x5"/> <!-- V 초성 앞으로 이동 -->
1331 <Key at="0x57" value="C0|0x88"/> <!-- W Backspace 1 -->
1332 <Key at="0x58" value="C0|0x8A"/> <!-- X Backspace 3(항상 가로챔) -->
1333 </KeyTable>
1334 </InputSchemeSetting>
1335 <GeneratorSetting object="CNgsImeEx">
1336 <UnitMixTable>
1337 <UnitMix unit="JUNG" a="O_" b="A_" to="WA"/>
1338 </UnitMixTable>
1339 <VirtualUnitTable/>
1340 <AutomataTable default="0">
1341 <Automata state="0" value="1" default="0" remark="초기"/>
1342 <Automata state="1" value="D==176&&A==176 || D==185&&A==185 ? 0 : A || B || C ? (A || D)&&(B || E) ? 2 : 1 : -2" default="-1" remark="미완성"/>
1343 <Automata state="2" value="A&&A!=500 ? 0 : B||C||A==500 ? 2 : -2" default="0" remark="완성"/>
1344 </AutomataTable>
1345 </GeneratorSetting>
1346 </InputEntry>
1347 </InputLayer>
1348</EditContextSetting>"#;
1349
1350 fn auto_engine() -> Engine {
1351 let cfg = Config::parse(AUTO).unwrap();
1352 Engine::new(cfg.compile(0).unwrap())
1353 }
1354
1355 #[test]
1356 fn automaton_loads() {
1357 let e = auto_engine();
1358 assert_eq!(e.layout.automata.len(), 3);
1359 assert_eq!(e.auto_state, 0);
1360 }
1361
1362 #[test]
1363 fn infinite_jamo_edit_replaces_jung() {
1364 let mut e = auto_engine();
1366 let (c, p) = typ(&mut e, "cam"); assert_eq!(c, "");
1368 assert_eq!(p, "산");
1369 let out = e.press(b'e', false); assert_eq!(out.commit, ""); assert_eq!(out.preedit, "선"); }
1373
1374 #[test]
1375 fn infinite_jamo_edit_jong() {
1376 let mut e = auto_engine();
1379 typ(&mut e, "gam"); assert_eq!(e.preedit(), "간");
1381 let out = e.press(b'o', false); assert_eq!(out.preedit, "곤");
1383 }
1384
1385 #[test]
1386 fn kk_breaks_to_next_syllable() {
1387 let mut e = auto_engine();
1389 let o1 = e.press(b'k', false); assert_eq!(o1.preedit, "ㅋ");
1391 let o2 = e.press(b'k', false); assert_eq!(o2.commit, "ㅋ");
1393 assert_eq!(o2.preedit, "ㅋ");
1394 }
1395
1396 #[test]
1397 fn automaton_compound_vowel() {
1398 let mut e = auto_engine();
1400 let (_c, p) = typ(&mut e, "goa");
1401 assert_eq!(p, "과");
1402 }
1403
1404 #[test]
1405 fn automaton_new_cho_commits() {
1406 let mut e = auto_engine();
1408 typ(&mut e, "ga"); let out = e.press(b'n', false); assert_eq!(out.commit, "가");
1411 assert_eq!(out.preedit, "ㄴ");
1412 }
1413
1414 #[test]
1415 fn backspace_after_infinite_edit() {
1416 let mut e = auto_engine();
1419 typ(&mut e, "ga"); let o1 = e.press(b'i', false); assert_eq!(o1.preedit, "개");
1422 let o2 = e.backspace(); assert_eq!(o2.preedit, "ㄱ");
1424 }
1425
1426 #[test]
1427 fn backspace_after_compound_keeps_steps() {
1428 let mut e = auto_engine();
1430 typ(&mut e, "goa"); assert_eq!(e.preedit(), "과");
1432 let o = e.backspace();
1433 assert_eq!(o.preedit, "고");
1434 }
1435
1436 fn bksp_engine(value1: &str) -> Engine {
1438 let xml = format!(
1439 r#"<?xml version="1.0" encoding="utf-8"?>
1440<EditContextSetting version="0x500">
1441 <EditorLayer flag="0"><FinalConvTable/></EditorLayer>
1442 <InputLayer default="0" current="0">
1443 <InputEntry>
1444 <InputSchemeSetting object="CBasicInputScheme">
1445 <KeyTable name="b" flag="0" from="33" to="126">
1446 <Key at="0x67" value="H3|G_"/><Key at="0x61" value="H3|A_"/>
1447 <Key at="0x6D" value="H3|_N"/><Key at="0x73" value="H3|_S"/>
1448 </KeyTable>
1449 </InputSchemeSetting>
1450 <GeneratorSetting object="CNgsImeEx">
1451 <UnitMixTable><UnitMix unit="JONG" a="_N" b="_S" to="_NJ"/></UnitMixTable>
1452 <VirtualUnitTable/>
1453 <AutomataTable default="0">
1454 <Automata state="0" value="1" default="0"/>
1455 <Automata state="1" value="A||B||C ? (A||D)&&(B||E) ? 2 : 1 : -2" default="-1"/>
1456 <Automata state="2" value="A&&A!=500 ? 0 : B||C||A==500 ? 2 : -2" default="0"/>
1457 </AutomataTable>
1458 <Extra><Bksp key="1" value1="{value1}" value2="BySyllable" condition1="0" condition2="0"/></Extra>
1459 </GeneratorSetting>
1460 </InputEntry>
1461 </InputLayer>
1462</EditContextSetting>"#
1463 );
1464 let cfg = Config::parse(&xml).unwrap();
1465 Engine::new(cfg.compile(0).unwrap())
1466 }
1467
1468 #[test]
1469 fn bksp_mode_lastkey() {
1470 let mut e = bksp_engine("ByUnitStep");
1472 typ(&mut e, "gam"); let o = e.backspace();
1474 assert_eq!(o.preedit, "가");
1475 }
1476
1477 #[test]
1478 fn bksp_mode_syllable() {
1479 let mut e = bksp_engine("BySyllable");
1481 typ(&mut e, "gam"); let o = e.backspace();
1483 assert_eq!(o.preedit, "");
1484 }
1485
1486 #[test]
1487 fn bksp_mode_lowest_whole() {
1488 let mut e = bksp_engine("2"); typ(&mut e, "gams"); assert_eq!(e.preedit(), "갅");
1492 let o = e.backspace();
1493 assert_eq!(o.preedit, "가"); }
1495
1496 #[test]
1497 fn bksp_mode_lowest_lastkey() {
1498 let mut e = bksp_engine("1"); typ(&mut e, "gams"); assert_eq!(e.preedit(), "갅");
1502 let o = e.backspace();
1503 assert_eq!(o.preedit, "간"); }
1505
1506 #[test]
1507 fn bksp_streak_keeps_initial_unit() {
1508 let mut e = bksp_engine("BySyllable");
1512 typ(&mut e, "gam"); let o1 = e.backspace(); assert_eq!(o1.preedit, "");
1515 }
1517
1518 #[test]
1519 fn bksp_streak_broken_by_press() {
1520 let mut e = bksp_engine("ByUnitStep");
1522 typ(&mut e, "ga"); let _ = e.backspace(); assert_eq!(e.preedit(), "ㄱ");
1525 typ(&mut e, "a"); assert_eq!(e.preedit(), "가");
1527 assert!(e.bksp_streak.is_none());
1528 }
1529
1530 fn attach_engine() -> Engine {
1532 let xml = r#"<?xml version="1.0" encoding="utf-8"?>
1533<EditContextSetting version="0x500">
1534 <EditorLayer flag="0"><FinalConvTable/></EditorLayer>
1535 <InputLayer default="0" current="0">
1536 <InputEntry>
1537 <InputSchemeSetting object="CBasicInputScheme">
1538 <KeyTable name="b" flag="0" from="33" to="126">
1539 <Key at="0x67" value="H3|G_"/><Key at="0x61" value="H3|A_"/>
1540 <Key at="0x6E" value="H3|N_"/><Key at="0x6D" value="H3|_N"/>
1541 </KeyTable>
1542 </InputSchemeSetting>
1543 <GeneratorSetting object="CNgsImeEx">
1544 <UnitMixTable/><VirtualUnitTable/>
1545 <AutomataTable default="0">
1546 <Automata state="0" value="1" default="0"/>
1547 <Automata state="1" value="A||B||C ? (A||D)&&(B||E) ? 2 : 1 : -2" default="-1"/>
1548 <Automata state="2" value="A&&A!=500 ? 0 : B||C||A==500 ? 2 : -2" default="0"/>
1549 </AutomataTable>
1550 <Extra><Bksp key="1" value1="ByUnitStep|BkspAttach" value2="BySyllable" condition1="0" condition2="0"/></Extra>
1551 </GeneratorSetting>
1552 </InputEntry>
1553 </InputLayer>
1554</EditContextSetting>"#;
1555 let cfg = Config::parse(xml).unwrap();
1556 Engine::new(cfg.compile(0).unwrap())
1557 }
1558
1559 #[test]
1560 fn bksp_attach_revives_prev_syllable() {
1561 let mut e = attach_engine();
1565 let mut committed = String::new();
1566 for ch in "gan".chars() {
1567 committed.push_str(&e.press(ch as u8, false).commit);
1568 }
1569 assert_eq!(committed, "가"); assert_eq!(e.preedit(), "ㄴ"); let o1 = e.backspace(); assert_eq!(o1.preedit, "");
1573 assert_eq!(o1.delete_before, 0);
1574 let o2 = e.backspace(); assert_eq!(o2.preedit, "ㄱ");
1576 assert_eq!(o2.delete_before, 1);
1577 assert!(o2.consumed);
1578 }
1579
1580 #[test]
1581 fn bksp_no_attach_passes_through() {
1582 let mut e = bksp_engine("ByUnitStep"); let _ = e.press(b'g', false);
1585 let _ = e.backspace(); let o = e.backspace(); assert!(!o.consumed);
1588 assert_eq!(o.delete_before, 0);
1589 }
1590
1591 #[test]
1592 fn bksp_attach_chain_multiple_syllables() {
1593 let mut e = attach_engine();
1597 let mut committed = String::new();
1598 for ch in "ganaga".chars() {
1599 committed.push_str(&e.press(ch as u8, false).commit);
1600 }
1601 assert_eq!(committed, "가나"); assert_eq!(e.preedit(), "가");
1603 let o0 = e.backspace(); assert_eq!(o0.preedit, "ㄱ");
1605 let o1 = e.backspace(); assert_eq!(o1.preedit, "");
1607 let o2 = e.backspace(); assert_eq!((o2.preedit.as_str(), o2.delete_before), ("ㄴ", 1));
1609 let o3 = e.backspace(); assert_eq!(o3.preedit, "");
1611 let o4 = e.backspace(); assert_eq!((o4.preedit.as_str(), o4.delete_before), ("ㄱ", 1));
1613 let o5 = e.backspace(); assert_eq!(o5.preedit, "");
1615 let o6 = e.backspace(); assert!(!o6.consumed);
1617 }
1618
1619 #[test]
1620 fn space_after_syllable_commits_char_and_consumes() {
1621 let mut e = attach_engine();
1625 let mut committed = String::new();
1626 for ch in "gana".chars() {
1627 committed.push_str(&e.press(ch as u8, false).commit);
1628 }
1629 assert_eq!(committed, "가"); assert_eq!(e.preedit(), "나");
1631 let sp = e.press(b' ', false);
1632 assert_eq!(sp.commit, "나 "); assert!(sp.consumed); assert_eq!(sp.preedit, "");
1635 let bk = e.backspace(); assert!(!bk.consumed);
1637 assert_eq!(bk.delete_before, 0);
1638 assert_eq!(bk.preedit, "");
1639 }
1640
1641 #[test]
1642 fn space_with_empty_buffer_passes_through() {
1643 let mut e = attach_engine();
1645 let sp = e.press(b' ', false);
1646 assert_eq!(sp.commit, "");
1647 assert!(!sp.consumed);
1648 }
1649
1650 #[test]
1651 fn reset_clears_attach_history() {
1652 let mut e = attach_engine();
1655 for ch in "gan".chars() {
1656 let _ = e.press(ch as u8, false); }
1658 e.reset();
1659 let bk = e.backspace();
1660 assert!(!bk.consumed);
1661 assert_eq!(bk.preedit, "");
1662 assert_eq!(bk.delete_before, 0);
1663 }
1664
1665 #[test]
1666 fn flush_clears_attach_history() {
1667 let mut e = attach_engine();
1669 for ch in "gan".chars() {
1670 let _ = e.press(ch as u8, false);
1671 }
1672 let _ = e.flush();
1673 let bk = e.backspace();
1674 assert!(!bk.consumed);
1675 assert_eq!(bk.preedit, "");
1676 assert_eq!(bk.delete_before, 0);
1677 }
1678
1679 #[test]
1685 fn c0_delete_cho() {
1686 let mut e = auto_engine();
1688 typ(&mut e, "gam"); let o = e.press(b'A', false); assert_eq!(o.commit, "");
1691 assert!(o.consumed);
1692 assert_eq!(o.preedit, "\u{115F}\u{1161}\u{11AB}");
1694 }
1695
1696 #[test]
1697 fn c0_delete_jung() {
1698 let mut e = auto_engine();
1699 typ(&mut e, "gam"); let o = e.press(b'B', false); assert_eq!(o.preedit, "\u{1100}\u{1160}\u{11AB}");
1703 }
1704
1705 #[test]
1706 fn c0_delete_jong() {
1707 let mut e = auto_engine();
1708 typ(&mut e, "gam"); let o = e.press(b'C', false); assert_eq!(o.preedit, "가");
1711 }
1712
1713 #[test]
1714 fn c0_move_jong_back() {
1715 let mut e = auto_engine();
1717 typ(&mut e, "gam"); let o = e.press(b'D', false); assert_eq!(o.commit, "가");
1720 assert_eq!(o.preedit, "ㄴ");
1721 assert!(o.consumed);
1722 }
1723
1724 #[test]
1725 fn c0_move_jong_back_then_compose() {
1726 let mut e = auto_engine();
1729 typ(&mut e, "gam"); let o = e.press(b'D', false); assert_eq!(o.commit, "가");
1732 assert_eq!(o.preedit, "ㄴ");
1733 let o2 = e.press(b'g', false); assert_eq!(o2.preedit, "\u{1100}\u{1160}\u{11AB}");
1735 let o3 = e.press(b'a', false); assert_eq!(o3.preedit, "간");
1737 }
1738
1739 #[test]
1740 fn c0_move_cho_back() {
1741 let mut e = auto_engine();
1743 typ(&mut e, "gam"); let o = e.press(b'E', false); assert_eq!(o.commit, "\u{115F}\u{1161}\u{11AB}"); assert_eq!(o.preedit, "ㄱ");
1747 }
1748
1749 #[test]
1750 fn c0_move_jung_back() {
1751 let mut e = auto_engine();
1753 typ(&mut e, "gam"); let o = e.press(b'F', false); assert_eq!(o.commit, "\u{1100}\u{1160}\u{11AB}"); assert_eq!(o.preedit, "ㅏ");
1757 }
1758
1759 #[test]
1760 fn c0_keep_only_cho() {
1761 let mut e = auto_engine();
1762 typ(&mut e, "gam"); let o = e.press(b'G', false); assert_eq!(o.preedit, "ㄱ");
1765 }
1766
1767 #[test]
1768 fn c0_keep_only_jung() {
1769 let mut e = auto_engine();
1770 typ(&mut e, "gam"); let o = e.press(b'H', false); assert_eq!(o.preedit, "ㅏ");
1773 }
1774
1775 #[test]
1776 fn c0_keep_only_jong() {
1777 let mut e = auto_engine();
1778 typ(&mut e, "gam"); let o = e.press(b'I', false); assert_eq!(o.preedit, "ㄴ");
1781 }
1782
1783 #[test]
1784 fn c0_bksp_partial_units() {
1785 let mut e = auto_engine();
1787 typ(&mut e, "gam"); assert_eq!(e.press(b'J', false).preedit, "가"); let mut e2 = auto_engine();
1790 typ(&mut e2, "gam");
1791 assert_eq!(e2.press(b'K', false).preedit, "가"); let mut e3 = auto_engine();
1793 typ(&mut e3, "gam");
1794 assert_eq!(e3.press(b'L', false).preedit, "가"); let mut e4 = auto_engine();
1796 typ(&mut e4, "gam");
1797 assert_eq!(e4.press(b'M', false).preedit, ""); }
1799
1800 #[test]
1801 fn c0_delete_then_backspace_consistent() {
1802 let mut e = auto_engine();
1805 typ(&mut e, "gam"); e.press(b'C', false); assert_eq!(e.preedit(), "가");
1808 let o = e.backspace(); assert_eq!(o.preedit, "ㄱ");
1810 }
1811
1812 #[test]
1813 fn c0_on_empty_is_noop_consumed() {
1814 let mut e = auto_engine();
1816 let o = e.press(b'A', false); assert_eq!(o.commit, "");
1818 assert_eq!(o.preedit, "");
1819 assert!(o.consumed);
1820 }
1821
1822 #[test]
1823 fn c0_swap_cho_jong() {
1824 let mut e = auto_engine();
1826 typ(&mut e, "gam"); let o = e.press(b'N', false); assert_eq!(o.preedit, "낙");
1829 }
1830
1831 #[test]
1832 fn c0_dokkaebi_moves_jong_as_cho() {
1833 let mut e = auto_engine();
1835 typ(&mut e, "gam"); let o = e.press(b'O', false); assert_eq!(o.commit, "가");
1838 assert_eq!(o.preedit, "ㄴ");
1839 let o2 = e.press(b'a', false); assert_eq!(o2.preedit, "나");
1841 }
1842
1843 #[test]
1844 fn c0_split_last_key() {
1845 let mut e = auto_engine();
1847 typ(&mut e, "gam"); let o = e.press(b'R', false); assert_eq!(o.commit, "가");
1850 assert_eq!(o.preedit, "ㄴ");
1851 }
1852
1853 #[test]
1854 fn c0_infinite_restore() {
1855 let mut e = auto_engine();
1857 typ(&mut e, "ga"); let o1 = e.press(b'i', false); assert_eq!(o1.preedit, "개");
1860 let o2 = e.press(b'Q', false); assert_eq!(o2.commit, "");
1862 assert_eq!(o2.preedit, "가");
1863 }
1864
1865 #[test]
1866 fn c0_infinite_split() {
1867 let mut e = auto_engine();
1869 typ(&mut e, "ga"); e.press(b'i', false); let o = e.press(b'P', false); assert_eq!(o.commit, "가");
1873 assert_eq!(o.preedit, "ㅐ");
1874 }
1875
1876 #[test]
1877 fn c0_infinite_restore_noop_without_overwrite() {
1878 let mut e = auto_engine();
1880 typ(&mut e, "ga"); let o = e.press(b'Q', false); assert_eq!(o.preedit, "가");
1883 }
1884
1885 #[test]
1886 fn c0_infinite_restore_noop_after_backspace() {
1887 let mut e = auto_engine();
1889 typ(&mut e, "ga"); e.press(b'i', false); let b = e.backspace(); assert_eq!(b.preedit, "ㄱ");
1893 let o = e.press(b'Q', false); assert_eq!(o.commit, "");
1895 assert_eq!(o.preedit, "ㄱ");
1896 }
1897
1898 #[test]
1899 fn c0_infinite_split_no_phantom_after_keep_only() {
1900 let mut e = auto_engine();
1903 typ(&mut e, "ga"); e.press(b'i', false); let g = e.press(b'G', false); assert_eq!(g.preedit, "ㄱ");
1907 let o = e.press(b'P', false); assert_eq!(o.commit, "");
1909 assert_eq!(o.preedit, "ㄱ");
1910 }
1911
1912 #[test]
1913 fn c0_swap_last_two_keys_reorders_compound() {
1914 let mut e = auto_engine();
1917 typ(&mut e, "oa"); assert_eq!(e.cur.jung, Some(0x116A)); let o = e.press(b'S', false); assert!(o.consumed);
1921 assert_eq!(e.cur.jung, Some(0x1169)); }
1923
1924 #[test]
1927 fn c0_backspace1_like_bksp() {
1928 let mut e = auto_engine();
1930 typ(&mut e, "gam"); let o = e.press(b'W', false); assert!(o.consumed);
1933 assert_eq!(o.preedit, "가");
1934 }
1935
1936 #[test]
1937 fn c0_backspace3_always_consumes() {
1938 let mut e = auto_engine();
1940 let o3 = e.press(b'X', false); assert!(o3.consumed);
1942 let mut e2 = auto_engine();
1943 let o1 = e2.press(b'W', false); assert!(!o1.consumed);
1945 }
1946
1947 #[test]
1948 fn c0_recombine_forward_jong() {
1949 let mut e = auto_engine();
1952 e.set_surrounding_ok(true);
1953 typ(&mut e, "gam"); let d = e.press(b'D', false); assert_eq!(d.commit, "가");
1956 let o = e.press(b'T', false); assert_eq!(o.preedit, "간");
1958 assert_eq!(o.delete_before, 1);
1959 assert!(o.consumed);
1960 }
1961
1962 #[test]
1963 fn c0_recombine_forward_dubeol() {
1964 let mut e = auto_engine();
1967 e.set_surrounding_ok(true);
1968 typ(&mut e, "gan"); let o = e.press(b'U', false); assert_eq!(o.preedit, "간");
1971 assert_eq!(o.delete_before, 1);
1972 }
1973
1974 #[test]
1975 fn c0_recombine_noop_without_surrounding() {
1976 let mut e = auto_engine();
1978 typ(&mut e, "gan"); let o = e.press(b'U', false); assert_eq!(o.preedit, "ㄴ"); assert_eq!(o.delete_before, 0);
1983 }
1984
1985 fn reverse_bksp_engine() -> Engine {
1989 let xml = r#"<?xml version="1.0" encoding="utf-8"?>
1990<EditContextSetting version="0x500">
1991 <EditorLayer flag="0"><FinalConvTable/></EditorLayer>
1992 <InputLayer default="0" current="0">
1993 <InputEntry>
1994 <InputSchemeSetting object="CBasicInputScheme">
1995 <KeyTable name="rev" flag="0" from="33" to="126">
1996 <Key at="0x6F" value="H3|Q_"/> <!-- o 초성 ㅇ -->
1997 <Key at="0x73" value="H3|_S"/> <!-- s 종성 ㅅ -->
1998 <Key at="0x61" value="H3|A_"/> <!-- a 중성 ㅏ -->
1999 </KeyTable>
2000 </InputSchemeSetting>
2001 <GeneratorSetting object="CNgsImeEx">
2002 <UnitMixTable/><VirtualUnitTable/>
2003 <AutomataTable default="0">
2004 <Automata state="0" value="1" default="0"/>
2005 <Automata state="1" value="A||B||C ? (A||D)&&(B||E) ? 2 : 1 : -2" default="-1"/>
2006 <Automata state="2" value="A&&A!=500 ? 0 : B||C||A==500 ? 2 : -2" default="0"/>
2007 </AutomataTable>
2008 <Extra><Bksp key="1" value1="BkspAttach" value2="ByUnitStep|BkspAttach" condition1="ReverseJLTRN" condition2="0"/></Extra>
2009 </GeneratorSetting>
2010 </InputEntry>
2011 </InputLayer>
2012</EditContextSetting>"#;
2013 let cfg = Config::parse(xml).unwrap();
2014 Engine::new(cfg.compile(0).unwrap())
2015 }
2016
2017 #[test]
2018 fn moachigi_backspace_deletes_jong_first() {
2019 let mut e = reverse_bksp_engine();
2020 typ(&mut e, "osa"); assert_eq!(e.preedit(), "앗");
2022 let o = e.backspace(); assert_eq!(o.preedit, "아");
2024 let o2 = e.backspace(); assert_eq!(o2.preedit, "ㅇ");
2026 }
2027
2028 #[test]
2029 fn reverse_jltrn_sets_lowest_lastkey() {
2030 let e = reverse_bksp_engine();
2032 assert_eq!(
2033 e.layout().bksp.composing,
2034 crate::config::BkspUnit::LowestLastKey
2035 );
2036 assert!(e.layout().bksp.attach); }
2038}