cranpose_text/
hyphenator.rs1use std::{io::Read, path::Path};
2
3use foldhash::HashMap;
4use hyphenation::{Hyphenator as _, Language, Load, Standard};
5
6#[derive(thiserror::Error, Debug)]
8pub enum HyphenationError {
9 #[error("no hyphenation patterns for the locale {0}")]
11 UnsupportedLocale(String),
12 #[error("hyphenation dictionary for {locale}: {message}")]
14 LoadFailed {
15 locale: String,
17 message: String,
19 },
20}
21
22#[derive(Default)]
27pub struct Hyphenator {
28 dictionaries: HashMap<Language, Option<Standard>>,
30}
31
32impl Hyphenator {
33 pub fn register_dictionary(
36 &mut self,
37 locale: &str,
38 reader: &mut impl Read,
39 ) -> Result<(), HyphenationError> {
40 let language = language(locale)
41 .ok_or_else(|| HyphenationError::UnsupportedLocale(locale.to_owned()))?;
42 let dictionary = Standard::from_reader(language, reader).map_err(|error| {
43 HyphenationError::LoadFailed {
44 locale: locale.to_owned(),
45 message: error.to_string(),
46 }
47 })?;
48 self.dictionaries.insert(language, Some(dictionary));
49 Ok(())
50 }
51
52 pub fn register_dictionary_path(
55 &mut self,
56 locale: &str,
57 path: impl AsRef<Path>,
58 ) -> Result<(), HyphenationError> {
59 let mut file = std::fs::File::open(path).map_err(|error| HyphenationError::LoadFailed {
60 locale: locale.to_owned(),
61 message: error.to_string(),
62 })?;
63 self.register_dictionary(locale, &mut file)
64 }
65
66 pub fn break_points(&mut self, locale: Option<&str>, word: &str, points: &mut Vec<usize>) {
71 let Some(language) = language(locale.unwrap_or_default()) else {
72 return;
73 };
74 let dictionary = self
75 .dictionaries
76 .entry(language)
77 .or_insert_with(|| embedded(language));
78 if let Some(dictionary) = dictionary {
79 points.extend(dictionary.hyphenate(word).breaks);
80 }
81 }
82}
83
84#[cfg(feature = "hyphenation-embedded")]
85fn embedded(language: Language) -> Option<Standard> {
86 Standard::from_embedded(language).ok()
87}
88
89#[cfg(not(feature = "hyphenation-embedded"))]
90fn embedded(_language: Language) -> Option<Standard> {
91 None
92}
93
94fn language(tag: &str) -> Option<Language> {
98 let tag = tag.trim().replace('_', "-").to_ascii_lowercase();
99 if let Some(language) = Language::try_from_code(&tag) {
100 return Some(language);
101 }
102 let primary = tag.split('-').next().unwrap_or_default();
103 Language::try_from_code(primary).or_else(|| {
104 Some(match primary {
105 "" | "und" | "en" => Language::EnglishUS,
106 "de" => Language::German1996,
107 "el" => Language::GreekMono,
108 "mn" => Language::Mongolian,
109 "sr" if tag.contains("latn") => Language::SerbocroatianLatin,
110 "sr" => Language::SerbianCyrillic,
111 "no" => Language::NorwegianBokmal,
112 _ => return None,
113 })
114 })
115}