readsight 1.0.1

Multilingual readability library — 86 languages, 17 formulas, TeX-based syllable counting via the Frank M. Liang algorithm.
Documentation
//! The `ReadSight` engine facade.

use std::collections::BTreeMap;
use std::rc::Rc;

use crate::config::Config;
use crate::error::Result;
use crate::formula::impls::WienerSachtextformel;
use crate::formula::{registry, FormulaRegistry, FormulaResult};
use crate::hyphenation::{parse_tex, LiangHyphenator};
use crate::language::{JsonLanguageRepository, Language};
use crate::syllable::{
    CompositeSyllableCounter, HeuristicSyllableCounter, SyllableCounterKind, TexSyllableCounter,
};
use crate::text::{TextAnalyzer, TextSplitter, TextStatistics};

/// The main entry point: build once for a language, then query it.
pub struct ReadSight {
    language: Rc<Language>,
    analyzer: TextAnalyzer,
    registry: FormulaRegistry,
}

impl ReadSight {
    /// Construct an engine for `language` using the embedded data.
    pub fn new(language: &str) -> Result<Self> {
        Self::with_config(language, Config::default_embedded())
    }

    /// Construct an engine for `language` using a custom [`Config`].
    pub fn with_config(language: &str, config: Config) -> Result<Self> {
        let mut repository = JsonLanguageRepository::new(config.clone());
        let language = Rc::new(repository.find(language)?.clone());

        let tex_contents = config.read_pattern(&language.code)?;
        let loaded = parse_tex(&tex_contents);
        let hyphenator = Rc::new(LiangHyphenator::new(
            loaded.patterns,
            loaded.exceptions,
            language.min_hyphen_left,
            language.min_hyphen_right,
        ));

        let syllable_counter = Self::load_syllable_counter(&language, &hyphenator);
        let splitter = TextSplitter::new(&language)?;
        let analyzer = TextAnalyzer::new(
            Rc::clone(&hyphenator),
            syllable_counter,
            splitter,
            Rc::clone(&language),
        );
        let registry = registry::create();

        Ok(ReadSight {
            language,
            analyzer,
            registry,
        })
    }

    fn load_syllable_counter(
        language: &Language,
        hyphenator: &Rc<LiangHyphenator>,
    ) -> SyllableCounterKind {
        let mode = language.syllable_mode.as_str();
        if mode == "tex" || language.syllable_heuristics.is_none() {
            return SyllableCounterKind::Tex(TexSyllableCounter::new(Rc::clone(hyphenator)));
        }

        let heuristic = HeuristicSyllableCounter::new(language.syllable_heuristics.as_ref());
        if mode == "heuristic" {
            return SyllableCounterKind::Heuristic(heuristic);
        }

        SyllableCounterKind::Composite(CompositeSyllableCounter::new(
            heuristic,
            TexSyllableCounter::new(Rc::clone(hyphenator)),
        ))
    }

    /// All supported language codes (sorted). `config` defaults to embedded.
    pub fn supported_languages(config: Option<&Config>) -> Vec<String> {
        let owned;
        let config = match config {
            Some(c) => c,
            None => {
                owned = Config::default_embedded();
                &owned
            }
        };
        JsonLanguageRepository::new(config.clone())
            .list_codes()
            .unwrap_or_default()
    }

    /// The parsed language.
    pub fn language(&self) -> &Language {
        &self.language
    }

    /// Formulas supported for this engine's language, in registration order.
    pub fn supported_formulas(&self) -> Vec<String> {
        self.registry.list_for_language(&self.language)
    }

    /// The formula registry.
    pub fn registry(&self) -> &FormulaRegistry {
        &self.registry
    }

    // --- Text / syllable API ---

    /// Exact TeX hyphenation split.
    pub fn split_word(&self, word: &str) -> Vec<String> {
        self.analyzer.split_word(word)
    }

    /// Syllable split (per the language's syllable mode).
    pub fn split_syllables(&self, word: &str) -> Vec<String> {
        self.analyzer.split_syllables(word)
    }

    /// Syllable count for a word.
    pub fn syllable_count(&self, word: &str) -> i64 {
        self.analyzer.syllable_count(word)
    }

    /// Word count.
    pub fn word_count(&self, text: &str) -> i64 {
        self.analyzer.word_count(text)
    }

    /// Sentence count.
    pub fn sentence_count(&self, text: &str) -> i64 {
        self.analyzer.sentence_count(text)
    }

    /// Letter count.
    pub fn letter_count(&self, text: &str) -> i64 {
        self.analyzer.letter_count(text)
    }

    /// Total syllables across a text.
    pub fn total_syllables(&self, text: &str) -> i64 {
        self.analyzer.total_syllables(text)
    }

