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