readsight 1.0.0

Multilingual readability library — 86 languages, 17 formulas, TeX-based syllable counting via the Frank M. Liang algorithm.
Documentation
//! The `Language` struct, parsed from a language JSON file.

use serde::Deserialize;
use serde_json::{Map, Value};

use super::script::Script;

#[derive(Deserialize)]
struct RawHyphenMins {
    left: i32,
    right: i32,
}

fn default_syllable_mode() -> String {
    "tex".to_string()
}

#[derive(Deserialize)]
struct RawLanguage {
    code: String,
    name: String,
    #[serde(rename = "nativeName")]
    native_name: String,
    script: Script,
    #[serde(rename = "hyphenMins")]
    hyphen_mins: RawHyphenMins,
    #[serde(rename = "letterPattern")]
    letter_pattern: String,
    #[serde(rename = "wordSplitPattern")]
    word_split_pattern: String,
    #[serde(rename = "sentenceBoundaryPattern")]
    sentence_boundary_pattern: String,
    #[serde(default)]
    formulas: Map<String, Value>,
    #[serde(rename = "syllableHeuristics", default)]
    syllable_heuristics: Option<Value>,
    #[serde(rename = "syllableMode", default = "default_syllable_mode")]
    syllable_mode: String,
}

/// A parsed language definition.
#[derive(Debug, Clone)]
pub struct Language {
    /// ISO-ish language code (e.g. `"en-us"`).
    pub code: String,
    /// English name.
    pub name: String,
    /// Native name.
    pub native_name: String,
    /// Writing system.
    pub script: Script,
    /// Minimum characters before the first hyphenation point.
    pub min_hyphen_left: i32,
    /// Minimum characters after the last hyphenation point.
    pub min_hyphen_right: i32,
    /// Regex body matching a single letter.
    pub letter_pattern: String,
    /// Regex body used to split words (matches inter-word separators).
    pub word_split_pattern: String,
    /// Regex body matching sentence boundaries.
    pub sentence_boundary_pattern: String,
    /// Per-formula coefficient configuration (`formulas` JSON block).
    pub formula_configs: Map<String, Value>,
    /// Optional syllable-heuristic configuration (`syllableHeuristics` JSON block).
    pub syllable_heuristics: Option<Value>,
    /// Syllable mode: `"tex"`, `"heuristic"`, or `"composite"`.
    pub syllable_mode: String,
}

impl Language {
    /// Parse a `Language` from its JSON representation.
    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
        let raw: RawLanguage = serde_json::from_str(json)?;
        Ok(Language {
            code: raw.code,
            name: raw.name,
            native_name: raw.native_name,
            script: raw.script,
            min_hyphen_left: raw.hyphen_mins.left,
            min_hyphen_right: raw.hyphen_mins.right,
            letter_pattern: raw.letter_pattern,
            word_split_pattern: raw.word_split_pattern,
            sentence_boundary_pattern: raw.sentence_boundary_pattern,
            formula_configs: raw.formulas,
            syllable_heuristics: raw.syllable_heuristics,
            syllable_mode: raw.syllable_mode,
        })
    }

    /// Whether the language JSON declares configuration for `formula_name`.
    ///
    /// Note: this is **not** the list used by the engine to decide formula
    /// support — that comes from each formula's `supported_languages()`.
    pub fn supports_formula(&self, formula_name: &str) -> bool {
        self.formula_configs.contains_key(formula_name)
    }

    /// Configuration object for `formula_name`, if present.
    pub fn get_formula_config(&self, formula_name: &str) -> Option<&Value> {
        self.formula_configs.get(formula_name)
    }

    /// Keys of the `formulas` JSON block.
    pub fn get_supported_formulas(&self) -> Vec<String> {
        self.formula_configs.keys().cloned().collect()
    }

    /// Read a numeric value from a formula's config object.
    pub fn formula_number(&self, formula_name: &str, key: &str) -> Option<f64> {
        self.get_formula_config(formula_name)
            .and_then(|cfg| cfg.get(key))
            .and_then(value_as_f64)
    }
}

/// Interpret a JSON value as `f64` if it is numeric or a numeric string
/// (mirrors PHP `is_numeric`).
pub(crate) fn value_as_f64(v: &Value) -> Option<f64> {
    match v {
        Value::Number(n) => n.as_f64(),
        Value::String(s) => s.trim().parse::<f64>().ok(),
        _ => None,
    }
}