Skip to main content

openapi_ui/
template.rs

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