mini-docs 0.7.0

A minimal, secure build-time Markdown to HTML generator for the mini-* family.
Documentation
use std::fs;
use std::thread::sleep;
use std::time::Duration;

use mini_docs::{Builder, DocError, MarkdownProcessor};
use serde_json::Value;

const MTIME_SETTLE: Duration = Duration::from_millis(50);

/// A simple processor that appends a marker string to the body.
struct MarkerProcessor;

impl MarkdownProcessor for MarkerProcessor {
    fn process(&self, body: &str, _frontmatter: &Value) -> Result<String, DocError> {
        Ok(format!("{body}\n\n*Processed by marker processor*"))
    }

    fn name(&self) -> &str {
        "marker"
    }
}

/// A processor that duplicates the body to test double processing.
struct DuplicateProcessor;

impl MarkdownProcessor for DuplicateProcessor {
    fn process(&self, body: &str, _frontmatter: &Value) -> Result<String, DocError> {
        Ok(format!("## Duplicate\n{body}\n## Duplicate\n{body}"))
    }

    fn name(&self) -> &str {
        "duplicate"
    }
}

/// A processor with a conflicting name.
struct ConflictProcessor;

impl MarkdownProcessor for ConflictProcessor {
    fn process(&self, body: &str, _frontmatter: &Value) -> Result<String, DocError> {
        Ok(body.to_string())
    }

    fn name(&self) -> &str {
        "marker" // intentionally conflicts with MarkerProcessor
    }
}

/// A processor that appends a marker string is executed and the output appears in the rendered HTML.
#[test]
fn processor_output_appears_in_rendered_html() {
    let input = tempfile::tempdir().expect("create input tempdir");
    let templates = tempfile::tempdir().expect("create templates tempdir");
    let output = tempfile::tempdir().expect("create output tempdir");

    fs::write(input.path().join("page.md"), "# Test\n\nBody content.\n").expect("write page.md");
    fs::write(
        templates.path().join("page.html"),
        "{{ page.content | safe }}",
    )
    .expect("write page.html");

    Builder::new(input.path())
        .templates(templates.path())
        .output(output.path())
        .default_template("page.html")
        .processor(MarkerProcessor)
        .build()
        .expect("build should succeed");

    let written = fs::read_to_string(output.path().join("page.html")).expect("read output file");
    assert!(
        written.contains("Processed by marker processor"),
        "processor marker text should appear in rendered HTML: {written}"
    );
}

/// Two processors with the same name produce DocError::Extension from build() before any page is written.
#[test]
fn duplicate_processor_names_cause_error() {
    let input = tempfile::tempdir().expect("create input tempdir");
    let templates = tempfile::tempdir().expect("create templates tempdir");
    let output = tempfile::tempdir().expect("create output tempdir");

    fs::write(input.path().join("page.md"), "# Test\n\nBody content.\n").expect("write page.md");
    fs::write(
        templates.path().join("page.html"),
        "{{ page.content | safe }}",
    )
    .expect("write page.html");

    let result = Builder::new(input.path())
        .templates(templates.path())
        .output(output.path())
        .default_template("page.html")
        .processor(MarkerProcessor)
        .processor(ConflictProcessor)
        .build();

    match result {
        Err(DocError::Extension(msg)) => {
            assert!(
                msg.contains("marker"),
                "error message should mention the conflicting name: {msg}"
            );
        }
        _ => panic!("expected DocError::Extension with duplicate name, got {result:?}"),
    }

    // verify no output was written
    assert!(
        !output.path().join("page.html").exists(),
        "no output should be written when build fails"
    );
}

/// A processor registered on Builder works through the watch() / tick() path too.
#[test]
fn processor_works_in_watch_path() {
    let input = tempfile::tempdir().expect("create input tempdir");
    let templates = tempfile::tempdir().expect("create templates tempdir");
    let output = tempfile::tempdir().expect("create output tempdir");

    fs::write(input.path().join("page.md"), "# Test\n\nInitial.\n").expect("write page.md");
    fs::write(
        templates.path().join("page.html"),
        "{{ page.content | safe }}",
    )
    .expect("write page.html");

    let builder = Builder::new(input.path())
        .templates(templates.path())
        .output(output.path())
        .default_template("page.html")
        .processor(MarkerProcessor);

    let mut watcher = builder.watch().expect("initial watch build should succeed");

    let initial = fs::read_to_string(output.path().join("page.html")).expect("read output file");
    assert!(
        initial.contains("Processed by marker processor"),
        "processor should run during initial watch build: {initial}"
    );

    sleep(MTIME_SETTLE);
    fs::write(input.path().join("page.md"), "# Test\n\nUpdated content.\n")
        .expect("rewrite page.md");

    let changed = watcher.tick().expect("tick should succeed");
    assert_eq!(
        changed.len(),
        1,
        "the page should be rebuilt on tick: {changed:?}"
    );

    let after = fs::read_to_string(output.path().join("page.html")).expect("read updated output");
    assert!(
        after.contains("Processed by marker processor"),
        "processor should run on watch tick: {after}"
    );
    assert!(
        after.contains("Updated content"),
        "updated content should be present: {after}"
    );
}

/// Multiple processors run in order.
#[test]
fn multiple_processors_run_in_order() {
    let input = tempfile::tempdir().expect("create input tempdir");
    let templates = tempfile::tempdir().expect("create templates tempdir");
    let output = tempfile::tempdir().expect("create output tempdir");

    fs::write(input.path().join("page.md"), "# Header\n\nContent.\n").expect("write page.md");
    fs::write(
        templates.path().join("page.html"),
        "{{ page.content | safe }}",
    )
    .expect("write page.html");

    Builder::new(input.path())
        .templates(templates.path())
        .output(output.path())
        .default_template("page.html")
        .processor(DuplicateProcessor)
        .processor(MarkerProcessor)
        .build()
        .expect("build should succeed");

    let written = fs::read_to_string(output.path().join("page.html")).expect("read output file");

    // DuplicateProcessor should run first, wrapping the content
    // Then MarkerProcessor appends the marker
    assert!(
        written.contains("Processed by marker processor"),
        "marker processor should run: {written}"
    );
    assert!(
        written.contains("Duplicate"),
        "duplicate processor output should be present: {written}"
    );

    // The duplication should produce multiple copies of the header
    let header_count = written.matches("<h1").count();
    assert!(
        header_count >= 2,
        "duplicate processor should have created multiple copies of the header: {written}"
    );
}