use hyphenation::{Hyphenator, Language, Load, Standard};
use rustyfi_backend::HyphenLang;
use std::collections::HashMap;
use std::io::Cursor;
use std::sync::{Arc, Mutex, OnceLock};
fn language_of(tag: HyphenLang) -> Language {
match tag {
HyphenLang::EnglishUS => Language::EnglishUS,
HyphenLang::EnglishGB => Language::EnglishGB,
}
}
const EN_GB_STANDARD_BINCODE: &[u8] = include_bytes!("hyph-data/en-gb.standard.bincode");
fn cache() -> &'static Mutex<HashMap<HyphenLang, Arc<Standard>>> {
static DICTS: OnceLock<Mutex<HashMap<HyphenLang, Arc<Standard>>>> = OnceLock::new();
DICTS.get_or_init(|| Mutex::new(HashMap::new()))
}
fn dict(tag: HyphenLang) -> Option<Arc<Standard>> {
let mut map = cache().lock().unwrap();
if let Some(d) = map.get(&tag) {
return Some(Arc::clone(d));
}
let standard = match tag {
HyphenLang::EnglishUS => Standard::from_embedded(language_of(tag)).ok()?,
HyphenLang::EnglishGB => {
Standard::from_reader(language_of(tag), &mut Cursor::new(EN_GB_STANDARD_BINCODE))
.ok()?
}
};
let arc = Arc::new(standard);
map.insert(tag, Arc::clone(&arc));
Some(arc)
}
pub fn hyphenate_word(
tag: HyphenLang,
word: &str,
left_min: usize,
right_min: usize,
) -> Vec<usize> {
let Some(dict) = dict(tag) else {
return Vec::new();
};
if word.is_empty() {
return Vec::new();
}
let hyphenated = dict.hyphenate(word);
if hyphenated.breaks.is_empty() {
return Vec::new();
}
let mut char_offsets: Vec<usize> = word.char_indices().map(|(b, _)| b).collect();
char_offsets.push(word.len());
let n_chars = char_offsets.len() - 1;
hyphenated
.breaks
.iter()
.filter_map(|&b| char_offsets.iter().position(|&x| x == b))
.filter(|&char_idx| char_idx >= left_min && n_chars - char_idx >= right_min)
.collect()
}
pub(crate) fn strip_soft_hyphens(word: &str) -> (String, Vec<usize>) {
if !word.contains('\u{ad}') {
return (word.to_string(), Vec::new());
}
let mut clean = String::with_capacity(word.len());
let mut breaks = Vec::new();
let mut char_idx = 0usize;
for c in word.chars() {
if c == '\u{ad}' {
breaks.push(char_idx);
} else {
clean.push(c);
char_idx += 1;
}
}
(clean, breaks)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hyphenation_breaks_into_expected_fragments() {
let breaks = hyphenate_word(HyphenLang::EnglishUS, "hyphenation", 3, 2);
assert!(!breaks.is_empty(), "expected at least one break");
let chars: Vec<char> = "hyphenation".chars().collect();
let mut prev = 0;
let mut fragments = Vec::new();
for &b in &breaks {
fragments.push(chars[prev..b].iter().collect::<String>());
prev = b;
}
fragments.push(chars[prev..].iter().collect::<String>());
assert_eq!(fragments.join(""), "hyphenation");
for f in &fragments {
assert!(!f.is_empty());
}
}
#[test]
fn anfractuous_matches_the_crate_doc_example() {
let breaks = hyphenate_word(HyphenLang::EnglishUS, "anfractuous", 0, 0);
assert_eq!(breaks, vec![2, 6, 8]);
}
#[test]
fn short_word_yields_no_breaks_under_min_fragment_filter() {
let breaks = hyphenate_word(HyphenLang::EnglishUS, "word", 3, 2);
assert!(breaks.is_empty());
}
#[test]
fn dict_is_loaded_once_and_shared() {
let a = dict(HyphenLang::EnglishUS).expect("embedded en-US dictionary");
let b = dict(HyphenLang::EnglishUS).expect("embedded en-US dictionary");
assert!(Arc::ptr_eq(&a, &b), "expected the same cached Arc instance");
}
#[test]
fn en_gb_dict_is_loaded_once_and_shared() {
let a = dict(HyphenLang::EnglishGB).expect("vendored en-GB dictionary");
let b = dict(HyphenLang::EnglishGB).expect("vendored en-GB dictionary");
assert!(Arc::ptr_eq(&a, &b), "expected the same cached Arc instance");
}
#[test]
fn en_gb_hyphenates_a_word_differently_from_en_us_proving_the_gb_dictionary_is_used() {
let us_breaks = hyphenate_word(HyphenLang::EnglishUS, "photography", 3, 2);
let gb_breaks = hyphenate_word(HyphenLang::EnglishGB, "photography", 3, 2);
assert_eq!(us_breaks, vec![3, 6, 8], "en-US: pho-tog-ra-phy");
assert_eq!(gb_breaks, vec![3, 5], "en-GB: pho-togra-phy");
assert_ne!(
us_breaks, gb_breaks,
"en-US and en-GB must disagree on this word, or this test isn't proving anything"
);
}
#[test]
fn strip_soft_hyphens_extracts_a_single_explicit_break() {
let (clean, breaks) = strip_soft_hyphens("hy\u{ad}phenation");
assert_eq!(clean, "hyphenation");
assert_eq!(breaks, vec![2]);
}
#[test]
fn strip_soft_hyphens_extracts_multiple_explicit_breaks_in_order() {
let (clean, breaks) = strip_soft_hyphens("a\u{ad}b\u{ad}c\u{ad}d");
assert_eq!(clean, "abcd");
assert_eq!(breaks, vec![1, 2, 3]);
}
#[test]
fn strip_soft_hyphens_is_a_no_op_when_no_soft_hyphen_is_present() {
let (clean, breaks) = strip_soft_hyphens("hyphenation");
assert_eq!(clean, "hyphenation");
assert!(breaks.is_empty());
}
}