mini-docs 0.3.5

A minimal, secure build-time Markdown to HTML generator for the mini-* family.
Documentation
/// Sanitizes rendered Markdown HTML before it enters the Tera context.
///
/// This must run before a template ever sees the content, and the template's
/// `{{ page.content | safe }}` must only ever mark *this* function's output safe —
/// marking unsanitized HTML safe re-opens the exact XSS hole `ammonia` closes.
///
/// `id` is allowlisted as a generic attribute beyond ammonia's default policy (which
/// only permits it on `<a>`) — our heading-anchor slugs (see `page::render_markdown`)
/// rely on `id` surviving on `h1`–`h6`. `id` is inert (no script execution vector), so
/// this widens *which elements* keep an id, not *what an id can contain*.
///
/// With the `sanitize` feature disabled (an explicit, documented opt-out for
/// trusted-content callers who have measured the trade-off), this is the identity
/// function.
#[cfg(feature = "sanitize")]
pub(crate) fn sanitize_html(html: &str) -> String {
    ammonia::Builder::default()
        .add_generic_attributes(["id"])
        .clean(html)
        .to_string()
}

#[cfg(not(feature = "sanitize"))]
pub(crate) fn sanitize_html(html: &str) -> String {
    html.to_string()
}

#[cfg(all(test, feature = "sanitize"))]
mod tests {
    use super::*;

    #[test]
    fn strips_script_tags() {
        let dirty = "<p>hi</p><script>alert(1)</script>";

        assert_eq!(sanitize_html(dirty), "<p>hi</p>");
    }

    #[test]
    fn strips_event_handler_attributes() {
        let dirty = r#"<img src="x" onerror="alert(1)">"#;

        let clean = sanitize_html(dirty);

        assert!(!clean.contains("onerror"));
    }

    #[test]
    fn preserves_id_attribute_on_headings() {
        let html = r#"<h2 id="getting-started">Getting Started</h2>"#;

        assert_eq!(sanitize_html(html), html);
    }
}