readsight 1.0.0

Multilingual readability library — 86 languages, 17 formulas, TeX-based syllable counting via the Frank M. Liang algorithm.
Documentation
//! Configuration and data-source resolution.

use std::path::PathBuf;

use include_dir::{include_dir, Dir};

use crate::error::{Error, Result};

/// Embedded copy of the `data/` directory (86 language JSON files + 86 `.tex`
/// pattern files). Bundled so the crate works with zero filesystem access.
static DATA_DIR: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/data");

/// Where language and pattern data is loaded from.
#[derive(Clone, Debug, Default)]
pub enum DataSource {
    /// Use the data embedded in the binary (default).
    #[default]
    Embedded,
    /// Load data from the filesystem.
    Filesystem {
        /// Directory containing `hyph-*.tex` files.
        patterns_dir: PathBuf,
        /// Directory containing `*.json` language files.
        languages_dir: PathBuf,
    },
}

/// Engine configuration.
#[derive(Clone, Debug, Default)]
pub struct Config {
    /// Data source (embedded or filesystem).
    pub source: DataSource,
}

impl Config {
    /// Default configuration using embedded data.
    pub fn default_embedded() -> Self {
        Config {
            source: DataSource::Embedded,
        }
    }

    /// Configuration reading data from the given directories.
    pub fn from_dirs(patterns_dir: impl Into<PathBuf>, languages_dir: impl Into<PathBuf>) -> Self {
        Config {
            source: DataSource::Filesystem {
                patterns_dir: patterns_dir.into(),
                languages_dir: languages_dir.into(),
            },
        }
    }

    /// Read a language JSON file as a string.
    pub(crate) fn read_language(&self, code: &str) -> Result<Option<String>> {
        let name = format!("{code}.json");
        match &self.source {
            DataSource::Embedded => Ok(DATA_DIR
                .get_file(format!("languages/{name}"))
                .and_then(|f| f.contents_utf8())
                .map(|s| s.to_string())),
            DataSource::Filesystem { languages_dir, .. } => {
                let path = languages_dir.join(&name);
                if path.exists() {
                    Ok(Some(std::fs::read_to_string(path)?))
                } else {
                    Ok(None)
                }
            }
        }
    }

    /// Read a `.tex` pattern file as a string.
    pub(crate) fn read_pattern(&self, code: &str) -> Result<String> {
        let name = format!("hyph-{code}.tex");
        match &self.source {
            DataSource::Embedded => DATA_DIR
                .get_file(format!("patterns/{name}"))
                .and_then(|f| f.contents_utf8())
                .map(|s| s.to_string())
                .ok_or(Error::PatternFileNotFound(name)),
            DataSource::Filesystem { patterns_dir, .. } => {
                let path = patterns_dir.join(&name);
                if path.exists() {
                    Ok(std::fs::read_to_string(path)?)
                } else {
                    Err(Error::PatternFileNotFound(path.display().to_string()))
                }
            }
        }
    }

    /// List all available language codes, sorted ascending.
    pub(crate) fn list_language_codes(&self) -> Result<Vec<String>> {
        let mut codes: Vec<String> = match &self.source {
            DataSource::Embedded => DATA_DIR
                .get_dir("languages")
                .map(|d| {
                    d.files()
                        .filter_map(|f| {
                            let p = f.path();
                            (p.extension().and_then(|e| e.to_str()) == Some("json"))
                                .then(|| p.file_stem().and_then(|s| s.to_str()).map(String::from))
                                .flatten()
                        })
                        .collect()
                })
                .unwrap_or_default(),
            DataSource::Filesystem { languages_dir, .. } => {
                let mut v = Vec::new();
                if let Ok(entries) = std::fs::read_dir(languages_dir) {
                    for entry in entries.flatten() {
                        let p = entry.path();
                        if p.extension().and_then(|e| e.to_str()) == Some("json") {
                            if let Some(stem) = p.file_stem().and_then(|s| s.to_str()) {
                                v.push(stem.to_string());
                            }
                        }
                    }
                }
                v
            }
        };
        codes.sort();
        Ok(codes)
    }
}