Skip to main content

openapi_ui/
template.rs

1//! HTML template rendering for the documentation UI.
2
3use crate::openapi::OpenAPISpec;
4use crate::theme;
5
6/// The raw HTML template used for rendering.
7pub const TEMPLATE_HTML: &str = include_str!("index.html");
8
9const SAMPLE_DATA: &str = include_str!("sample_data.json");
10
11/// Renders the HTML template with the given spec, theme, and favicon.
12pub fn template(spec: &OpenAPISpec, theme_name: &str, favicon: &str) -> String {
13    let spec_json = serde_json::to_string(spec).unwrap_or_default();
14    let js_string = serde_json::to_string(&spec_json).unwrap_or_default();
15
16    let mode = theme::ThemeMode::from_str(theme_name);
17
18    TEMPLATE_HTML
19        .replace("{{light}}", &theme::ThemeMode::Light.get_css())
20        .replace("{{dark}}", &theme::ThemeMode::Dark.get_css())
21        .replace("{{theme}}", mode.as_str())
22        .replace("{{favicon}}", favicon)
23        .replace("/* SPEC_JSON_PLACEHOLDER */ null", &js_string)
24}
25
26/// Renders the HTML template with optional custom CSS injected after the built-in themes.
27pub fn template_with_custom_theme(
28    spec: &OpenAPISpec,
29    theme_name: &str,
30    custom_css: Option<&str>,
31    favicon: &str,
32) -> String {
33    let spec_json = serde_json::to_string(spec).unwrap_or_default();
34    let js_string = serde_json::to_string(&spec_json).unwrap_or_default();
35
36    let mode = theme::ThemeMode::from_str(theme_name);
37    let inject_theme_script = mode == theme::ThemeMode::System;
38
39    // Inline script to set theme before page renders (prevents flash of wrong theme)
40    let theme_script = if inject_theme_script {
41        r#"<script>(function(){var t=localStorage.getItem("apidocs-theme");if(!t||t==="system"){t=window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}document.documentElement.setAttribute("data-theme",t)})()</script>"#
42    } else {
43        ""
44    };
45
46    let light_content = theme::ThemeMode::Light.get_css();
47    let dark_content = theme::ThemeMode::Dark.get_css();
48
49    let mut html = TEMPLATE_HTML
50        .replace("{{light}}", &light_content)
51        .replace("{{dark}}", &dark_content)
52        .replace("{{theme}}", mode.as_str())
53        .replace("{{favicon}}", favicon)
54        .replace("/* SPEC_JSON_PLACEHOLDER */ null", &js_string);
55
56    if inject_theme_script {
57        html = html.replace("<head>", &format!("<head>\n        {}", theme_script));
58    }
59
60    if let Some(css) = custom_css {
61        html.replace("</head>", &format!("<style>{}</style></head>", css))
62    } else {
63        html
64    }
65}
66
67/// Renders a demo template using the built-in Petstore sample data.
68pub fn base_template() -> String {
69    let sample_data_js = serde_json::to_string(SAMPLE_DATA).unwrap_or_default();
70
71    TEMPLATE_HTML
72        .replace("{{light}}", &theme::ThemeMode::Light.get_css())
73        .replace("{{dark}}", &theme::ThemeMode::Dark.get_css())
74        .replace("{{theme}}", "system")
75        .replace(
76            "{{favicon}}",
77            "https://www.openapis.org/wp-content/uploads/sites/31/2019/06/favicon-140x140.png",
78        )
79        .replace("/* SPEC_JSON_PLACEHOLDER */ null", "null")
80        .replace("/* SAMPLE_DATA_PLACEHOLDER */ null", &sample_data_js)
81}
82
83/// Renders the HTML template with embedded theme CSS.
84pub fn template_with_embedded_theme(
85    spec: &OpenAPISpec,
86    theme_name: &str,
87    favicon: &str,
88) -> String {
89    let spec_json = serde_json::to_string(spec).unwrap_or_default();
90    let js_string = serde_json::to_string(&spec_json).unwrap_or_default();
91
92    let mode = theme::ThemeMode::from_str(theme_name);
93
94    TEMPLATE_HTML
95        .replace("{{light}}", &theme::ThemeMode::Light.get_css())
96        .replace("{{dark}}", &theme::ThemeMode::Dark.get_css())
97        .replace("{{theme}}", mode.as_str())
98        .replace("{{favicon}}", favicon)
99        .replace("/* SPEC_JSON_PLACEHOLDER */ null", &js_string)
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::openapi::{Info, OpenAPISpec};
106    use std::collections::HashMap;
107
108    #[test]
109    fn test_template_generation() {
110        let spec = OpenAPISpec {
111            openapi: "3.0.0".to_string(),
112            info: Info {
113                title: "Test API".to_string(),
114                version: "1.0.0".to_string(),
115                description: Some("A test API".to_string()),
116                terms_of_service: None,
117                contact: None,
118                license: None,
119                x_logo: None,
120            },
121            servers: vec![],
122            paths: HashMap::new(),
123            components: None,
124            security: None,
125            tags: None,
126            external_docs: None,
127        };
128
129        let html = template(&spec, "dark", "favicon.ico");
130        assert!(html.contains("Test API"));
131        assert!(html.contains("3.0.0"));
132        assert!(html.contains("<!doctype html>"));
133    }
134
135    #[test]
136    fn test_template_with_custom_theme() {
137        let spec = OpenAPISpec {
138            openapi: "3.0.0".to_string(),
139            info: Info {
140                title: "Custom Theme API".to_string(),
141                version: "1.0.0".to_string(),
142                description: None,
143                terms_of_service: None,
144                contact: None,
145                license: None,
146                x_logo: None,
147            },
148            servers: vec![],
149            paths: HashMap::new(),
150            components: None,
151            security: None,
152            tags: None,
153            external_docs: None,
154        };
155
156        let custom_css = ":root { --accent: #ff0000; }";
157        let html = template_with_custom_theme(&spec, "light", Some(custom_css), "favicon.ico");
158        assert!(html.contains("Custom Theme API"));
159    }
160
161    #[test]
162    fn test_template_with_system_theme_injects_script() {
163        let spec = OpenAPISpec {
164            openapi: "3.0.0".to_string(),
165            info: Info {
166                title: "System Theme API".to_string(),
167                version: "1.0.0".to_string(),
168                description: None,
169                terms_of_service: None,
170                contact: None,
171                license: None,
172                x_logo: None,
173            },
174            servers: vec![],
175            paths: HashMap::new(),
176            components: None,
177            security: None,
178            tags: None,
179            external_docs: None,
180        };
181
182        let html = template_with_custom_theme(&spec, "system", None, "favicon.ico");
183        assert!(html.contains("apidocs-theme"));
184        assert!(html.contains("prefers-color-scheme"));
185        assert!(html.contains("data-theme=\"system\""));
186    }
187
188    #[test]
189    fn test_system_theme_script_content() {
190        let spec = OpenAPISpec {
191            openapi: "3.0.0".to_string(),
192            info: Info {
193                title: "Test".to_string(),
194                version: "1.0.0".to_string(),
195                description: None,
196                terms_of_service: None,
197                contact: None,
198                license: None,
199                x_logo: None,
200            },
201            servers: vec![],
202            paths: HashMap::new(),
203            components: None,
204            security: None,
205            tags: None,
206            external_docs: None,
207        };
208
209        let html = template_with_custom_theme(&spec, "system", None, "favicon.ico");
210
211        assert!(html.contains("localStorage.getItem(\"apidocs-theme\")"));
212        assert!(html.contains("matchMedia(\"(prefers-color-scheme: dark)\")"));
213
214        assert!(html.contains("if(!t||t===\"system\")"));
215
216        assert!(html.contains("setAttribute(\"data-theme\",t)"));
217
218    }
219
220    #[test]
221    fn test_base_template() {
222        let html = base_template();
223        assert!(html.contains("<!doctype html>"));
224        assert!(html.contains("<html"));
225        assert!(html.contains("INJECTED_SPEC"));
226    }
227
228    #[test]
229    fn test_template_with_embedded_theme() {
230        let spec = OpenAPISpec {
231            openapi: "3.0.0".to_string(),
232            info: Info {
233                title: "Embedded Theme API".to_string(),
234                version: "1.0.0".to_string(),
235                description: None,
236                terms_of_service: None,
237                contact: None,
238                license: None,
239                x_logo: None,
240            },
241            servers: vec![],
242            paths: HashMap::new(),
243            components: None,
244            security: None,
245            tags: None,
246            external_docs: None,
247        };
248
249        let html = template_with_embedded_theme(&spec, "dark", "favicon.ico");
250        assert!(html.contains("Embedded Theme API"));
251        assert!(html.contains("<!doctype html>"));
252    }
253}