Skip to main content

cranpose_text/
hyphenator.rs

1use std::{io::Read, path::Path};
2
3use foldhash::HashMap;
4use hyphenation::{Hyphenator as _, Language, Load, Standard};
5
6/// Why a hyphenation dictionary could not be registered.
7#[derive(thiserror::Error, Debug)]
8pub enum HyphenationError {
9    /// The locale tag names no language that has hyphenation patterns.
10    #[error("no hyphenation patterns for the locale {0}")]
11    UnsupportedLocale(String),
12    /// The dictionary could not be read or parsed.
13    #[error("hyphenation dictionary for {locale}: {message}")]
14    LoadFailed {
15        /// The locale tag as it was given.
16        locale: String,
17        /// What the dictionary loader reported.
18        message: String,
19    },
20}
21
22/// Hyphenation dictionaries by language, for `Hyphens.Auto`. A dictionary
23/// the app registers comes first; with the `hyphenation-embedded` feature,
24/// the one built in for a language loads the first time text in it
25/// hyphenates.
26#[derive(Default)]
27pub struct Hyphenator {
28    // `None` for a language with no dictionary, so it is looked for once.
29    dictionaries: HashMap<Language, Option<Standard>>,
30}
31
32impl Hyphenator {
33    /// Registers the dictionary for the BCP 47 tag `locale` read from
34    /// `reader`, in place of an earlier one.
35    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    /// Registers the dictionary for the BCP 47 tag `locale` read from the
53    /// file at `path`, in place of an earlier one.
54    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    /// Appends the byte offsets inside `word` where it may end a line with a
67    /// hyphen, ascending, by the dictionary of the BCP 47 tag `locale`,
68    /// American English when there is none. Appends none when no dictionary
69    /// of that language is registered or built in.
70    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
94// The patterns' language of a BCP 47 tag: the tag itself when the patterns
95// carry it, else its language subtag, else that language's usual patterns.
96// No tag, or the undetermined `und`, means American English.
97fn 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}