mod hmm;
mod kcc;
mod normalize;
mod strategy;
mod trie;
pub use hmm::HmmModel;
pub use kcc::{is_khmer, split_kcc};
pub use normalize::normalize;
pub use strategy::Strategy;
pub use trie::KhmerTokenizer;
pub const DEFAULT_DICT: &str = include_str!("dict.txt");
impl KhmerTokenizer {
pub fn with_default_dict() -> Self {
Self::from_dict_str(DEFAULT_DICT)
}
pub fn from_dict_str(dict: &str) -> Self {
let words = dict
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'));
Self::from_words(words)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tk() -> KhmerTokenizer {
KhmerTokenizer::from_words([
"សួស្តី",
"អ្នក",
"ទាំងអស់គ្នា",
"កម្ពុជា",
"ភាសា",
"ខ្មែរ",
"ខ្ញុំ",
"ស្រឡាញ់",
])
}
#[test]
fn segments_known_words() {
assert_eq!(
tk().segment("សួស្តីអ្នកទាំងអស់គ្នា"),
vec!["សួស្តី", "អ្នក", "ទាំងអស់គ្នា"]
);
assert_eq!(
tk().segment("ខ្ញុំស្រឡាញ់កម្ពុជា"),
vec!["ខ្ញុំ", "ស្រឡាញ់", "កម្ពុជា"]
);
assert_eq!(tk().segment("ភាសាខ្មែរ"), vec!["ភាសា", "ខ្មែរ"]);
}
#[test]
fn handles_mixed_scripts() {
assert_eq!(
tk().segment("ខ្ញុំស្រឡាញ់ Rust 2026 កម្ពុជា"),
vec!["ខ្ញុំ", "ស្រឡាញ់", "Rust", "2026", "កម្ពុជា"]
);
}
#[test]
fn oov_falls_back_to_clusters() {
assert_eq!(tk().segment("ឆ្នាំថ្មី"), vec!["ឆ្នាំ", "ថ្មី"]);
}
#[test]
fn default_dict_loads_and_segments() {
let tk = KhmerTokenizer::with_default_dict();
assert!(!tk.is_empty());
assert_eq!(
tk.segment("សួស្តីអ្នកទាំងអស់គ្នា"),
vec!["សួស្តី", "អ្នក", "ទាំងអស់គ្នា"]
);
}
#[test]
fn dict_str_skips_comments_and_blanks() {
let tk = KhmerTokenizer::from_dict_str("# comment\n\nខ្មែរ\n ភាសា \n");
assert_eq!(tk.len(), 2);
}
}