1pub mod tables;
12pub mod engine;
13pub mod spelling;
14pub mod charset;
15pub mod vseq;
16
17use std::collections::HashSet;
18use std::ffi::{CStr, CString};
19use std::os::raw::c_char;
20use std::sync::OnceLock;
21
22#[derive(Clone, Copy, PartialEq, Eq)]
24pub enum Method { Telex, Vni, TeipVni, Viqr }
25
26static DICT: OnceLock<Vec<&'static str>> = OnceLock::new();
33
34static BASES: OnceLock<Vec<String>> = OnceLock::new();
39
40fn dict() -> &'static Vec<&'static str> {
42 DICT.get_or_init(|| {
43 include_str!("../data/vietnamese.cm.dict")
44 .lines()
45 .collect()
46 })
47}
48
49fn bases() -> &'static Vec<String> {
51 BASES.get_or_init(|| {
52 let mut v: Vec<String> =
53 dict().iter().map(|w| charset::remove_tone(w)).collect();
54 v.sort();
55 v.dedup();
56 v
57 })
58}
59
60pub struct SkeyEngine {
62 method: Method,
63 modern: bool,
64 short_w: bool,
65 auto_restore: bool,
66 dict: bool,
67 user_words: HashSet<String>,
68}
69
70impl SkeyEngine {
71 pub fn new(method: Method) -> Self {
72 SkeyEngine {
73 method,
74 modern: true,
75 short_w: false,
76 auto_restore: false,
77 dict: false,
78 user_words: HashSet::new(),
79 }
80 }
81
82 pub fn set_method(&mut self, method: Method) { self.method = method; }
83 pub fn set_modern(&mut self, v: bool) { self.modern = v; }
84 pub fn set_short_w(&mut self, v: bool) { self.short_w = v; }
85 pub fn set_auto_restore(&mut self, v: bool) { self.auto_restore = v; }
86 pub fn set_dict(&mut self, v: bool) { self.dict = v; }
90 pub fn add_word(&mut self, word: &str) {
93 self.user_words.insert(word.to_lowercase());
94 }
95
96 pub fn clear_words(&mut self) {
98 self.user_words.clear();
99 }
100
101 fn dict_known(&self, word: &str) -> bool {
115 let base = charset::remove_tone(word);
116 if base != word {
117 if self.user_words.contains(word)
119 || dict().binary_search(&word).is_ok()
120 {
121 return true;
122 }
123 for w in &self.user_words {
124 let wb = charset::remove_tone(w);
125 if wb.starts_with(&base) && wb != base {
126 return true;
127 }
128 }
129 let bs = bases();
130 match bs.binary_search(&base) {
131 Ok(_) => false, Err(i) => i < bs.len() && bs[i].starts_with(&base),
133 }
134 } else {
135 for w in &self.user_words {
137 if charset::remove_tone(w).starts_with(&base) {
138 return true;
139 }
140 }
141 let bs = bases();
142 match bs.binary_search(&base) {
143 Ok(_) => true,
144 Err(i) => i < bs.len() && bs[i].starts_with(&base),
145 }
146 }
147 }
148
149 pub fn transform(&self, input: &str) -> String {
150 let composed = match self.method {
151 Method::Telex => engine::convert_telex(input, self.modern, self.short_w),
152 Method::Vni => engine::convert_vni(input, self.modern, self.short_w),
153 Method::TeipVni => engine::convert_teip_vni(input, self.modern, self.short_w),
154 Method::Viqr => engine::convert_viqr(input),
155 };
156 let all_ascii = composed.chars().all(|c| c.is_ascii());
164 let known = if self.dict {
165 self.dict_known(&composed.to_lowercase())
169 } else {
170 Self::is_valid(&composed)
171 };
172 if self.auto_restore && !all_ascii && composed != input
173 && !known && !has_vn_markers(input)
174 {
175 input.to_string()
176 } else {
177 composed
178 }
179 }
180
181 pub fn is_valid(s: &str) -> bool {
182 spelling::is_valid_cvc(s)
183 }
184}
185
186pub(crate) fn has_vn_markers(input: &str) -> bool {
191 let lower = input.to_lowercase();
208 let dd_marker = lower.starts_with("dd")
209 || lower.match_indices("dd").any(|(i, _)| {
210 i > 0
211 && !matches!(
212 lower[..i].chars().next_back(),
213 Some('a' | 'e' | 'i' | 'o' | 'u' | 'y')
214 )
215 });
216 dd_marker
217 || lower.contains("aaa")
218 || lower.contains("eee")
219 || lower.contains("ooo")
220 || lower.contains("aww")
221 || lower.contains("oww")
222 || lower.contains("uww")
223}
224
225#[unsafe(no_mangle)]
228pub unsafe extern "C" fn skey_engine_new(method: i32) -> *mut SkeyEngine {
229 let m = match method {
230 0 => Method::Telex, 1 => Method::Vni, 2 => Method::Viqr, 3 => Method::TeipVni, _ => Method::Telex,
235 };
236 Box::into_raw(Box::new(SkeyEngine::new(m)))
237}
238
239#[unsafe(no_mangle)]
240pub unsafe extern "C" fn skey_engine_free(engine: *mut SkeyEngine) {
241 if !engine.is_null() { unsafe { drop(Box::from_raw(engine)); } }
242}
243
244#[unsafe(no_mangle)]
245pub unsafe extern "C" fn skey_engine_set_method(engine: *mut SkeyEngine, method: i32) {
246 if engine.is_null() { return; }
247 let m = match method {
248 0 => Method::Telex,
249 1 => Method::Vni,
250 2 => Method::Viqr,
251 3 => Method::TeipVni,
252 _ => Method::Telex,
253 };
254 unsafe { (*engine).set_method(m); }
255}
256
257#[unsafe(no_mangle)]
259pub unsafe extern "C" fn skey_engine_set_tone_style(e: *mut SkeyEngine, v: i32) {
260 if !e.is_null() { unsafe { (*e).set_modern(v != 0); } }
261}
262#[unsafe(no_mangle)]
263pub unsafe extern "C" fn skey_engine_set_free_marking(e: *mut SkeyEngine, v: i32) {
264 if !e.is_null() { unsafe { (*e).set_modern(v != 0); } }
267}
268#[unsafe(no_mangle)]
269pub unsafe extern "C" fn skey_engine_set_short_w(e: *mut SkeyEngine, v: i32) {
270 if !e.is_null() { unsafe { (*e).set_short_w(v != 0); } }
271}
272#[unsafe(no_mangle)]
273pub unsafe extern "C" fn skey_engine_set_auto_restore(e: *mut SkeyEngine, v: i32) {
274 if !e.is_null() { unsafe { (*e).set_auto_restore(v != 0); } }
275}
276#[unsafe(no_mangle)]
277pub unsafe extern "C" fn skey_engine_set_dict(e: *mut SkeyEngine, v: i32) {
278 if !e.is_null() { unsafe { (*e).set_dict(v != 0); } }
279}
280#[unsafe(no_mangle)]
281pub unsafe extern "C" fn skey_engine_add_word(e: *mut SkeyEngine, word: *const c_char) {
282 if e.is_null() || word.is_null() { return; }
283 let s = match unsafe { CStr::from_ptr(word) }.to_str() {
284 Ok(s) => s, Err(_) => return,
285 };
286 unsafe { (*e).add_word(s); }
287}
288#[unsafe(no_mangle)]
289pub unsafe extern "C" fn skey_engine_clear_words(e: *mut SkeyEngine) {
290 if !e.is_null() { unsafe { (*e).clear_words(); } }
291}
292
293#[unsafe(no_mangle)]
296pub unsafe extern "C" fn skey_engine_dict_words() -> *mut c_char {
297 CString::new(dict().join("\n")).unwrap_or_default().into_raw()
298}
299#[unsafe(no_mangle)]
300pub unsafe extern "C" fn skey_engine_set_bracket_uo(_e: *mut SkeyEngine, _v: i32) {}
301
302#[unsafe(no_mangle)]
303pub unsafe extern "C" fn skey_engine_transform(
304 engine: *const SkeyEngine,
305 input: *const c_char,
306) -> *mut c_char {
307 if engine.is_null() || input.is_null() { return std::ptr::null_mut(); }
308 let e = unsafe { &*engine };
309 let s = match unsafe { CStr::from_ptr(input) }.to_str() {
310 Ok(s) => s, Err(_) => return std::ptr::null_mut(),
311 };
312 CString::new(e.transform(s)).unwrap_or_default().into_raw()
313}
314
315#[unsafe(no_mangle)]
318pub unsafe extern "C" fn skey_charset_encode(
319 input: *const c_char,
320 charset: i32,
321 out_len: *mut usize,
322) -> *mut u8 {
323 if input.is_null() || out_len.is_null() { return std::ptr::null_mut(); }
324 let s = match unsafe { CStr::from_ptr(input) }.to_str() {
325 Ok(s) => s, Err(_) => return std::ptr::null_mut(),
326 };
327 let cs = match charset {
328 0 => charset::VietCharset::Unicode,
329 1 => charset::VietCharset::TCVN3,
330 2 => charset::VietCharset::VNIWin,
331 3 => charset::VietCharset::WinCP1258,
332 4 => charset::VietCharset::VIQR,
333 _ => charset::VietCharset::Unicode,
334 };
335 let encoded = charset::encode(s, cs);
336 let len = encoded.len();
337 let ptr = libc::malloc(len).cast::<u8>();
338 if ptr.is_null() { return std::ptr::null_mut(); }
339 unsafe { std::ptr::copy_nonoverlapping(encoded.as_ptr(), ptr, len); }
340 unsafe { *out_len = len; }
341 ptr
342}
343
344#[unsafe(no_mangle)]
345pub unsafe extern "C" fn skey_charset_decode(
346 input: *const u8,
347 len: usize,
348 charset: i32,
349) -> *mut c_char {
350 if input.is_null() { return std::ptr::null_mut(); }
351 let bytes = unsafe { std::slice::from_raw_parts(input, len) };
352 let cs = match charset {
353 0 => charset::VietCharset::Unicode,
354 1 => charset::VietCharset::TCVN3,
355 2 => charset::VietCharset::VNIWin,
356 3 => charset::VietCharset::WinCP1258,
357 4 => charset::VietCharset::VIQR,
358 _ => charset::VietCharset::Unicode,
359 };
360 let decoded = charset::decode(bytes, cs);
361 CString::new(decoded).unwrap_or_default().into_raw()
362}
363
364#[unsafe(no_mangle)]
365pub unsafe extern "C" fn skey_charset_remove_tone(input: *const c_char) -> *mut c_char {
366 if input.is_null() { return std::ptr::null_mut(); }
367 let s = match unsafe { CStr::from_ptr(input) }.to_str() {
368 Ok(s) => s, Err(_) => return std::ptr::null_mut(),
369 };
370 CString::new(charset::remove_tone(s)).unwrap_or_default().into_raw()
371}
372
373#[unsafe(no_mangle)]
374pub unsafe extern "C" fn skey_charset_free_buf(ptr: *mut u8) {
375 if !ptr.is_null() { unsafe { libc::free(ptr.cast()); } }
376}
377
378#[unsafe(no_mangle)]
381pub unsafe extern "C" fn skey_engine_is_valid(s: *const c_char) -> i32 {
382 if s.is_null() { return 0; }
383 let s = match unsafe { CStr::from_ptr(s) }.to_str() {
384 Ok(s) => s, Err(_) => return 0,
385 };
386 if SkeyEngine::is_valid(s) { 1 } else { 0 }
387}
388
389#[unsafe(no_mangle)]
390pub unsafe extern "C" fn skey_free_string(s: *mut c_char) {
391 if !s.is_null() { unsafe { drop(CString::from_raw(s)); } }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 fn eng(auto_restore: bool, dict: bool) -> SkeyEngine {
399 let mut e = SkeyEngine::new(Method::Telex);
400 e.set_auto_restore(auto_restore);
401 e.set_dict(dict);
402 e
403 }
404
405 #[test]
406 fn dict_restores_nonwords() {
407 let e = eng(true, true);
408 assert_eq!(e.transform("luwocs"), "luwocs");
411 assert_eq!(e.transform("need"), "need");
413 assert_eq!(e.transform("force"), "force");
415 }
416
417 #[test]
418 fn dict_keeps_prefixes_realtime() {
419 let e = eng(true, true);
422 assert_eq!(e.transform("phuowc"), "phươc"); assert_eq!(e.transform("phuowcs"), "phước"); assert_eq!(e.transform("truwoc"), "trươc"); assert_eq!(e.transform("luow"), "lươ");
428 assert_eq!(e.transform("luowj"), "lượ");
429 assert_eq!(e.transform("luowjc"), "lược");
430 let mut e2 = eng(true, true);
432 e2.add_word("lước");
433 assert_eq!(e2.transform("luwoc"), "lươc");
434 }
435
436 #[test]
437 fn dict_keeps_real_words() {
438 let e = eng(true, true);
439 assert_eq!(e.transform("truwocs"), "trước");
440 assert_eq!(e.transform("khoong"), "không");
441 assert_eq!(e.transform("Vieetj"), "Việt");
442 assert_eq!(e.transform("luoojcw"), "lược"); }
444
445 #[test]
446 fn dict_keeps_abbreviations() {
447 let e = eng(true, true);
448 assert_eq!(e.transform("ddc"), "đc"); }
450
451 #[test]
452 fn auto_restore_english_dd_words() {
453 let e = eng(true, false);
456 assert_eq!(e.transform("add"), "add");
457 assert_eq!(e.transform("addr"), "addr");
458 assert_eq!(e.transform("addre"), "addre");
459 assert_eq!(e.transform("addres"), "addres");
460 assert_eq!(e.transform("address"), "address");
461 assert_eq!(e.transform("ladder"), "ladder");
462 assert_eq!(e.transform("odd"), "odd"); let e = eng(true, true);
465 assert_eq!(e.transform("address"), "address");
466 let e = eng(true, false);
468 assert_eq!(e.transform("ddc"), "đc");
469 assert_eq!(e.transform("ddi"), "đi");
470 assert_eq!(e.transform("vcdd"), "vcđ");
473 assert_eq!(e.transform("kdd"), "kđ");
474 assert_eq!(e.transform("cmdd"), "cmđ");
475 }
476
477 #[test]
478 fn double_vowel_swap_survives_auto_restore() {
479 let e = eng(true, false);
482 assert_eq!(e.transform("lawsma"), "lấm");
483 let e = eng(true, true);
484 assert_eq!(e.transform("lawsma"), "lấm"); assert_eq!(e.transform("luowjco"), "luộc"); }
487
488 #[test]
489 fn dict_off_keeps_rule_based_behavior() {
490 let e = eng(true, false);
491 assert_eq!(e.transform("luwocs"), "lước"); assert_eq!(e.transform("need"), "need"); }
494
495 #[test]
496 fn dict_user_words_override() {
497 let mut e = eng(true, true);
498 assert_eq!(e.transform("luwocs"), "luwocs");
499 e.add_word("Lước"); assert_eq!(e.transform("luwocs"), "lước");
501 assert_eq!(e.transform("LUWOCS"), "LƯỚC");
502 }
503
504 #[test]
505 fn dict_embedded_is_sorted() {
506 let dict = DICT.get_or_init(|| {
508 include_str!("../data/vietnamese.cm.dict")
509 .lines()
510 .collect()
511 });
512 assert!(dict.len() > 7000, "dict unexpectedly small: {}", dict.len());
513 for w in dict.windows(2) {
514 assert!(w[0] < w[1], "dict not sorted at {:?}", w);
515 }
516 }
517}