1#[cfg(feature = "text_layout_hyphenation")]
37use hyphenation::Language as HyphenationLanguage;
38#[cfg(feature = "text_layout_hyphenation")]
39pub use hyphenation::Language;
40
41#[cfg(not(feature = "text_layout_hyphenation"))]
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45#[allow(dead_code)]
46pub enum Language {
47 EnglishUS,
49 French,
50 German1996,
51 Spanish,
52 Portuguese,
53 Estonian,
54 Hungarian,
55 Polish,
56 Czech,
57 Slovak,
58 Latvian,
59 Lithuanian,
60 Romanian,
61 Turkish,
62 Croatian,
63 Icelandic,
64 Welsh,
65 NorwegianBokmal,
66 Swedish,
67 Russian,
69 Ukrainian,
70 Belarusian,
71 Bulgarian,
72 Macedonian,
73 SerbianCyrillic,
74 Mongolian,
75 SlavonicChurch,
76 GreekMono,
78 GreekPoly,
79 Coptic,
80 Hindi,
82 Bengali,
83 Assamese,
84 Marathi,
85 Sanskrit,
86 Gujarati,
87 Panjabi,
88 Kannada,
89 Malayalam,
90 Oriya,
91 Tamil,
92 Telugu,
93 Georgian,
95 Ethiopic,
96 Thai,
97 Chinese,
98}
99
100#[derive(PartialEq, Eq, Debug, Clone, Copy)]
101pub enum Script {
102 Arabic,
104 Bengali,
105 Cyrillic,
106 Devanagari,
107 Ethiopic,
108 Georgian,
109 Greek,
110 Gujarati,
111 Gurmukhi,
112 Hangul,
113 Hebrew,
114 Hiragana,
115 Kannada,
116 Katakana,
117 Khmer,
118 Latin,
119 Malayalam,
120 Mandarin,
121 Myanmar,
122 Oriya,
123 Sinhala,
124 Tamil,
125 Telugu,
126 Thai,
127}
128
129#[inline]
133#[must_use] pub const fn is_stop_char(ch: char) -> bool {
134 matches!(ch, '\u{0000}'..='\u{0040}' | '\u{005B}'..='\u{0060}' | '\u{007B}'..='\u{007E}')
135}
136
137type ScriptChecker = (Script, fn(char) -> bool);
138type ScriptCounter = (Script, fn(char) -> bool, usize);
139
140const SCRIPT_CHECKERS: [ScriptChecker; 24] = [
141 (Script::Latin, is_latin),
142 (Script::Cyrillic, is_cyrillic),
143 (Script::Arabic, is_arabic),
144 (Script::Mandarin, is_mandarin),
145 (Script::Devanagari, is_devanagari),
146 (Script::Hebrew, is_hebrew),
147 (Script::Ethiopic, is_ethiopic),
148 (Script::Georgian, is_georgian),
149 (Script::Bengali, is_bengali),
150 (Script::Hangul, is_hangul),
151 (Script::Hiragana, is_hiragana),
152 (Script::Katakana, is_katakana),
153 (Script::Greek, is_greek),
154 (Script::Kannada, is_kannada),
155 (Script::Tamil, is_tamil),
156 (Script::Thai, is_thai),
157 (Script::Gujarati, is_gujarati),
158 (Script::Gurmukhi, is_gurmukhi),
159 (Script::Telugu, is_telugu),
160 (Script::Malayalam, is_malayalam),
161 (Script::Oriya, is_oriya),
162 (Script::Myanmar, is_myanmar),
163 (Script::Sinhala, is_sinhala),
164 (Script::Khmer, is_khmer),
165];
166
167pub fn detect_script(text: &str) -> Option<Script> {
172 let mut script_counters: [ScriptCounter; 24] = SCRIPT_CHECKERS.map(|(s, f)| (s, f, 0));
173
174 let half = text.chars().count() / 2;
175
176 for ch in text.chars() {
177 if is_stop_char(ch) {
178 continue;
179 }
180
181 for i in 0..script_counters.len() {
184 let found = {
185 let (script, check_fn, ref mut count) = script_counters[i];
186 if check_fn(ch) {
187 *count += 1;
188 if *count > half {
189 return Some(script);
190 }
191 true
192 } else {
193 false
194 }
195 };
196 if found {
199 if i > 0 {
203 script_counters.swap(i - 1, i);
204 }
205 break;
206 }
207 }
208 }
209
210 let (script, _, count) = script_counters
211 .iter()
212 .copied()
213 .max_by_key(|&(_, _, count)| count)
214 .unwrap();
215 if count != 0 {
216 Some(script)
217 } else {
218 None
219 }
220}
221
222#[must_use] pub fn detect_char_script(ch: char) -> Option<Script> {
223 for &(script, check_fn) in &SCRIPT_CHECKERS {
224 if check_fn(ch) {
225 return Some(script);
226 }
227 }
228 None
229}
230
231fn detect_bengali_language(text: &str) -> Language {
233 for c in text.chars() {
234 if matches!(c, '\u{09F0}' | '\u{09F1}') {
237 return Language::Assamese;
239 }
240 }
241 Language::Bengali
243}
244
245fn detect_cyrillic_language(text: &str) -> Language {
246 for c in text.chars() {
247 match c {
248 '\u{0460}'..='\u{047F}' => return Language::SlavonicChurch,
250 'ѓ' | 'ќ' | 'ѕ' => return Language::Macedonian,
253 'ў' => return Language::Belarusian,
254 'є' | 'і' | 'ї' | 'ґ' => return Language::Ukrainian,
255 'ө' | 'ү' | 'һ' => return Language::Mongolian,
256 'ј' | 'љ' | 'њ' | 'ћ' | 'ђ' | 'џ' => return Language::SerbianCyrillic,
257 'щ' => return Language::Bulgarian,
260 _ => {}
261 }
262 }
263
264 Language::Russian
265}
266
267fn detect_devanagari_language(text: &str) -> Language {
268 for c in text.chars() {
269 match c {
270 '\u{0933}' => return Language::Marathi, '\u{1CD0}'..='\u{1CFF}' => return Language::Sanskrit,
274 _ => (),
275 }
276 }
277
278 Language::Hindi
279}
280
281fn detect_greek_language(text: &str) -> Language {
282 for c in text.chars() {
283 match c {
284 '\u{2C80}'..='\u{2CFF}' => return Language::Coptic,
286 '\u{1F00}'..='\u{1FFF}' => return Language::GreekPoly,
288 _ => {}
289 }
290 }
291
292 Language::GreekMono
293}
294
295#[allow(clippy::match_same_arms)] fn detect_latin_language(text: &str) -> Language {
297 let mut has_french_c = false;
299 let mut has_portuguese_o = false;
300 let mut has_portuguese_a = false;
301
302 for c in text.chars() {
303 match c {
304 'ß' => return Language::German1996,
306 'ő' | 'ű' => return Language::Hungarian,
307 'ł' => return Language::Polish,
308 'ř' | 'ů' => return Language::Czech,
309 'ľ' | 'ĺ' | 'ŕ' => return Language::Slovak,
310 'ā' | 'ē' | 'ģ' | 'ī' | 'ķ' | 'ļ' | 'ņ' | 'ō' | 'ū' => {
311 return Language::Latvian
312 }
313 'ą' | 'ę' | 'ė' | 'į' | 'ų' => return Language::Lithuanian,
314 'ă' | 'ș' | 'ț' => return Language::Romanian,
315 'ğ' | 'ı' | 'ş' => return Language::Turkish,
316 'đ' => return Language::Croatian, 'þ' | 'ð' => return Language::Icelandic,
319 'ŵ' | 'ŷ' => return Language::Welsh,
320 'æ' | 'ø' => return Language::NorwegianBokmal, 'å' => return Language::Swedish, 'ñ' => return Language::Spanish,
323 'ä' | 'ö' | 'ü' => return Language::German1996,
324
325 'õ' => has_portuguese_o = true,
328 'ã' => has_portuguese_a = true,
329
330 'ç' => has_french_c = true, 'á' | 'é' | 'í' | 'ó' | 'ú' => return Language::Spanish,
333
334 _ => (),
335 }
336 }
337
338 if has_french_c && !has_portuguese_o && !has_portuguese_a {
341 return Language::French;
342 }
343
344 if has_portuguese_o && !has_french_c && !has_portuguese_a {
345 return Language::Estonian;
346 }
347
348 if has_portuguese_o || has_portuguese_a || has_french_c {
349 return Language::Portuguese;
350 }
351
352 Language::EnglishUS
353}
354
355#[allow(clippy::match_same_arms)] #[must_use] pub fn script_to_language(script: Script, text: &str) -> Language {
357 match script {
358 Script::Ethiopic => Language::Ethiopic,
359 Script::Georgian => Language::Georgian,
360 Script::Gujarati => Language::Gujarati,
361 Script::Gurmukhi => Language::Panjabi,
362 Script::Kannada => Language::Kannada,
363 Script::Malayalam => Language::Malayalam,
364 Script::Mandarin => Language::Chinese,
365 Script::Oriya => Language::Oriya,
366 Script::Tamil => Language::Tamil,
367 Script::Telugu => Language::Telugu,
368 Script::Thai => Language::Thai,
369 Script::Bengali => detect_bengali_language(text),
370 Script::Cyrillic => detect_cyrillic_language(text),
371 Script::Devanagari => detect_devanagari_language(text),
372 Script::Greek => detect_greek_language(text),
373 Script::Latin => detect_latin_language(text),
374
375 Script::Myanmar => Language::Thai,
377 Script::Khmer => Language::Thai,
378 Script::Sinhala => Language::Hindi,
379
380 Script::Arabic => Language::Chinese,
382 Script::Hebrew => Language::Chinese,
383 Script::Hangul => Language::Chinese,
384 Script::Hiragana => Language::Chinese,
385 Script::Katakana => Language::Chinese,
386 }
387}
388
389#[must_use] pub const fn is_cyrillic(ch: char) -> bool {
390 matches!(ch,
391 '\u{0400}'..='\u{0484}'
392 | '\u{0487}'..='\u{052F}'
393 | '\u{2DE0}'..='\u{2DFF}'
394 | '\u{A640}'..='\u{A69D}'
395 | '\u{1D2B}'
396 | '\u{1D78}'
397 | '\u{A69F}'
398 )
399}
400
401#[must_use] pub const fn is_latin(ch: char) -> bool {
403 matches!(ch,
404 'a'..='z'
405 | 'A'..='Z'
406 | '\u{0080}'..='\u{00FF}'
407 | '\u{0100}'..='\u{017F}'
408 | '\u{0180}'..='\u{024F}'
409 | '\u{0250}'..='\u{02AF}'
410 | '\u{1D00}'..='\u{1D7F}'
411 | '\u{1D80}'..='\u{1DBF}'
412 | '\u{1E00}'..='\u{1EFF}'
413 | '\u{2100}'..='\u{214F}'
414 | '\u{2C60}'..='\u{2C7F}'
415 | '\u{A720}'..='\u{A7FF}'
416 | '\u{AB30}'..='\u{AB6F}'
417 )
418}
419
420#[must_use] pub const fn is_arabic(ch: char) -> bool {
422 matches!(ch,
423 '\u{0600}'..='\u{06FF}'
424 | '\u{0750}'..='\u{07FF}'
425 | '\u{08A0}'..='\u{08FF}'
426 | '\u{FB50}'..='\u{FDFF}'
427 | '\u{FE70}'..='\u{FEFF}'
428 | '\u{10E60}'..='\u{10E7F}'
429 | '\u{1EE00}'..='\u{1EEFF}'
430 )
431}
432
433#[must_use] pub const fn is_devanagari(ch: char) -> bool {
435 matches!(ch, '\u{0900}'..='\u{097F}' | '\u{A8E0}'..='\u{A8FF}' | '\u{1CD0}'..='\u{1CFF}')
436}
437
438#[must_use] pub const fn is_ethiopic(ch: char) -> bool {
440 matches!(ch, '\u{1200}'..='\u{139F}' | '\u{2D80}'..='\u{2DDF}' | '\u{AB00}'..='\u{AB2F}')
441}
442
443#[must_use] pub const fn is_hebrew(ch: char) -> bool {
445 matches!(ch, '\u{0590}'..='\u{05FF}')
446}
447
448#[must_use] pub const fn is_georgian(ch: char) -> bool {
449 matches!(ch, '\u{10A0}'..='\u{10FF}')
450}
451
452#[must_use] pub const fn is_mandarin(ch: char) -> bool {
453 matches!(ch,
454 '\u{2E80}'..='\u{2E99}'
455 | '\u{2E9B}'..='\u{2EF3}'
456 | '\u{2F00}'..='\u{2FD5}'
457 | '\u{3005}'
458 | '\u{3007}'
459 | '\u{3021}'..='\u{3029}'
460 | '\u{3038}'..='\u{303B}'
461 | '\u{3400}'..='\u{4DB5}'
462 | '\u{4E00}'..='\u{9FCC}'
463 | '\u{F900}'..='\u{FA6D}'
464 | '\u{FA70}'..='\u{FAD9}'
465 )
466}
467
468#[must_use] pub const fn is_bengali(ch: char) -> bool {
469 matches!(ch, '\u{0980}'..='\u{09FF}')
470}
471
472#[must_use] pub const fn is_hiragana(ch: char) -> bool {
473 matches!(ch, '\u{3040}'..='\u{309F}')
474}
475
476#[must_use] pub const fn is_katakana(ch: char) -> bool {
477 matches!(ch,
478 '\u{30A0}'..='\u{30FF}'
479 | '\u{FF66}'..='\u{FF9F}'
482 )
483}
484
485#[must_use] pub const fn is_hangul(ch: char) -> bool {
487 matches!(ch,
488 '\u{AC00}'..='\u{D7AF}'
489 | '\u{1100}'..='\u{11FF}'
490 | '\u{3130}'..='\u{318F}'
491 | '\u{A960}'..='\u{A97F}'
492 | '\u{D7B0}'..='\u{D7FF}'
493 | '\u{FFA0}'..='\u{FFDC}'
499 )
500}
501
502#[must_use] pub const fn is_greek(ch: char) -> bool {
504 matches!(ch, '\u{0370}'..='\u{03FF}')
505}
506
507#[must_use] pub const fn is_kannada(ch: char) -> bool {
509 matches!(ch, '\u{0C80}'..='\u{0CFF}')
510}
511
512#[must_use] pub const fn is_tamil(ch: char) -> bool {
514 matches!(ch, '\u{0B80}'..='\u{0BFF}')
515}
516
517#[must_use] pub const fn is_thai(ch: char) -> bool {
519 matches!(ch, '\u{0E00}'..='\u{0E7F}')
520}
521
522#[must_use] pub const fn is_gujarati(ch: char) -> bool {
524 matches!(ch, '\u{0A80}'..='\u{0AFF}')
525}
526
527#[must_use] pub const fn is_gurmukhi(ch: char) -> bool {
530 matches!(ch, '\u{0A00}'..='\u{0A7F}')
531}
532
533#[must_use] pub const fn is_telugu(ch: char) -> bool {
534 matches!(ch, '\u{0C00}'..='\u{0C7F}')
535}
536
537#[must_use] pub const fn is_malayalam(ch: char) -> bool {
539 matches!(ch, '\u{0D00}'..='\u{0D7F}')
540}
541
542#[must_use] pub const fn is_oriya(ch: char) -> bool {
544 matches!(ch, '\u{0B00}'..='\u{0B7F}')
545}
546
547#[must_use] pub const fn is_myanmar(ch: char) -> bool {
549 matches!(ch, '\u{1000}'..='\u{109F}')
550}
551
552#[must_use] pub const fn is_sinhala(ch: char) -> bool {
554 matches!(ch, '\u{0D80}'..='\u{0DFF}')
555}
556
557#[must_use] pub const fn is_khmer(ch: char) -> bool {
559 matches!(ch, '\u{1780}'..='\u{17FF}' | '\u{19E0}'..='\u{19FF}')
560}
561
562#[cfg(test)]
563mod script_class_tests {
564 use super::{detect_script, is_hangul, is_katakana, Script};
565
566 #[test]
567 fn halfwidth_katakana_is_not_hangul() {
568 for ch in ['\u{FF71}', '\u{FF72}', '\u{FF73}'] {
570 assert!(!is_hangul(ch), "{ch:?} wrongly matched is_hangul");
571 assert!(is_katakana(ch), "{ch:?} should match is_katakana");
572 }
573 assert_eq!(detect_script("\u{FF71}\u{FF72}\u{FF73}"), Some(Script::Katakana));
574 }
575
576 #[test]
577 fn fullwidth_latin_is_not_hangul() {
578 for ch in ['\u{FF21}', '\u{FF22}', '\u{FF23}'] {
580 assert!(!is_hangul(ch), "{ch:?} wrongly matched is_hangul");
581 }
582 assert_ne!(detect_script("\u{FF21}\u{FF22}\u{FF23}"), Some(Script::Hangul));
583 }
584
585 #[test]
586 fn real_hangul_still_detected() {
587 assert!(is_hangul('\u{AC00}')); assert_eq!(detect_script("\u{AC00}\u{AC01}"), Some(Script::Hangul));
589 }
590}