mini-docs 0.3.7

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;

/// mtime resolution varies by filesystem; sleeping past it keeps the "did this file
/// change" comparison from racing a same-tick write.
const MTIME_SETTLE: Duration = Duration::from_millis(50);

/// A base template's change must fan out to every page that (in principle) extends
/// it — mini-docs doesn't parse the `{% extends %}` graph, so it conservatively
/// rebuilds every page whenever any template file's mtime changes.
#[test]
fn template_change_rebuilds_all_dependent_pages_on_next_tick() {
    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("guide.md"), "# Guide\n\nBody text.\n").expect("write guide.md");
    fs::write(
        templates.path().join("base.html"),
        "<body>v1: {% block content %}{% endblock %}</body>",
    )
    .expect("write base.html");
    fs::write(
        templates.path().join("page.html"),
        "{% extends \"base.html\" %}{% block content %}{{ page.content | safe }}{% endblock %}",
    )
    .expect("write page.html");

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

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

    let before = fs::read_to_string(output.path().join("guide.html")).expect("read initial output");
    assert!(
        before.contains("v1:"),
        "initial build should use the v1 base template: {before}"
    );

    sleep(MTIME_SETTLE);
    fs::write(
        templates.path().join("base.html"),
        "<body>v2: {% block content %}{% endblock %}</body>",
    )
    .expect("rewrite base.html");

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

    let after = fs::read_to_string(output.path().join("guide.html")).expect("read rebuilt output");
    assert!(
        after.contains("v2:"),
        "base template change should fan out to the page: {after}"
    );
    assert!(
        !after.contains("v1:"),
        "stale v1 content should not remain: {after}"
    );
}

/// Only the `.md` file that actually changed is rebuilt when no template changed.
#[test]
fn unchanged_md_file_is_not_rebuilt_alongside_a_changed_one() {
    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("a.md"), "# A\n").expect("write a.md");
    fs::write(input.path().join("b.md"), "# B\n").expect("write b.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");

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

    sleep(MTIME_SETTLE);
    fs::write(input.path().join("a.md"), "# A changed\n").expect("rewrite a.md");

    let changed = watcher.tick().expect("tick should succeed");

    assert_eq!(changed.len(), 1, "only a.md should be rebuilt: {changed:?}");
    assert_eq!(changed[0].file_name().unwrap(), "a.md");
}

/// A tick with no filesystem changes at all rebuilds nothing.
#[test]
fn tick_with_no_changes_rebuilds_nothing() {
    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("a.md"), "# A\n").expect("write a.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");

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

    let changed = watcher.tick().expect("tick should succeed");
    assert!(
        changed.is_empty(),
        "nothing changed, so nothing should rebuild: {changed:?}"
    );
}