Skip to main content

yuru_ko/
lib.rs

1//! Korean Hangul matching backend for Yuru.
2//!
3//! The backend adds deterministic Hangul romanization, choseong initials, and
4//! Korean 2-set keyboard keys while preserving source spans for highlighting.
5
6/// Hangul decomposition and generated key helpers.
7pub mod hangul;
8
9use yuru_core::{
10    base_query_variants, KeyBudget, LangMode, LanguageBackend, QueryBudget, QueryVariant, SearchKey,
11};
12
13#[derive(Clone, Debug)]
14/// Korean language backend for romanization, initials, and keyboard keys.
15pub struct KoreanBackend {
16    romanization: bool,
17    initials: bool,
18    keyboard: bool,
19}
20
21impl KoreanBackend {
22    /// Creates a Korean backend with selected generated key families.
23    pub fn new(romanization: bool, initials: bool, keyboard: bool) -> Self {
24        Self {
25            romanization,
26            initials,
27            keyboard,
28        }
29    }
30}
31
32impl Default for KoreanBackend {
33    fn default() -> Self {
34        Self {
35            romanization: true,
36            initials: true,
37            keyboard: true,
38        }
39    }
40}
41
42impl LanguageBackend for KoreanBackend {
43    fn mode(&self) -> LangMode {
44        LangMode::Korean
45    }
46
47    fn build_candidate_keys(&self, text: &str, budget: KeyBudget) -> Vec<SearchKey> {
48        if budget.max_keys == 0 || text.len() > budget.max_total_bytes {
49            return Vec::new();
50        }
51        hangul::build_korean_keys_with_sources_with_budget(
52            text,
53            budget.max_keys,
54            budget.max_total_bytes,
55        )
56        .into_iter()
57        .filter_map(|key| {
58            let search_key = match key.kind {
59                hangul::KoreanKeyKind::Romanized => {
60                    if !self.romanization {
61                        return None;
62                    }
63                    SearchKey::korean_romanized(key.text)
64                }
65                hangul::KoreanKeyKind::Initials => {
66                    if !self.initials {
67                        return None;
68                    }
69                    SearchKey::korean_initials(key.text)
70                }
71                hangul::KoreanKeyKind::Keyboard => {
72                    if !self.keyboard {
73                        return None;
74                    }
75                    SearchKey::korean_keyboard(key.text)
76                }
77            };
78            Some(search_key.with_source_map(key.source_map))
79        })
80        .collect()
81    }
82
83    fn expand_query(&self, query: &str, _budget: QueryBudget) -> Vec<QueryVariant> {
84        base_query_variants(query)
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use yuru_core::{build_candidate, KeyKind, SearchConfig};
91
92    use super::*;
93
94    #[test]
95    fn korean_mode_builds_hangul_keys() {
96        let backend = KoreanBackend::default();
97        let cand = build_candidate(0, "한글.txt", &backend, &SearchConfig::default());
98
99        assert!(cand
100            .keys
101            .iter()
102            .any(|key| key.kind == KeyKind::KoreanRomanized && key.text == "hangeul"));
103        assert!(cand
104            .keys
105            .iter()
106            .any(|key| key.kind == KeyKind::KoreanInitials && key.text == "ㅎㄱ"));
107        assert!(cand
108            .keys
109            .iter()
110            .any(|key| key.kind == KeyKind::KoreanKeyboard && key.text == "gksrmf"));
111    }
112
113    #[test]
114    fn korean_mode_can_disable_each_generated_key_family() {
115        let backend = KoreanBackend::new(false, true, false);
116        let cand = build_candidate(0, "한글", &backend, &SearchConfig::default());
117
118        assert!(cand
119            .keys
120            .iter()
121            .any(|key| key.kind == KeyKind::KoreanInitials));
122        assert!(!cand
123            .keys
124            .iter()
125            .any(|key| key.kind == KeyKind::KoreanRomanized));
126        assert!(!cand
127            .keys
128            .iter()
129            .any(|key| key.kind == KeyKind::KoreanKeyboard));
130    }
131
132    #[test]
133    fn korean_mode_does_not_build_japanese_or_chinese_keys() {
134        let backend = KoreanBackend::default();
135        let cand = build_candidate(0, "한글", &backend, &SearchConfig::default());
136
137        assert!(!cand.keys.iter().any(|key| matches!(
138            key.kind,
139            KeyKind::KanaReading
140                | KeyKind::RomajiReading
141                | KeyKind::PinyinFull
142                | KeyKind::PinyinJoined
143                | KeyKind::PinyinInitials
144        )));
145    }
146}