readsight 1.0.1

Multilingual readability library — 86 languages, 17 formulas, TeX-based syllable counting via the Frank M. Liang algorithm.
Documentation
//! The Frank M. Liang (TeX) hyphenation algorithm.

use std::cell::RefCell;
use std::collections::HashMap;

use super::exceptions::HyphenationExceptionsCollection;
use super::pattern::PatternsCollection;

/// A word hyphenator.
pub trait Hyphenator {
    /// Split a word into syllable parts.
    fn hyphenate(&self, word: &str) -> Vec<String>;
    /// Count the syllables in a word.
    fn count_syllables(&self, word: &str) -> i64;
}

/// Liang's pattern-based hyphenator.
pub struct LiangHyphenator {
    patterns: PatternsCollection,
    exceptions: HyphenationExceptionsCollection,
    min_hyphen_left: i32,
    min_hyphen_right: i32,
    user_hyphenations: RefCell<HashMap<String, String>>,
}

impl LiangHyphenator {
    /// Construct a hyphenator.
    pub fn new(
        patterns: PatternsCollection,
        exceptions: HyphenationExceptionsCollection,
        min_hyphen_left: i32,
        min_hyphen_right: i32,
    ) -> Self {
        LiangHyphenator {
            patterns,
            exceptions,
            min_hyphen_left,
            min_hyphen_right,
            user_hyphenations: RefCell::new(HashMap::new()),
        }
    }

    /// Register user hyphenation overrides (`word => hyphenated-form`), both
    /// lowercased.
    pub fn add_hyphenations<I, K, V>(&self, hyphenations: I)
    where
        I: IntoIterator<Item = (K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
        let mut map = self.user_hyphenations.borrow_mut();
        for (word, hyphenated) in hyphenations {
            map.insert(
                word.as_ref().to_lowercase(),
                hyphenated.as_ref().to_lowercase(),
            );
        }
    }

    fn split_by_hyphenation(hyphenated: &str, original_word: &str) -> Vec<String> {
        let orig: Vec<char> = original_word.chars().collect();
        let mut parts: Vec<String> = Vec::new();
        let mut part = String::new();
        let mut j = 0usize;

        for ch in hyphenated.chars() {
            if ch == '-' {
                parts.push(std::mem::take(&mut part));
            } else {
                if j < orig.len() {
                    part.push(orig[j]);
                }
                j += 1;
            }
        }

        if !part.is_empty() {
            parts.push(part);
        }

        parts
    }

    fn split_by_patterns(&self, word: &str, word_len: usize, word_lower: &str) -> Vec<String> {
        #![allow(clippy::needless_range_loop)]
        let left = self.min_hyphen_left.max(0) as usize;
        let right = self.min_hyphen_right.max(0) as usize;

        let text = format!(".{word_lower}.");
        let text_chars: Vec<char> = text.chars().collect();
        let text_length = word_len + 2;

        let mut pattern_length = self.patterns.max_length();
        if pattern_length > text_length {
            pattern_length = text_length;
        }

        let mut scores: Vec<Option<i32>> = vec![None; text_length + 1];

        let end = text_length.saturating_sub(right);
        for start in 0..end {
            let max_len = pattern_length.min(text_length - start);
            for len in 1..=max_len {
                let subword: String = text_chars[start..start + len].iter().collect();
                if let Some(weights) = self.patterns.get_weights(&subword) {
                    for (offset, &w) in weights.iter().enumerate() {
                        let idx = start + offset;
                        if scores[idx].map_or(true, |s| w > s) {
                            scores[idx] = Some(w);
                        }
                    }
                }
            }
        }

        let word_chars: Vec<char> = word.chars().collect();
        let mut parts: Vec<String> = Vec::new();
        let mut part: String = word_chars[0..left.min(word_chars.len())].iter().collect();
        let break_end = text_length.saturating_sub(right);

        for i in (left + 1)..break_end {
            if let Some(score) = scores[i] {
                if score & 1 != 0 {
                    parts.push(std::mem::take(&mut part));
                }
            }
            if let Some(&c) = word_chars.get(i - 1) {
                part.push(c);
            }
        }

        for i in break_end..(text_length - 1) {
            if let Some(&c) = word_chars.get(i - 1) {
                part.push(c);
            }
        }

        if !part.is_empty() {
            parts.push(part);
        }

        parts
    }
}

impl Hyphenator for LiangHyphenator {
    fn hyphenate(&self, word: &str) -> Vec<String> {
        let word_length = word.chars().count();

        if word_length == 0 {
            return Vec::new();
        }

        if (word_length as i32) < self.min_hyphen_left + self.min_hyphen_right {
            return vec![word.to_string()];
        }

        let word_lower = word.to_lowercase();

        if let Some(h) = self.user_hyphenations.borrow().get(&word_lower) {
            return Self::split_by_hyphenation(h, word);
        }

        if let Some(h) = self.exceptions.get(&word_lower) {
            return Self::split_by_hyphenation(h, word);
        }

        self.split_by_patterns(word, word_length, &word_lower)
    }

    fn count_syllables(&self, word: &str) -> i64 {
        let parts = self.hyphenate(word);
        if parts.is_empty() {
            0
        } else {
            parts.len() as i64
        }
    }
}