    /// Mean syllables per word.
    pub fn average_syllables_per_word(&self, text: &str) -> f64 {
        self.analyzer.average_syllables_per_word(text)
    }

    /// Mean words per sentence.
    pub fn average_words_per_sentence(&self, text: &str) -> f64 {
        self.analyzer.average_words_per_sentence(text)
    }

    /// Polysyllabic word count (> 2 syllables).
    pub fn polysyllable_count(&self, text: &str, count_proper_nouns: bool) -> i64 {
        self.analyzer.polysyllable_count(text, count_proper_nouns)
    }

    /// Words with more than `n` syllables.
    pub fn words_with_more_than_n_syllables(
        &self,
        text: &str,
        n: i64,
        count_proper_nouns: bool,
    ) -> i64 {
        self.analyzer
            .words_with_more_than_n_syllables(text, n, count_proper_nouns)
    }

    /// Syllable histogram (ascending keys).
    pub fn histogram_syllables(&self, text: &str) -> BTreeMap<i64, i64> {
        self.analyzer.histogram_syllables(text)
    }

    /// Full statistics bundle (errors on empty text).
    pub fn analyze(&self, text: &str) -> Result<TextStatistics> {
        self.analyzer.analyze(text)
    }

    /// Register user hyphenation overrides (`word => hyphenated-form`).
    pub fn add_hyphenations<I, K, V>(&self, hyphenations: I)
    where
        I: IntoIterator<Item = (K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
        self.analyzer.add_hyphenations(hyphenations);
    }

    // --- Formula API ---

    /// Evaluate a formula by name.
    pub fn score(&self, formula_name: &str, text: &str) -> Result<FormulaResult> {
        let stats = self.analyze(text)?;
        self.registry
            .calculate(formula_name, &self.language, &stats)
    }

    /// Flesch Reading Ease.
    pub fn flesch_reading_ease(&self, text: &str) -> Result<FormulaResult> {
        self.score("flesch_reading_ease", text)
    }

    /// Flesch-Kincaid Grade Level.
    pub fn flesch_kincaid_grade_level(&self, text: &str) -> Result<FormulaResult> {
        self.score("flesch_kincaid_grade_level", text)
    }

    /// Gunning Fog.
    pub fn gunning_fog(&self, text: &str) -> Result<FormulaResult> {
        self.score("gunning_fog", text)
    }

    /// SMOG Index.
    pub fn smog_index(&self, text: &str) -> Result<FormulaResult> {
        self.score("smog", text)
    }

    /// Coleman-Liau.
    pub fn coleman_liau(&self, text: &str) -> Result<FormulaResult> {
        self.score("coleman_liau", text)
    }

    /// Automated Readability Index.
    pub fn automated_readability_index(&self, text: &str) -> Result<FormulaResult> {
        self.score("ari", text)
    }

    /// LIX.
    pub fn lix(&self, text: &str) -> Result<FormulaResult> {
        self.score("lix", text)
    }

    /// Gulpease.
    pub fn gulpease(&self, text: &str) -> Result<FormulaResult> {
        self.score("gulpease", text)
    }

    /// Fernández-Huerta.
    pub fn fernandez_huerta(&self, text: &str) -> Result<FormulaResult> {
        self.score("fernandez_huerta", text)
    }

    /// Szigriszt-Pazos.
    pub fn szigriszt_pazos(&self, text: &str) -> Result<FormulaResult> {
        self.score("szigriszt_pazos", text)
    }

    /// Gutiérrez-Polini.
    pub fn gutierrez_polini(&self, text: &str) -> Result<FormulaResult> {
        self.score("gutierrez_polini", text)
    }

    /// Crawford.
    pub fn crawford(&self, text: &str) -> Result<FormulaResult> {
        self.score("crawford", text)
    }

    /// FOG-PL.
    pub fn fog_pl(&self, text: &str) -> Result<FormulaResult> {
        self.score("fog_pl", text)
    }

    /// Dale-Chall.
    pub fn dale_chall(&self, text: &str) -> Result<FormulaResult> {
        self.score("dale_chall", text)
    }

    /// Spache.
    pub fn spache(&self, text: &str) -> Result<FormulaResult> {
        self.score("spache", text)
    }

    /// OSMAN (Arabic).
    pub fn osman(&self, text: &str) -> Result<FormulaResult> {
        self.score("osman", text)
    }

    /// Wiener Sachtextformel with the given variant (1..=4).
    pub fn wiener_sachtextformel(&self, text: &str, variant: i32) -> Result<FormulaResult> {
        let stats = self.analyze(text)?;
        WienerSachtextformel.calculate_variant(&stats, &self.language, variant)
    }
}