use std::{collections::HashMap, io::Read, path::Path};
use hyphenation::{Hyphenator as _, Language, Load, Standard};
#[derive(thiserror::Error, Debug)]
pub enum HyphenationError {
#[error("no hyphenation patterns for the locale {0}")]
UnsupportedLocale(String),
#[error("hyphenation dictionary for {locale}: {message}")]
LoadFailed {
locale: String,
message: String,
},
}
#[derive(Default)]
pub struct Hyphenator {
dictionaries: HashMap<Language, Option<Standard>>,
}
impl Hyphenator {
pub fn register_dictionary(
&mut self,
locale: &str,
reader: &mut impl Read,
) -> Result<(), HyphenationError> {
let language = language(locale)
.ok_or_else(|| HyphenationError::UnsupportedLocale(locale.to_owned()))?;
let dictionary = Standard::from_reader(language, reader).map_err(|error| {
HyphenationError::LoadFailed {
locale: locale.to_owned(),
message: error.to_string(),
}
})?;
self.dictionaries.insert(language, Some(dictionary));
Ok(())
}
pub fn register_dictionary_path(
&mut self,
locale: &str,
path: impl AsRef<Path>,
) -> Result<(), HyphenationError> {
let mut file = std::fs::File::open(path).map_err(|error| HyphenationError::LoadFailed {
locale: locale.to_owned(),
message: error.to_string(),
})?;
self.register_dictionary(locale, &mut file)
}
pub fn break_points(&mut self, locale: Option<&str>, word: &str, points: &mut Vec<usize>) {
let Some(language) = language(locale.unwrap_or_default()) else {
return;
};
let dictionary = self
.dictionaries
.entry(language)
.or_insert_with(|| embedded(language));
if let Some(dictionary) = dictionary {
points.extend(dictionary.hyphenate(word).breaks);
}
}
}
#[cfg(feature = "hyphenation-embedded")]
fn embedded(language: Language) -> Option<Standard> {
Standard::from_embedded(language).ok()
}
#[cfg(not(feature = "hyphenation-embedded"))]
fn embedded(_language: Language) -> Option<Standard> {
None
}
fn language(tag: &str) -> Option<Language> {
let tag = tag.trim().replace('_', "-").to_ascii_lowercase();
if let Some(language) = Language::try_from_code(&tag) {
return Some(language);
}
let primary = tag.split('-').next().unwrap_or_default();
Language::try_from_code(primary).or_else(|| {
Some(match primary {
"" | "und" | "en" => Language::EnglishUS,
"de" => Language::German1996,
"el" => Language::GreekMono,
"mn" => Language::Mongolian,
"sr" if tag.contains("latn") => Language::SerbocroatianLatin,
"sr" => Language::SerbianCyrillic,
"no" => Language::NorwegianBokmal,
_ => return None,
})
})
}