mini-docs 0.7.0

A minimal, secure build-time Markdown to HTML generator for the mini-* family.
Documentation
use serde_json::Value;

use crate::error::DocError;

/// A Markdown processor that transforms the raw body before title/template resolution.
///
/// Processors run in registration order on the raw body immediately after frontmatter
/// split, and their output feeds title/template resolution and markdown rendering.
///
/// # Naming Contract
///
/// The `name()` method returns a unique identifier for this processor. It must be
/// non-empty, ASCII-only, and a valid bare Tera identifier (`[a-z0-9_]+`). An invalid
/// name will be rejected at build time.
pub trait MarkdownProcessor: Send + Sync {
    /// Transforms `body` in the context of this page's `frontmatter`, returning the
    /// processed body or a `DocError`.
    ///
    /// Errors should be prefixed with `name()` for attribution, e.g. `"litmus: ..."`
    /// so callers know which processor failed.
    fn process(&self, body: &str, frontmatter: &Value) -> Result<String, DocError>;

    /// Returns the name of this processor, used to detect duplicates and for error
    /// attribution. Must be non-empty, ASCII-only, and a valid Tera identifier.
    fn name(&self) -> &str;
}

/// A Markdown analyzer that extracts metadata from the body for template rendering.
///
/// Analyzers run after processors, and their results are merged into the Tera context
/// under `page.extensions.<name()>` where templates can render them.
///
/// # Naming Contract
///
/// The `name()` method returns a unique identifier for this analyzer. It must be
/// non-empty, ASCII-only, and a valid bare Tera identifier (`[a-z0-9_]+`). An invalid
/// name will be rejected at build time. Analyzer names are scoped separately from
/// processor names — a processor and analyzer may share a name without conflict.
pub trait MarkdownAnalyzer: Send + Sync {
    /// Analyzes `body` in the context of this page's `frontmatter`, returning a JSON
    /// object with metadata or a `DocError`.
    ///
    /// Errors should be prefixed with `name()` for attribution, e.g. `"litmus: ..."`
    /// so callers know which analyzer failed.
    fn analyze(&self, body: &str, frontmatter: &Value) -> Result<Value, DocError>;

    /// Returns the name of this analyzer, used to detect duplicates and as the key in
    /// `page.extensions.<name()>`. Must be non-empty, ASCII-only, and a valid Tera
    /// identifier.
    fn name(&self) -> &str;
}

/// Validates that a name is a valid Tera identifier: non-empty, ASCII-only, `[a-z0-9_]+`.
#[allow(dead_code)]
pub(crate) fn validate_identifier(name: &str) -> bool {
    !name.is_empty()
        && name
            .chars()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}

#[cfg(test)]
#[path = "../tests/unit/extension.rs"]
mod tests;