use super::{dubeolsik, romaja, KoreanLayout};
use crate::types::{QueryRequest, SchemeKey, SchemeType};
#[derive(Debug, Clone, Default)]
pub struct KoreanScheme {
layout: KoreanLayout,
raw: String,
committed: String,
hanja: bool,
}
impl KoreanScheme {
pub fn new() -> Self {
Self::default()
}
pub fn with_layout(layout: KoreanLayout) -> Self {
Self {
layout,
..Self::new()
}
}
pub fn reset(&mut self) {
self.raw.clear();
self.committed.clear();
self.hanja = false;
}
pub fn handle_key(&mut self, key: SchemeKey) -> bool {
match key {
SchemeKey::Backspace => {
let changed = self.hanja || self.raw.pop().is_some();
self.hanja = false;
changed
}
SchemeKey::Letter(letter) if letter.is_ascii_alphabetic() => {
self.hanja = false;
self.raw.push(char::from(letter));
let open = match self.layout {
KoreanLayout::Dubeolsik => {
dubeolsik::split_finished_into(&self.raw, &mut self.committed)
}
KoreanLayout::Romaja => {
romaja::split_finished_into(&self.raw, &mut self.committed)
}
};
let open_start = self.raw.len() - open.len();
if open_start > 0 {
self.raw.drain(..open_start);
}
true
}
SchemeKey::Letter(_)
| SchemeKey::Apostrophe
| SchemeKey::Semicolon
| SchemeKey::Minus
| SchemeKey::Symbol(_)
| SchemeKey::Requery => false,
}
}
pub fn build_request(&self) -> QueryRequest {
let mut request = QueryRequest::default();
self.build_request_into(&mut request);
request
}
pub fn build_request_into(&self, request: &mut QueryRequest) {
request.scheme = SchemeType::Korean;
request.raw_input.clear();
request.raw_input.extend(
self.raw
.bytes()
.map(|byte| char::from(byte.to_ascii_lowercase())),
);
request.raw_input_with_cases.clone_from(&self.raw);
request.normalized_input.clone_from(&request.raw_input);
request.raw_segmentation.clone_from(&self.raw);
self.compose_into(&mut request.normalized_segmentation);
request
.segmentation
.clone_from(&request.normalized_segmentation);
request.korean_hanja = self.hanja;
request.valid = !self.raw.is_empty();
}
pub fn preedit(&self) -> String {
if self.layout == KoreanLayout::Dubeolsik {
return dubeolsik::compose(&self.raw);
}
let mut text = String::new();
self.compose_into(&mut text);
text
}
fn compose_into(&self, output: &mut String) {
match self.layout {
KoreanLayout::Dubeolsik => dubeolsik::compose_into(&self.raw, output),
KoreanLayout::Romaja => romaja::compose_into(&self.raw, output),
}
}
pub fn set_raw_input(&mut self, raw: &str, raw_with_cases: &str) {
let source = if raw_with_cases.is_empty() {
raw
} else {
raw_with_cases
};
self.raw.clear();
self.raw.reserve(source.len());
self.raw
.extend(source.chars().filter(char::is_ascii_alphabetic));
self.committed.clear();
self.hanja = false;
}
pub fn open_hanja(&mut self) {
self.hanja = !self.raw.is_empty();
}
pub fn close_hanja(&mut self) {
self.hanja = false;
}
pub fn hanja_open(&self) -> bool {
self.hanja
}
pub fn take_committed(&mut self) -> String {
std::mem::take(&mut self.committed)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn typed(keys: &str) -> (KoreanScheme, String) {
let mut scheme = KoreanScheme::new();
let mut committed = String::new();
for byte in keys.bytes() {
scheme.handle_key(SchemeKey::Letter(byte));
committed.push_str(&scheme.take_committed());
}
(scheme, committed)
}
#[test]
fn finished_syllables_leave_the_composition() {
let cases = [
("rk", "", "가"),
("rks", "", "간"),
("rksk", "가", "나"),
("dkssud", "안", "녕"),
("dkssudgktpdy", "안녕하세", "요"),
("ekfr", "", "닭"),
("ekfrk", "달", "가"),
("rr", "ㄱ", "ㄱ"),
("kk", "ㅏ", "ㅏ"),
("rhk", "", "과"),
("RkTkEk", "까싸", "따"),
];
for (keys, committed, preedit) in cases {
let (scheme, text) = typed(keys);
assert_eq!(text, committed, "{keys}");
assert_eq!(scheme.preedit(), preedit, "{keys}");
}
}
#[test]
fn the_composition_keeps_only_the_open_syllable_keys() {
let (scheme, _) = typed("ekfrk");
assert_eq!(scheme.build_request().raw_input_with_cases, "rk");
let (scheme, _) = typed("dkssud");
assert_eq!(scheme.build_request().raw_input_with_cases, "sud");
}
#[test]
fn set_raw_input_reuses_existing_storage() {
let mut scheme = KoreanScheme::new();
scheme.set_raw_input("abcdefghijklmnopqrstuvwxyz", "");
let capacity = scheme.raw.capacity();
scheme.set_raw_input("rk", "");
assert_eq!(scheme.build_request().raw_input_with_cases, "rk");
assert!(scheme.raw.capacity() >= capacity);
}
#[test]
fn moving_a_final_reuses_the_open_key_storage() {
let mut scheme = KoreanScheme::new();
scheme.set_raw_input("abcdefghijklmnop", "");
scheme.reset();
scheme.committed.reserve(16);
for key in b"rks" {
scheme.handle_key(SchemeKey::Letter(*key));
}
let pointer = scheme.raw.as_ptr();
let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
scheme.handle_key(SchemeKey::Letter(b'k'));
});
assert_eq!(allocations, 0);
assert_eq!(scheme.raw.as_ptr(), pointer);
assert_eq!(scheme.preedit(), "나");
assert_eq!(scheme.take_committed(), "가");
}
#[test]
fn typing_reuses_raw_and_committed_storage_without_temporaries() {
let mut scheme = KoreanScheme::new();
scheme.raw.reserve(32);
scheme.committed.reserve(128);
let keys = b"dkssudgktpdyekfrkrkqtlRkTkEk";
let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
for key in keys {
scheme.handle_key(SchemeKey::Letter(*key));
}
});
assert_eq!(scheme.committed, "안녕하세요달가갑시까싸");
assert_eq!(scheme.preedit(), "따");
assert_eq!(allocations, 0, "逐键折叠应直接追加已完成的音节");
}
#[test]
fn request_refresh_reuses_hangul_output_without_temporaries() {
let mut scheme = KoreanScheme::new();
scheme.set_raw_input("dkssudgktpdyekfrk", "");
let mut request = scheme.build_request();
let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
for _ in 0..8 {
scheme.build_request_into(&mut request);
}
});
assert_eq!(request.normalized_segmentation, "안녕하세요달가");
assert_eq!(request.segmentation, "안녕하세요달가");
assert_eq!(allocations, 0, "热请求刷新应复用合成字符串");
}
#[test]
fn backspace_removes_one_jamo_at_a_time() {
let (mut scheme, _) = typed("ekfr");
let mut shown = vec![scheme.preedit()];
while !scheme.preedit().is_empty() {
scheme.handle_key(SchemeKey::Backspace);
shown.push(scheme.preedit());
}
assert_eq!(shown, ["닭", "달", "다", "ㄷ", ""]);
let (mut scheme, _) = typed("rhkd");
let mut shown = vec![scheme.preedit()];
while !scheme.preedit().is_empty() {
scheme.handle_key(SchemeKey::Backspace);
shown.push(scheme.preedit());
}
assert_eq!(shown, ["광", "과", "고", "ㄱ", ""]);
let (mut scheme, committed) = typed("rksk");
assert_eq!(committed, "가");
scheme.handle_key(SchemeKey::Backspace);
assert_eq!(scheme.preedit(), "ㄴ");
assert_eq!(scheme.take_committed(), "");
scheme.handle_key(SchemeKey::Backspace);
assert_eq!(scheme.preedit(), "");
scheme.handle_key(SchemeKey::Backspace);
assert_eq!(scheme.preedit(), "");
}
#[test]
fn other_keys_are_ignored() {
let (mut scheme, _) = typed("rk");
for key in [
SchemeKey::Apostrophe,
SchemeKey::Semicolon,
SchemeKey::Minus,
SchemeKey::Requery,
SchemeKey::Letter(b'1'),
SchemeKey::Symbol(b';'),
SchemeKey::Symbol(b' '),
] {
scheme.handle_key(key);
}
assert_eq!(scheme.preedit(), "가");
assert_eq!(scheme.take_committed(), "");
}
#[test]
fn set_raw_input_round_trips_and_prefers_the_cased_spelling() {
let (scheme, _) = typed("Rks");
let request = scheme.build_request();
let mut restored = KoreanScheme::new();
restored.set_raw_input(&request.raw_input, &request.raw_input_with_cases);
assert_eq!(restored.preedit(), "깐");
assert_eq!(restored.build_request(), request);
restored.set_raw_input("rks", "");
assert_eq!(restored.preedit(), "간");
restored.set_raw_input("dkssud", "dks'sud 1");
assert_eq!(restored.preedit(), "안녕");
assert_eq!(restored.take_committed(), "");
restored.handle_key(SchemeKey::Letter(b'g'));
assert_eq!(restored.take_committed(), "안녕");
assert_eq!(restored.preedit(), "ㅎ");
}
#[test]
fn set_raw_input_reserves_source_capacity() {
let source: String = (0..100)
.map(|index| if index % 5 == 0 { '1' } else { 'a' })
.collect();
let mut scheme = KoreanScheme::new();
scheme.set_raw_input(&source, "");
assert_eq!(scheme.raw.len(), 80);
assert_eq!(scheme.raw.capacity(), source.len());
}
#[test]
fn request_fields() {
let (scheme, _) = typed("gkS");
let request = scheme.build_request();
assert_eq!(request.scheme, SchemeType::Korean);
assert_eq!(request.raw_input, "gks");
assert_eq!(request.normalized_input, "gks");
assert_eq!(request.raw_input_with_cases, "gkS");
assert_eq!(request.raw_segmentation, "gkS");
assert_eq!(request.normalized_segmentation, "한");
assert_eq!(request.segmentation, "한");
assert!(request.valid);
assert!(!KoreanScheme::new().build_request().valid);
}
#[test]
fn reset_drops_the_composition_and_pending_commits() {
let mut scheme = KoreanScheme::new();
for byte in b"rkr" {
scheme.handle_key(SchemeKey::Letter(*byte));
}
scheme.handle_key(SchemeKey::Letter(b'r'));
scheme.reset();
assert_eq!(scheme.preedit(), "");
assert_eq!(scheme.take_committed(), "");
}
#[test]
fn romaja_restore_preserves_case_and_sessions_keep_separate_layouts() {
let mut roman = KoreanScheme::with_layout(KoreanLayout::Romaja);
let mut default = KoreanScheme::new();
for byte in b"Ga" {
roman.handle_key(SchemeKey::Letter(*byte));
default.handle_key(SchemeKey::Letter(*byte));
}
assert_eq!(roman.preedit(), "까");
assert_eq!(default.take_committed(), "ㅎ");
assert_eq!(default.preedit(), "ㅁ");
let request = roman.build_request();
let mut restored = KoreanScheme::with_layout(KoreanLayout::Romaja);
restored.set_raw_input(&request.raw_input, &request.raw_input_with_cases);
assert_eq!(restored.build_request(), request);
restored.reset();
restored.set_raw_input("han", "");
assert_eq!(restored.preedit(), "한");
assert_eq!(default.preedit(), "ㅁ");
}
}