1#![allow(missing_docs)]
2
3pub mod playground_editor;
4
5pub mod fonts;
6
7#[cfg(feature = "search")]
8pub mod searcher;
9
10use std::fs::File;
11use std::io::Read;
12use std::path::{Path, PathBuf};
13
14use crate::errors::*;
15use log::warn;
16pub static INDEX: &[u8] = include_bytes!("index.hbs");
17pub static HEAD: &[u8] = include_bytes!("head.hbs");
18pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
19pub static HEADER: &[u8] = include_bytes!("header.hbs");
20pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
21pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
22pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
23pub static VARIABLES_CSS: &[u8] = include_bytes!("css/variables.css");
24pub static FAVICON_PNG: &[u8] = include_bytes!("favicon.png");
25pub static FAVICON_SVG: &[u8] = include_bytes!("favicon.svg");
26pub static JS: &[u8] = include_bytes!("book.js");
27pub static HIGHLIGHT_JS: &[u8] = include_bytes!("highlight.js");
28pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("tomorrow-night.css");
29pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("highlight.css");
30pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("ayu-highlight.css");
31pub static CLIPBOARD_JS: &[u8] = include_bytes!("clipboard.min.js");
32pub static FONT_AWESOME: &[u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
33pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
34pub static FONT_AWESOME_SVG: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
35pub static FONT_AWESOME_TTF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
36pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
37pub static FONT_AWESOME_WOFF2: &[u8] =
38 include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff2");
39pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
40
41/// The `Theme` struct should be used instead of the static variables because
42/// the `new()` method will look if the user has a theme directory in their
43/// source folder and use the users theme instead of the default.
44///
45/// You should only ever use the static variables directly if you want to
46/// override the user's theme with the defaults.
47#[derive(Debug, PartialEq)]
48pub struct Theme {
49 pub index: Vec<u8>,
50 pub head: Vec<u8>,
51 pub redirect: Vec<u8>,
52 pub header: Vec<u8>,
53 pub chrome_css: Vec<u8>,
54 pub general_css: Vec<u8>,
55 pub print_css: Vec<u8>,
56 pub variables_css: Vec<u8>,
57 pub fonts_css: Option<Vec<u8>>,
58 pub font_files: Vec<PathBuf>,
59 pub favicon_png: Option<Vec<u8>>,
60 pub favicon_svg: Option<Vec<u8>>,
61 pub js: Vec<u8>,
62 pub highlight_css: Vec<u8>,
63 pub tomorrow_night_css: Vec<u8>,
64 pub ayu_highlight_css: Vec<u8>,
65 pub highlight_js: Vec<u8>,
66 pub clipboard_js: Vec<u8>,
67}
68
69impl Theme {
70 /// Creates a `Theme` from the given `theme_dir`.
71 /// If a file is found in the theme dir, it will override the default version.
72 pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
73 let theme_dir = theme_dir.as_ref();
74 let mut theme = Theme::default();
75
76 // If the theme directory doesn't exist there's no point continuing...
77 if !theme_dir.exists() || !theme_dir.is_dir() {
78 return theme;
79 }
80
81 // Check for individual files, if they exist copy them across
82 {
83 let files = vec![
84 (theme_dir.join("index.hbs"), &mut theme.index),
85 (theme_dir.join("head.hbs"), &mut theme.head),
86 (theme_dir.join("redirect.hbs"), &mut theme.redirect),
87 (theme_dir.join("header.hbs"), &mut theme.header),
88 (theme_dir.join("book.js"), &mut theme.js),
89 (theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
90 (theme_dir.join("css/general.css"), &mut theme.general_css),
91 (theme_dir.join("css/print.css"), &mut theme.print_css),
92 (
93 theme_dir.join("css/variables.css"),
94 &mut theme.variables_css,
95 ),
96 (theme_dir.join("highlight.js"), &mut theme.highlight_js),
97 (theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
98 (theme_dir.join("highlight.css"), &mut theme.highlight_css),
99 (
100 theme_dir.join("tomorrow-night.css"),
101 &mut theme.tomorrow_night_css,
102 ),
103 (
104 theme_dir.join("ayu-highlight.css"),
105 &mut theme.ayu_highlight_css,
106 ),
107 ];
108
109 let load_with_warn = |filename: &Path, dest: &mut Vec<u8>| {
110 if !filename.exists() {
111 // Don't warn if the file doesn't exist.
112 return false;
113 }
114 if let Err(e) = load_file_contents(filename, dest) {
115 warn!("Couldn't load custom file, {}: {}", filename.display(), e);
116 false
117 } else {
118 true
119 }
120 };
121
122 for (filename, dest) in files {
123 load_with_warn(&filename, dest);
124 }
125
126 let fonts_dir = theme_dir.join("fonts");
127 if fonts_dir.exists() {
128 let mut fonts_css = Vec::new();
129 if load_with_warn(&fonts_dir.join("fonts.css"), &mut fonts_css) {
130 theme.fonts_css.replace(fonts_css);
131 }
132 if let Ok(entries) = fonts_dir.read_dir() {
133 theme.font_files = entries
134 .filter_map(|entry| {
135 let entry = entry.ok()?;
136 if entry.file_name() == "fonts.css" {
137 None
138 } else if entry.file_type().ok()?.is_dir() {
139 log::info!("skipping font directory {:?}", entry.path());
140 None
141 } else {
142 Some(entry.path())
143 }
144 })
145 .collect();
146 }
147 }
148
149 // If the user overrides one favicon, but not the other, do not
150 // copy the default for the other.
151 let favicon_png = &mut theme.favicon_png.as_mut().unwrap();
152 let png = load_with_warn(&theme_dir.join("favicon.png"), favicon_png);
153 let favicon_svg = &mut theme.favicon_svg.as_mut().unwrap();
154 let svg = load_with_warn(&theme_dir.join("favicon.svg"), favicon_svg);
155 match (png, svg) {
156 (true, true) | (false, false) => {}
157 (true, false) => {
158 theme.favicon_svg = None;
159 }
160 (false, true) => {
161 theme.favicon_png = None;
162 }
163 }
164 }
165
166 theme
167 }
168}
169
170impl Default for Theme {
171 fn default() -> Theme {
172 Theme {
173 index: INDEX.to_owned(),
174 head: HEAD.to_owned(),
175 redirect: REDIRECT.to_owned(),
176 header: HEADER.to_owned(),
177 chrome_css: CHROME_CSS.to_owned(),
178 general_css: GENERAL_CSS.to_owned(),
179 print_css: PRINT_CSS.to_owned(),
180 variables_css: VARIABLES_CSS.to_owned(),
181 fonts_css: None,
182 font_files: Vec::new(),
183 favicon_png: Some(FAVICON_PNG.to_owned()),
184 favicon_svg: Some(FAVICON_SVG.to_owned()),
185 js: JS.to_owned(),
186 highlight_css: HIGHLIGHT_CSS.to_owned(),
187 tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
188 ayu_highlight_css: AYU_HIGHLIGHT_CSS.to_owned(),
189 highlight_js: HIGHLIGHT_JS.to_owned(),
190 clipboard_js: CLIPBOARD_JS.to_owned(),
191 }
192 }
193}
194
195/// Checks if a file exists, if so, the destination buffer will be filled with
196/// its contents.
197fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result<()> {
198 let filename: &Path = filename.as_ref();
199
200 let mut buffer: Vec = Vec::new();
201 File::open(filename)?.read_to_end(&mut buffer)?;
202
203 // We needed the buffer so we'd only overwrite the existing content if we
204 // could successfully load the file into memory.
205 dest.clear();
206 dest.append(&mut buffer);
207
208 Ok(())
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use std::fs;
215 use std::path::PathBuf;
216 use tempfile::Builder as TempFileBuilder;
217
218 #[test]
219 fn theme_uses_defaults_with_nonexistent_src_dir() {
220 let non_existent = PathBuf::from("/non/existent/directory/");
221 assert!(!non_existent.exists());
222
223 let should_be = Theme::default();
224 let got = Theme::new(&non_existent);
225
226 assert_eq!(got, should_be);
227 }
228
229 #[test]
230 fn theme_dir_overrides_defaults() {
231 let files = [
232 "index.hbs",
233 "head.hbs",
234 "redirect.hbs",
235 "header.hbs",
236 "favicon.png",
237 "favicon.svg",
238 "css/chrome.css",
239 "css/general.css",
240 "css/print.css",
241 "css/variables.css",
242 "fonts/fonts.css",
243 "book.js",
244 "highlight.js",
245 "tomorrow-night.css",
246 "highlight.css",
247 "ayu-highlight.css",
248 "clipboard.min.js",
249 ];
250
251 let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
252 fs::create_dir(temp.path().join("css")).unwrap();
253 fs::create_dir(temp.path().join("fonts")).unwrap();
254
255 // "touch" all of the special files so we have empty copies
256 for file in &files {
257 File::create(&temp.path().join(file)).unwrap();
258 }
259
260 let got = Theme::new(temp.path());
261
262 let empty = Theme {
263 index: Vec::new(),
264 head: Vec::new(),
265 redirect: Vec::new(),
266 header: Vec::new(),
267 chrome_css: Vec::new(),
268 general_css: Vec::new(),
269 print_css: Vec::new(),
270 variables_css: Vec::new(),
271 fonts_css: Some(Vec::new()),
272 font_files: Vec::new(),
273 favicon_png: Some(Vec::new()),
274 favicon_svg: Some(Vec::new()),
275 js: Vec::new(),
276 highlight_css: Vec::new(),
277 tomorrow_night_css: Vec::new(),
278 ayu_highlight_css: Vec::new(),
279 highlight_js: Vec::new(),
280 clipboard_js: Vec::new(),
281 };
282
283 assert_eq!(got, empty);
284 }
285
286 #[test]
287 fn favicon_override() {
288 let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
289 fs::write(temp.path().join("favicon.png"), "1234").unwrap();
290 let got = Theme::new(temp.path());
291 assert_eq!(got.favicon_png.as_ref().unwrap(), b"1234");
292 assert_eq!(got.favicon_svg, None);
293
294 let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
295 fs::write(temp.path().join("favicon.svg"), "4567").unwrap();
296 let got = Theme::new(temp.path());
297 assert_eq!(got.favicon_png, None);
298 assert_eq!(got.favicon_svg.as_ref().unwrap(), b"4567");
299 }
300}
301