mini-docs 0.7.0

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` and `class` are allowlisted as generic attributes beyond ammonia's default
/// policy (which permits `id` only on `<a>` and strips `class` outright). Heading-anchor
/// slugs (see `page::render_markdown`) rely on `id` surviving on `h1`–`h6`, and
/// extension-emitted markup — `CiteProcessor`'s footnotes — is unstylable without
/// `class`. Both attributes are inert: neither is a script execution vector, and
/// neither can carry a URL. This widens *which elements* keep them, not *what they can
/// contain*.
///
/// The cost is that a content author can name any class the site's stylesheet defines,
/// including one meant for chrome rather than prose. That is a styling concern, not a
/// security one, and it is the same trade already accepted for `id`.
///
/// 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", "class"])
        .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);
    }

    #[test]
    fn preserves_class_attribute() {
        let html = r#"<ol class="footnotes"><li class="footnote">a</li></ol>"#;

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

    #[test]
    fn still_strips_script_bearing_attributes_from_a_classed_element() {
        let dirty = r#"<p class="note" onclick="alert(1)">hi</p>"#;

        let clean = sanitize_html(dirty);

        assert!(clean.contains(r#"class="note""#));
        assert!(
            !clean.contains("onclick"),
            "event handler survived: {clean}"
        );
    }
}