mini-docs 0.3.6

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

/// Returns `true` if this page's frontmatter marks it as a draft (`draft: true`) —
/// excluded from both the HTML build output and the `data.json` index entirely.
///
/// Marking an already-published page `draft: true` does not retroactively delete its
/// existing `.html` output — `build()` has no general orphan-removal pass (deleting a
/// `.md` file doesn't clean up its old output either); this is a pre-existing,
/// documented limitation, not draft-specific.
pub(crate) fn is_draft(frontmatter: &Value) -> bool {
    frontmatter
        .get("draft")
        .and_then(Value::as_bool)
        .unwrap_or(false)
}

/// Builds one `data.json` entry for a page.
///
/// `date`, `updated`, `version`, and `summary` default to `""` when the frontmatter
/// key is absent (or isn't a string); `tags` defaults to `[]`, silently dropping any
/// non-string list item; `pinned` defaults to `false`. Field order here matches a
/// hand-authored `data.json` for readability, but `serde_json::Map`'s default
/// (non-`preserve_order`) serialization is alphabetical regardless — that ordering is
/// cosmetic intent, not a guaranteed wire format.
pub(crate) fn page_entry(id: &str, title: &str, url: &str, frontmatter: &Value) -> Value {
    let string_field = |key: &str| -> String {
        frontmatter
            .get(key)
            .and_then(Value::as_str)
            .unwrap_or("")
            .to_string()
    };

    let tags: Vec<Value> = frontmatter
        .get("tags")
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .filter_map(Value::as_str)
                .map(|s| Value::String(s.to_string()))
                .collect()
        })
        .unwrap_or_default();

    let pinned = frontmatter
        .get("pinned")
        .and_then(Value::as_bool)
        .unwrap_or(false);

    let mut entry = Map::new();
    entry.insert("id".to_string(), Value::String(id.to_string()));
    entry.insert("title".to_string(), Value::String(title.to_string()));
    entry.insert("date".to_string(), Value::String(string_field("date")));
    entry.insert(
        "updated".to_string(),
        Value::String(string_field("updated")),
    );
    entry.insert(
        "version".to_string(),
        Value::String(string_field("version")),
    );
    entry.insert("url".to_string(), Value::String(url.to_string()));
    entry.insert(
        "summary".to_string(),
        Value::String(string_field("summary")),
    );
    entry.insert("tags".to_string(), Value::Array(tags));
    entry.insert("pinned".to_string(), Value::Bool(pinned));
    Value::Object(entry)
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn draft_true_is_detected() {
        assert!(is_draft(&json!({"draft": true})));
    }

    #[test]
    fn missing_draft_key_defaults_to_false() {
        assert!(!is_draft(&json!({})));
    }

    #[test]
    fn entry_fills_in_every_field_when_frontmatter_has_them_all() {
        let frontmatter = json!({
            "date": "2026-07-14",
            "updated": "2026-07-15",
            "version": "2",
            "summary": "A short summary.",
            "tags": ["rust", "docs"],
            "pinned": true,
        });

        let entry = page_entry(
            "design-review-sync",
            "Design review sync",
            "/design-review-sync",
            &frontmatter,
        );

        assert_eq!(
            entry,
            json!({
                "id": "design-review-sync",
                "title": "Design review sync",
                "date": "2026-07-14",
                "updated": "2026-07-15",
                "version": "2",
                "url": "/design-review-sync",
                "summary": "A short summary.",
                "tags": ["rust", "docs"],
                "pinned": true,
            })
        );
    }

    #[test]
    fn entry_defaults_missing_fields_when_frontmatter_is_empty() {
        let entry = page_entry(
            "weekly-eng-report",
            "Weekly eng report",
            "/weekly-eng-report",
            &json!({}),
        );

        assert_eq!(
            entry,
            json!({
                "id": "weekly-eng-report",
                "title": "Weekly eng report",
                "date": "",
                "updated": "",
                "version": "",
                "url": "/weekly-eng-report",
                "summary": "",
                "tags": [],
                "pinned": false,
            })
        );
    }

    #[test]
    fn non_string_tag_items_are_dropped_silently() {
        let frontmatter = json!({"tags": ["rust", true, "docs"]});

        let entry = page_entry("id", "Title", "/id", &frontmatter);

        assert_eq!(entry["tags"], json!(["rust", "docs"]));
    }
}