Skip to main content

docgen_core/
math.rs

1//! Build-time KaTeX rendering. Each math expression in a document is rendered
2//! to static HTML at build time via the `katex` crate (default `quick-js`
3//! backend), so the generated site ships **zero runtime JS for math**.
4
5use crate::util::escape_html;
6
7/// Render one math expression to KaTeX HTML at build time.
8///
9/// `display` selects block (`$$`) vs inline (`$`) layout. On a KaTeX parse
10/// error we fall back to an escaped `<code>` so a bad expression degrades
11/// gracefully instead of failing the whole build.
12///
13/// `throw_on_error` is **true** here: with it false, KaTeX swallows invalid
14/// input and emits its own red error markup (returning `Ok`), which would never
15/// reach our graceful fallback. Letting KaTeX return `Err` on a genuine parse
16/// failure lets us emit a clean escaped `<code class="docgen-math-error">`.
17///
18/// The error fallback honors `display`: a failed display (`$$`) equation is
19/// wrapped in a block `<div class="katex-display docgen-math-error">` so it
20/// still renders as a centered block (matching `.katex-display` spacing),
21/// while inline math degrades to inline `<code>`. The KaTeX error message is
22/// logged to stderr so a malformed expression leaves a build-time diagnostic.
23pub fn render_math(src: &str, display: bool) -> String {
24    let opts = katex::Opts::builder()
25        .display_mode(display)
26        .throw_on_error(true)
27        .build()
28        .expect("katex opts build");
29    match katex::render_with_opts(src, &opts) {
30        Ok(html) => html,
31        Err(e) => {
32            eprintln!("docgen: KaTeX failed to render math `{src}`: {e}");
33            let escaped = escape_html(src);
34            if display {
35                format!("<div class=\"katex-display docgen-math-error\">{escaped}</div>")
36            } else {
37                format!("<code class=\"docgen-math-error\">{escaped}</code>")
38            }
39        }
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46
47    #[test]
48    fn renders_inline_math_to_katex_html() {
49        let html = render_math("E=mc^2", false);
50        assert!(html.contains("katex"));
51        assert!(!html.contains("katex-display")); // inline → no display wrapper
52    }
53
54    #[test]
55    fn renders_display_math_with_display_wrapper() {
56        let html = render_math("\\int_0^1 x\\,dx", true);
57        assert!(html.contains("katex-display"));
58    }
59
60    #[test]
61    fn bad_expression_degrades_to_escaped_code() {
62        let html = render_math("\\frac{", false);
63        assert!(html.contains("docgen-math-error"));
64        assert!(html.contains("<code")); // inline → inline code
65        assert!(!html.contains("<script"));
66    }
67
68    #[test]
69    fn bad_display_expression_degrades_to_block() {
70        // A failed *display* equation must stay a centered block, not collapse
71        // to an inline <code> fragment.
72        let html = render_math("\\frac{", true);
73        assert!(html.contains("docgen-math-error"));
74        assert!(html.contains("katex-display")); // block wrapper retained
75        assert!(html.contains("<div")); // block element, not inline <code>
76        assert!(!html.contains("<code"));
77    }
78
79    #[test]
80    fn bad_expression_escapes_html_metacharacters() {
81        // A malformed expression carrying HTML metacharacters must be escaped
82        // before landing in raw HTML (render.unsafe = true downstream).
83        let html = render_math("<script>\\frac{", false);
84        assert!(html.contains("&lt;script&gt;"));
85        assert!(!html.contains("<script>"));
86    }
87}