jsslint-core 1.1.0

Rule engine behind the Journal of Statistical Software (JSS) LaTeX/BibTeX style checker. Powers the jsslint CLI, WASM, Python, and R distributions.
Documentation
//! Canonical-form lookup tables — embedded from
//! `specs/003-jss-rule-catalogue/terms.json` (generated from
//! `texlint.journals.jss.terms` by `tools/generate_terms_json.py`; run
//! that script and commit the diff whenever `terms.py` changes).

use serde::Deserialize;
use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;

#[derive(Debug, Deserialize)]
struct TermsJson {
    languages: Vec<String>,
    r_packages: Vec<String>,
    canonical: HashMap<String, String>,
    publisher_canonical: HashMap<String, String>,
    journal_canonical: HashMap<String, String>,
    /// Order is semantically significant (first match wins) — a `Vec`
    /// of `[prefix, canonical]` pairs, not a map.
    publisher_prefix_canonical: Vec<[String; 2]>,
}

pub struct Terms {
    pub languages: HashSet<String>,
    pub r_packages: HashSet<String>,
    pub canonical: HashMap<String, String>,
    pub publisher_canonical: HashMap<String, String>,
    pub journal_canonical: HashMap<String, String>,
    pub publisher_prefix_canonical: Vec<(String, String)>,
}

// Copied into OUT_DIR by build.rs (rather than a relative include_str!
// path) because the depth from this file to specs/003-jss-rule-catalogue
// differs between the canonical crate location and a vendored copy (see
// jsslintr's src/rust/jsslint-core).
const TERMS_JSON: &str = include_str!(concat!(env!("OUT_DIR"), "/terms.json"));

pub static TERMS: LazyLock<Terms> = LazyLock::new(|| {
    let raw: TermsJson = serde_json::from_str(TERMS_JSON)
        .expect("terms.json must parse (generated by tools/generate_terms_json.py)");
    Terms {
        languages: raw.languages.into_iter().collect(),
        r_packages: raw.r_packages.into_iter().collect(),
        canonical: raw.canonical,
        publisher_canonical: raw.publisher_canonical,
        journal_canonical: raw.journal_canonical,
        publisher_prefix_canonical: raw
            .publisher_prefix_canonical
            .into_iter()
            .map(|[prefix, canon]| (prefix, canon))
            .collect(),
    }
});

impl Terms {
    /// Mirrors `texlint.journals.jss.terms.canonical_form`: no case
    /// folding, no substring matching, no fuzzy matching.
    pub fn canonical_form(&self, token: &str) -> Option<String> {
        if token.trim().is_empty() {
            return None;
        }
        if let Some(canon) = self.canonical.get(token) {
            return Some(canon.clone());
        }
        if self.languages.contains(token) {
            return Some(token.to_string());
        }
        if self.r_packages.contains(token) {
            return Some(token.to_string());
        }
        None
    }
}