1use crate::identifier::{is_identifier, split_identifier};
4use crate::lang::is_cjk_char;
5use crate::Tokenizer;
6
7#[derive(Debug, Default, Clone)]
12pub struct WhitespaceTokenizer;
13
14impl Tokenizer for WhitespaceTokenizer {
15 fn tokenize(&self, text: &str) -> Vec<String> {
16 text.split_whitespace()
17 .map(|w| {
18 w.trim_matches(|c: char| c.is_ascii_punctuation())
19 .to_string()
20 })
21 .filter(|t| !t.is_empty())
22 .collect()
23 }
24}
25
26#[derive(Debug, Default, Clone)]
30pub struct CjkCharTokenizer;
31
32impl Tokenizer for CjkCharTokenizer {
33 fn tokenize(&self, text: &str) -> Vec<String> {
34 let mut tokens = Vec::new();
35 let mut latin_buf = String::new();
36
37 let flush = |buf: &mut String, out: &mut Vec<String>| {
38 for part in buf.split_whitespace() {
39 let p = part.trim_matches(|c: char| c.is_ascii_punctuation());
40 if !p.is_empty() {
41 out.push(p.to_string());
42 }
43 }
44 buf.clear();
45 };
46
47 for ch in text.chars() {
48 if is_cjk_char(ch) {
49 flush(&mut latin_buf, &mut tokens);
50 tokens.push(ch.to_string());
51 } else {
52 latin_buf.push(ch);
53 }
54 }
55 flush(&mut latin_buf, &mut tokens);
56 tokens
57 }
58}
59
60#[derive(Debug, Default, Clone)]
63pub struct KeywordTokenizer;
64
65impl Tokenizer for KeywordTokenizer {
66 fn tokenize(&self, text: &str) -> Vec<String> {
67 let t = text.trim();
68 if t.is_empty() {
69 vec![]
70 } else {
71 vec![t.to_string()]
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
81pub struct IdentifierTokenizer {
82 pub min_part_len: usize,
83}
84
85impl Default for IdentifierTokenizer {
86 fn default() -> Self {
87 Self { min_part_len: 1 }
88 }
89}
90
91impl Tokenizer for IdentifierTokenizer {
92 fn tokenize(&self, text: &str) -> Vec<String> {
93 let ws = WhitespaceTokenizer;
94 let mut result: Vec<String> = Vec::new();
95
96 for raw_word in text.split_whitespace() {
97 let word = raw_word.trim_matches(|c: char| c.is_ascii_punctuation());
98 if word.is_empty() {
99 continue;
100 }
101 if is_identifier(word) {
102 let lower = word.to_lowercase();
103 let parts = split_identifier(word, self.min_part_len);
104 result.push(lower.clone());
105 for part in parts {
106 if part != lower {
107 result.push(part);
108 }
109 }
110 } else {
111 result.extend(ws.tokenize(word));
112 }
113 }
114 result
115 }
116}
117
118#[cfg(feature = "unicode")]
122#[derive(Debug, Default, Clone)]
123pub struct UnicodeWordTokenizer;
124
125#[cfg(feature = "unicode")]
126impl Tokenizer for UnicodeWordTokenizer {
127 fn tokenize(&self, text: &str) -> Vec<String> {
128 use unicode_segmentation::UnicodeSegmentation;
129 UnicodeSegmentation::unicode_words(text)
130 .map(|w| w.to_string())
131 .collect()
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn whitespace_normal() {
141 let t = WhitespaceTokenizer;
142 assert_eq!(t.tokenize("hello world"), vec!["hello", "world"]);
143 }
144
145 #[test]
146 fn whitespace_empty() {
147 assert!(WhitespaceTokenizer.tokenize("").is_empty());
148 assert!(WhitespaceTokenizer.tokenize(" ").is_empty());
149 }
150
151 #[test]
152 fn whitespace_strips_outer_punct() {
153 let t = WhitespaceTokenizer;
154 assert_eq!(t.tokenize("hello, world!"), vec!["hello", "world"]);
155 assert_eq!(t.tokenize("...foo..."), vec!["foo"]);
156 }
157
158 #[test]
159 fn whitespace_preserves_inner_punct() {
160 let t = WhitespaceTokenizer;
161 assert_eq!(t.tokenize("fine-tuning v1.2"), vec!["fine-tuning", "v1.2"]);
162 }
163
164 #[test]
165 fn whitespace_all_punct_dropped() {
166 assert!(WhitespaceTokenizer.tokenize("... , ...").is_empty());
167 }
168
169 #[test]
170 fn cjk_spec_example() {
171 let got = CjkCharTokenizer.tokenize("使用LoRA进行fine-tuning");
172 assert_eq!(got, vec!["使", "用", "LoRA", "进", "行", "fine-tuning"]);
173 }
174
175 #[test]
176 fn cjk_empty() {
177 assert!(CjkCharTokenizer.tokenize("").is_empty());
178 }
179
180 #[test]
181 fn cjk_only_cjk() {
182 assert_eq!(CjkCharTokenizer.tokenize("中文"), vec!["中", "文"]);
183 }
184
185 #[test]
186 fn cjk_only_latin() {
187 assert_eq!(
188 CjkCharTokenizer.tokenize("hello world"),
189 vec!["hello", "world"]
190 );
191 }
192
193 #[test]
194 fn cjk_punct_stripped_from_latin_run() {
195 let got = CjkCharTokenizer.tokenize("你好, world!");
196 assert_eq!(got, vec!["你", "好", "world"]);
197 }
198
199 #[test]
200 fn cjk_hangul_unigrams() {
201 assert_eq!(CjkCharTokenizer.tokenize("가나"), vec!["가", "나"]);
202 }
203
204 #[test]
205 fn keyword_whole_phrase() {
206 assert_eq!(
207 KeywordTokenizer.tokenize("attention is all you need"),
208 vec!["attention is all you need"]
209 );
210 }
211
212 #[test]
213 fn keyword_empty() {
214 assert!(KeywordTokenizer.tokenize("").is_empty());
215 assert!(KeywordTokenizer.tokenize(" ").is_empty());
216 }
217
218 #[test]
219 fn keyword_trims_outer_whitespace() {
220 assert_eq!(KeywordTokenizer.tokenize(" hello "), vec!["hello"]);
221 }
222
223 #[test]
224 fn keyword_single_word() {
225 assert_eq!(KeywordTokenizer.tokenize("LoRA"), vec!["LoRA"]);
226 }
227
228 #[test]
229 fn identifier_spec_example() {
230 let t = IdentifierTokenizer::default();
231 let got = t.tokenize("LoRA attention");
233 assert_eq!(got, vec!["lora", "lo", "ra", "attention"]);
234 }
235
236 #[test]
237 fn identifier_empty() {
238 assert!(IdentifierTokenizer::default().tokenize("").is_empty());
239 }
240
241 #[test]
242 fn identifier_plain_passthrough() {
243 assert_eq!(
244 IdentifierTokenizer::default().tokenize("attention"),
245 vec!["attention"]
246 );
247 }
248
249 #[test]
250 fn identifier_snake_case() {
251 let got = IdentifierTokenizer::default().tokenize("fine_tuning");
252 assert_eq!(got, vec!["fine_tuning", "fine", "tuning"]);
253 }
254
255 #[test]
256 fn identifier_kebab() {
257 let got = IdentifierTokenizer::default().tokenize("fine-tuning");
258 assert_eq!(got, vec!["fine-tuning", "fine", "tuning"]);
259 }
260
261 #[test]
262 fn identifier_min_part_len_filters_short() {
263 let t = IdentifierTokenizer { min_part_len: 3 };
264 assert_eq!(t.tokenize("LoRA"), vec!["lora"]);
266 }
267
268 #[test]
269 fn identifier_punct_stripped() {
270 let got = IdentifierTokenizer::default().tokenize("LoRA,");
271 assert_eq!(got, vec!["lora", "lo", "ra"]);
272 }
273
274 #[test]
275 fn identifier_tokenizer_filters_unicode_min_part_len_by_chars() {
276 let t = IdentifierTokenizer { min_part_len: 2 };
279 let got = t.tokenize("foo_\u{4F60}");
280 assert!(
281 !got.contains(&"\u{4F60}".to_string()),
282 "expected unicode single-char part to be filtered; got: {got:?}"
283 );
284 }
285
286 #[cfg(feature = "unicode")]
287 #[test]
288 fn unicode_word_basic() {
289 let got = UnicodeWordTokenizer.tokenize("Hello, world!");
290 assert_eq!(got, vec!["Hello", "world"]);
291 }
292
293 #[cfg(feature = "unicode")]
294 #[test]
295 fn unicode_word_empty() {
296 assert!(UnicodeWordTokenizer.tokenize("").is_empty());
297 }
298}