1use anyhow::Result;
4use mdbook_core::config::HtmlConfig;
5use mdbook_core::utils::fs;
6use std::path::{Path, PathBuf};
7use tracing::{info, warn};
8
9pub(crate) mod fonts;
10pub(crate) mod playground_editor;
11#[cfg(feature = "search")]
12pub(crate) mod searcher;
13
14static INDEX: &[u8] = include_bytes!("../../front-end/templates/index.hbs");
15static HEAD: &[u8] = include_bytes!("../../front-end/templates/head.hbs");
16static REDIRECT: &[u8] = include_bytes!("../../front-end/templates/redirect.hbs");
17static HEADER: &[u8] = include_bytes!("../../front-end/templates/header.hbs");
18static TOC_JS: &[u8] = include_bytes!("../../front-end/templates/toc.js.hbs");
19static TOC_HTML: &[u8] = include_bytes!("../../front-end/templates/toc.html.hbs");
20static CHROME_CSS: &[u8] = include_bytes!("../../front-end/css/chrome.css");
21static GENERAL_CSS: &[u8] = include_bytes!("../../front-end/css/general.css");
22static PRINT_CSS: &[u8] = include_bytes!("../../front-end/css/print.css");
23static VARIABLES_CSS: &[u8] = include_bytes!("../../front-end/css/variables.css");
24static FAVICON_PNG: &[u8] = include_bytes!("../../front-end/images/favicon.png");
25static FAVICON_SVG: &[u8] = include_bytes!("../../front-end/images/favicon.svg");
26static JS: &[u8] = include_bytes!("../../front-end/js/book.js");
27static HIGHLIGHT_JS: &[u8] = include_bytes!("../../front-end/js/highlight.js");
28static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("../../front-end/css/tomorrow-night.css");
29static HIGHLIGHT_CSS: &[u8] = include_bytes!("../../front-end/css/highlight.css");
30static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("../../front-end/css/ayu-highlight.css");
31static CLIPBOARD_JS: &[u8] = include_bytes!("../../front-end/js/clipboard.min.js");
32
33#[derive(Debug, PartialEq)]
40pub struct Theme {
41 pub(crate) index: Vec<u8>,
42 pub(crate) head: Vec<u8>,
43 pub(crate) redirect: Vec<u8>,
44 pub(crate) header: Vec<u8>,
45 pub(crate) toc_js: Vec<u8>,
46 pub(crate) toc_html: Vec<u8>,
47 pub(crate) chrome_css: Vec<u8>,
48 pub(crate) general_css: Vec<u8>,
49 pub(crate) print_css: Vec<u8>,
50 pub(crate) variables_css: Vec<u8>,
51 pub(crate) fonts_css: Option<Vec<u8>>,
52 pub(crate) font_files: Vec<PathBuf>,
53 pub(crate) favicon_png: Option<Vec<u8>>,
54 pub(crate) favicon_svg: Option<Vec<u8>>,
55 pub(crate) js: Vec<u8>,
56 pub(crate) highlight_css: Vec<u8>,
57 pub(crate) tomorrow_night_css: Vec<u8>,
58 pub(crate) ayu_highlight_css: Vec<u8>,
59 pub(crate) highlight_js: Vec<u8>,
60 pub(crate) clipboard_js: Vec<u8>,
61}
62
63impl Theme {
64 pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
67 let theme_dir = theme_dir.as_ref();
68 let mut theme = Theme::default();
69
70 if !theme_dir.exists() || !theme_dir.is_dir() {
72 return theme;
73 }
74
75 {
77 let files = vec![
78 (theme_dir.join("index.hbs"), &mut theme.index),
79 (theme_dir.join("head.hbs"), &mut theme.head),
80 (theme_dir.join("redirect.hbs"), &mut theme.redirect),
81 (theme_dir.join("header.hbs"), &mut theme.header),
82 (theme_dir.join("toc.js.hbs"), &mut theme.toc_js),
83 (theme_dir.join("toc.html.hbs"), &mut theme.toc_html),
84 (theme_dir.join("book.js"), &mut theme.js),
85 (theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
86 (theme_dir.join("css/general.css"), &mut theme.general_css),
87 (theme_dir.join("css/print.css"), &mut theme.print_css),
88 (
89 theme_dir.join("css/variables.css"),
90 &mut theme.variables_css,
91 ),
92 (theme_dir.join("highlight.js"), &mut theme.highlight_js),
93 (theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
94 (theme_dir.join("highlight.css"), &mut theme.highlight_css),
95 (
96 theme_dir.join("tomorrow-night.css"),
97 &mut theme.tomorrow_night_css,
98 ),
99 (
100 theme_dir.join("ayu-highlight.css"),
101 &mut theme.ayu_highlight_css,
102 ),
103 ];
104
105 let load_with_warn = |filename: &Path, dest: &mut Vec<u8>| {
106 if !filename.exists() {
107 return false;
109 }
110 if let Err(e) = load_file_contents(filename, dest) {
111 warn!("Couldn't load custom file, {}: {}", filename.display(), e);
112 false
113 } else {
114 true
115 }
116 };
117
118 for (filename, dest) in files {
119 load_with_warn(&filename, dest);
120 }
121
122 let fonts_dir = theme_dir.join("fonts");
123 if fonts_dir.exists() {
124 let mut fonts_css = Vec::new();
125 if load_with_warn(&fonts_dir.join("fonts.css"), &mut fonts_css) {
126 theme.fonts_css.replace(fonts_css);
127 }
128 if let Ok(entries) = fonts_dir.read_dir() {
129 theme.font_files = entries
130 .filter_map(|entry| {
131 let entry = entry.ok()?;
132 if entry.file_name() == "fonts.css" {
133 None
134 } else if entry.file_type().ok()?.is_dir() {
135 info!("skipping font directory {:?}", entry.path());
136 None
137 } else {
138 Some(entry.path())
139 }
140 })
141 .collect();
142 }
143 }
144
145 let favicon_png = &mut theme.favicon_png.as_mut().unwrap();
148 let png = load_with_warn(&theme_dir.join("favicon.png"), favicon_png);
149 let favicon_svg = &mut theme.favicon_svg.as_mut().unwrap();
150 let svg = load_with_warn(&theme_dir.join("favicon.svg"), favicon_svg);
151 match (png, svg) {
152 (true, true) | (false, false) => {}
153 (true, false) => {
154 theme.favicon_svg = None;
155 }
156 (false, true) => {
157 theme.favicon_png = None;
158 }
159 }
160 }
161
162 theme
163 }
164
165 pub fn copy_theme(html_config: &HtmlConfig, root: &Path) -> Result<()> {
167 let themedir = html_config.theme_dir(root);
168
169 fs::write(themedir.join("book.js"), JS)?;
170 fs::write(themedir.join("favicon.png"), FAVICON_PNG)?;
171 fs::write(themedir.join("favicon.svg"), FAVICON_SVG)?;
172 fs::write(themedir.join("highlight.css"), HIGHLIGHT_CSS)?;
173 fs::write(themedir.join("highlight.js"), HIGHLIGHT_JS)?;
174 fs::write(themedir.join("index.hbs"), INDEX)?;
175
176 let cssdir = themedir.join("css");
177
178 fs::write(cssdir.join("general.css"), GENERAL_CSS)?;
179 fs::write(cssdir.join("chrome.css"), CHROME_CSS)?;
180 fs::write(cssdir.join("variables.css"), VARIABLES_CSS)?;
181 if html_config.print.enable {
182 fs::write(cssdir.join("print.css"), PRINT_CSS)?;
183 }
184
185 fs::write(themedir.join("fonts").join("fonts.css"), fonts::CSS)?;
186 for (file_name, contents) in fonts::LICENSES {
187 fs::write(themedir.join(file_name), contents)?;
188 }
189 for (file_name, contents) in fonts::OPEN_SANS.iter() {
190 fs::write(themedir.join(file_name), contents)?;
191 }
192 fs::write(
193 themedir.join(fonts::SOURCE_CODE_PRO.0),
194 fonts::SOURCE_CODE_PRO.1,
195 )?;
196 Ok(())
197 }
198}
199
200impl Default for Theme {
201 fn default() -> Theme {
202 Theme {
203 index: INDEX.to_owned(),
204 head: HEAD.to_owned(),
205 redirect: REDIRECT.to_owned(),
206 header: HEADER.to_owned(),
207 toc_js: TOC_JS.to_owned(),
208 toc_html: TOC_HTML.to_owned(),
209 chrome_css: CHROME_CSS.to_owned(),
210 general_css: GENERAL_CSS.to_owned(),
211 print_css: PRINT_CSS.to_owned(),
212 variables_css: VARIABLES_CSS.to_owned(),
213 fonts_css: None,
214 font_files: Vec::new(),
215 favicon_png: Some(FAVICON_PNG.to_owned()),
216 favicon_svg: Some(FAVICON_SVG.to_owned()),
217 js: JS.to_owned(),
218 highlight_css: HIGHLIGHT_CSS.to_owned(),
219 tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
220 ayu_highlight_css: AYU_HIGHLIGHT_CSS.to_owned(),
221 highlight_js: HIGHLIGHT_JS.to_owned(),
222 clipboard_js: CLIPBOARD_JS.to_owned(),
223 }
224 }
225}
226
227fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result<()> {
230 let filename = filename.as_ref();
231 let mut buffer = std::fs::read(filename)?;
232
233 dest.clear();
236 dest.append(&mut buffer);
237
238 Ok(())
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use std::fs;
245 use tempfile::Builder as TempFileBuilder;
246
247 #[test]
248 fn theme_uses_defaults_with_nonexistent_src_dir() {
249 let non_existent = PathBuf::from("/non/existent/directory/");
250 assert!(!non_existent.exists());
251
252 let should_be = Theme::default();
253 let got = Theme::new(&non_existent);
254
255 assert_eq!(got, should_be);
256 }
257
258 #[test]
259 fn theme_dir_overrides_defaults() {
260 let files = [
261 "index.hbs",
262 "head.hbs",
263 "redirect.hbs",
264 "header.hbs",
265 "toc.js.hbs",
266 "toc.html.hbs",
267 "favicon.png",
268 "favicon.svg",
269 "css/chrome.css",
270 "css/general.css",
271 "css/print.css",
272 "css/variables.css",
273 "fonts/fonts.css",
274 "book.js",
275 "highlight.js",
276 "tomorrow-night.css",
277 "highlight.css",
278 "ayu-highlight.css",
279 "clipboard.min.js",
280 ];
281
282 let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
283 fs::create_dir(temp.path().join("css")).unwrap();
284 fs::create_dir(temp.path().join("fonts")).unwrap();
285
286 for file in &files {
288 fs::File::create(&temp.path().join(file)).unwrap();
289 }
290
291 let got = Theme::new(temp.path());
292
293 let empty = Theme {
294 index: Vec::new(),
295 head: Vec::new(),
296 redirect: Vec::new(),
297 header: Vec::new(),
298 toc_js: Vec::new(),
299 toc_html: Vec::new(),
300 chrome_css: Vec::new(),
301 general_css: Vec::new(),
302 print_css: Vec::new(),
303 variables_css: Vec::new(),
304 fonts_css: Some(Vec::new()),
305 font_files: Vec::new(),
306 favicon_png: Some(Vec::new()),
307 favicon_svg: Some(Vec::new()),
308 js: Vec::new(),
309 highlight_css: Vec::new(),
310 tomorrow_night_css: Vec::new(),
311 ayu_highlight_css: Vec::new(),
312 highlight_js: Vec::new(),
313 clipboard_js: Vec::new(),
314 };
315
316 assert_eq!(got, empty);
317 }
318
319 #[test]
320 fn favicon_override() {
321 let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
322 fs::write(temp.path().join("favicon.png"), "1234").unwrap();
323 let got = Theme::new(temp.path());
324 assert_eq!(got.favicon_png.as_ref().unwrap(), b"1234");
325 assert_eq!(got.favicon_svg, None);
326
327 let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
328 fs::write(temp.path().join("favicon.svg"), "4567").unwrap();
329 let got = Theme::new(temp.path());
330 assert_eq!(got.favicon_png, None);
331 assert_eq!(got.favicon_svg.as_ref().unwrap(), b"4567");
332 }
333}