mini-docs 0.7.0

A minimal, secure build-time Markdown to HTML generator for the mini-* family.
Documentation
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use crate::builder::{load_templates, Builder};
use crate::error::DocError;
use crate::walk;

/// Incremental, mtime-polling rebuild state produced by [`Builder::watch`].
///
/// No background thread and no dependency on an async runtime or `notify` — a caller
/// drives progress by calling [`Watcher::tick`] on whatever cadence it wants (a
/// blocking loop with `std::thread::sleep`, a GUI's idle callback, a test). This
/// matches `mini-static`'s own poller in spirit (mtime comparison, not OS file-events)
/// while keeping the dependency budget at zero for the default case.
pub struct Watcher<'b> {
    builder: &'b Builder,
    md_mtimes: HashMap<PathBuf, SystemTime>,
    template_mtimes: HashMap<PathBuf, SystemTime>,
}

impl<'b> Watcher<'b> {
    pub(crate) fn new(builder: &'b Builder) -> Result<Self, DocError> {
        builder.build()?;
        Ok(Self {
            builder,
            md_mtimes: snapshot_mtimes(builder.input_dir(), "md")?,
            template_mtimes: snapshot_mtimes(builder.templates_dir(), "html")?,
        })
    }

    /// Polls both watched trees once.
    ///
    /// If any template file's mtime changed (added, removed, or modified), every page
    /// is rebuilt — mini-docs doesn't parse Tera's `{% extends %}`/`{% include %}`
    /// graph, so a template-level change conservatively fans out to every dependent
    /// rather than risking a stale page. Otherwise, only the `.md` files whose own
    /// mtime changed are rebuilt.
    ///
    /// If the `.md` file set changed at all (any addition, removal, or modification —
    /// not just what the render cache decided to rebuild), the `data.json` index (if
    /// [`Builder::data_json`] is set) is regenerated too. A template-only change never
    /// alters index content, so it doesn't trigger this.
    ///
    /// Returns the `.md` paths rebuilt this tick (empty if nothing changed).
    pub fn tick(&mut self) -> Result<Vec<PathBuf>, DocError> {
        let current_templates = snapshot_mtimes(self.builder.templates_dir(), "html")?;
        let templates_changed = current_templates != self.template_mtimes;
        self.template_mtimes = current_templates;

        let current_md = snapshot_mtimes(self.builder.input_dir(), "md")?;
        let md_set_changed = current_md != self.md_mtimes;

        let changed: Vec<PathBuf> = if templates_changed {
            current_md.keys().cloned().collect()
        } else {
            current_md
                .iter()
                .filter(|(path, mtime)| self.md_mtimes.get(path.as_path()) != Some(*mtime))
                .map(|(path, _)| path.clone())
                .collect()
        };

        if !changed.is_empty() {
            let tera = load_templates(self.builder.templates_dir())?;
            for md_path in &changed {
                self.builder.build_one(&tera, md_path)?;
            }
        }

        if md_set_changed {
            self.builder.rebuild_data_json()?;
        }

        self.md_mtimes = current_md;
        Ok(changed)
    }
}

fn snapshot_mtimes(dir: &Path, extension: &str) -> Result<HashMap<PathBuf, SystemTime>, DocError> {
    let mut mtimes = HashMap::new();
    for path in walk::walk_files_with_extension(dir, extension)? {
        let mtime = std::fs::metadata(&path)?.modified()?;
        mtimes.insert(path, mtime);
    }
    Ok(mtimes)
}