Skip to main content

mdbook_fork4ls/front-end/
mod.rs

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