Skip to main content

scrybe_cli/
wrap.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Shawn Hartsock and contributors
3
4//! Full-HTML wrapper — wraps a body-only render output in a complete
5//! `<!DOCTYPE html>` document with CDN tags for KaTeX and Mermaid.
6
7use scrybe_render::RenderOutput;
8
9/// Wraps a [`RenderOutput`] fragment in a complete `<!DOCTYPE html>` document.
10///
11/// The generated document includes:
12/// - Charset + viewport `<meta>` tags
13/// - `<title>` set to `title`
14/// - KaTeX CSS/JS CDN tags (math rendering)
15/// - Mermaid.js CDN tag (diagram rendering)
16/// - Theme CSS extracted from the render output (if present)
17/// - The rendered body HTML
18pub fn wrap_full_html(output: &RenderOutput, title: &str) -> String {
19    // Extract theme CSS from the render output if it wrapped a <style> block.
20    let css = output
21        .html
22        .strip_prefix("<style>")
23        .and_then(|s| s.find("</style>").map(|end| &s[..end]))
24        .unwrap_or("");
25
26    format!(
27        r#"<!DOCTYPE html>
28<html lang="en">
29<head>
30  <meta charset="UTF-8">
31  <meta name="viewport" content="width=device-width, initial-scale=1.0">
32  <title>{title}</title>
33  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css">
34  <style>{css}</style>
35</head>
36<body>
37{body}
38<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.js"></script>
39<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16/dist/contrib/auto-render.min.js"
40  onload="renderMathInElement(document.body)"></script>
41<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
42<script>mermaid.initialize({{startOnLoad:true}});</script>
43</body>
44</html>"#,
45        title = title,
46        css = css,
47        body = output.body_html,
48    )
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use scrybe_core::Document;
55    use scrybe_render::{render_html, Theme};
56
57    #[test]
58    fn test_starts_with_doctype() {
59        let doc = Document::new("# Hello");
60        let output = render_html(&doc, Theme::Default);
61        let html = wrap_full_html(&output, "Test");
62        assert!(html.starts_with("<!DOCTYPE html>"));
63    }
64
65    #[test]
66    fn test_contains_katex() {
67        let doc = Document::new("$x^2$");
68        let output = render_html(&doc, Theme::Default);
69        let html = wrap_full_html(&output, "Math Doc");
70        assert!(html.contains("katex"));
71    }
72
73    #[test]
74    fn test_contains_mermaid() {
75        let doc = Document::new("# Diagram");
76        let output = render_html(&doc, Theme::Default);
77        let html = wrap_full_html(&output, "Mermaid Doc");
78        assert!(html.contains("mermaid"));
79    }
80
81    #[test]
82    fn test_title_is_set() {
83        let doc = Document::new("# My Heading");
84        let output = render_html(&doc, Theme::Default);
85        let html = wrap_full_html(&output, "My Custom Title");
86        assert!(html.contains("<title>My Custom Title</title>"));
87    }
88
89    #[test]
90    fn test_body_content_present() {
91        let doc = Document::new("# My Heading");
92        let output = render_html(&doc, Theme::Default);
93        let html = wrap_full_html(&output, "Test");
94        assert!(html.contains("My Heading"));
95    }
96
97    #[test]
98    fn test_viewport_meta() {
99        let doc = Document::new("text");
100        let output = render_html(&doc, Theme::Default);
101        let html = wrap_full_html(&output, "Test");
102        assert!(html.contains("viewport"));
103    }
104}