Skip to main content

mini_docs/
extension.rs

1use serde_json::Value;
2
3use crate::error::DocError;
4
5/// A Markdown processor that transforms the raw body before title/template resolution.
6///
7/// Processors run in registration order on the raw body immediately after frontmatter
8/// split, and their output feeds title/template resolution and markdown rendering.
9///
10/// # Naming Contract
11///
12/// The `name()` method returns a unique identifier for this processor. It must be
13/// non-empty, ASCII-only, and a valid bare Tera identifier (`[a-z0-9_]+`). An invalid
14/// name will be rejected at build time.
15pub trait MarkdownProcessor: Send + Sync {
16    /// Transforms `body` in the context of this page's `frontmatter`, returning the
17    /// processed body or a `DocError`.
18    ///
19    /// Errors should be prefixed with `name()` for attribution, e.g. `"litmus: ..."`
20    /// so callers know which processor failed.
21    fn process(&self, body: &str, frontmatter: &Value) -> Result<String, DocError>;
22
23    /// Returns the name of this processor, used to detect duplicates and for error
24    /// attribution. Must be non-empty, ASCII-only, and a valid Tera identifier.
25    fn name(&self) -> &str;
26}
27
28/// A Markdown analyzer that extracts metadata from the body for template rendering.
29///
30/// Analyzers run after processors, and their results are merged into the Tera context
31/// under `page.extensions.<name()>` where templates can render them.
32///
33/// # Naming Contract
34///
35/// The `name()` method returns a unique identifier for this analyzer. It must be
36/// non-empty, ASCII-only, and a valid bare Tera identifier (`[a-z0-9_]+`). An invalid
37/// name will be rejected at build time. Analyzer names are scoped separately from
38/// processor names — a processor and analyzer may share a name without conflict.
39pub trait MarkdownAnalyzer: Send + Sync {
40    /// Analyzes `body` in the context of this page's `frontmatter`, returning a JSON
41    /// object with metadata or a `DocError`.
42    ///
43    /// Errors should be prefixed with `name()` for attribution, e.g. `"litmus: ..."`
44    /// so callers know which analyzer failed.
45    fn analyze(&self, body: &str, frontmatter: &Value) -> Result<Value, DocError>;
46
47    /// Returns the name of this analyzer, used to detect duplicates and as the key in
48    /// `page.extensions.<name()>`. Must be non-empty, ASCII-only, and a valid Tera
49    /// identifier.
50    fn name(&self) -> &str;
51}
52
53/// Validates that a name is a valid Tera identifier: non-empty, ASCII-only, `[a-z0-9_]+`.
54#[allow(dead_code)]
55pub(crate) fn validate_identifier(name: &str) -> bool {
56    !name.is_empty()
57        && name
58            .chars()
59            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
60}
61
62#[cfg(test)]
63#[path = "../tests/unit/extension.rs"]
64mod tests;