mdbook/theme/
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;
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 favicon_png: Option<Vec<u8>>,
58    pub favicon_svg: Option<Vec<u8>>,
59    pub js: Vec<u8>,
60    pub highlight_css: Vec<u8>,
61    pub tomorrow_night_css: Vec<u8>,
62    pub ayu_highlight_css: Vec<u8>,
63    pub highlight_js: Vec<u8>,
64    pub clipboard_js: Vec<u8>,
65}
66
67impl Theme {
68    /// Creates a `Theme` from the given `theme_dir`.
69    /// If a file is found in the theme dir, it will override the default version.
70    pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
71        let theme_dir = theme_dir.as_ref();
72        let mut theme = Theme::default();
73
74        // If the theme directory doesn't exist there's no point continuing...
75        if !theme_dir.exists() || !theme_dir.is_dir() {
76            return theme;
77        }
78
79        // Check for individual files, if they exist copy them across
80        {
81            let files = vec![
82                (theme_dir.join("index.hbs"), &mut theme.index),
83                (theme_dir.join("head.hbs"), &mut theme.head),
84                (theme_dir.join("redirect.hbs"), &mut theme.redirect),
85                (theme_dir.join("header.hbs"), &mut theme.header),
86                (theme_dir.join("book.js"), &mut theme.js),
87                (theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
88                (theme_dir.join("css/general.css"), &mut theme.general_css),
89                (theme_dir.join("css/print.css"), &mut theme.print_css),
90                (
91                    theme_dir.join("css/variables.css"),
92                    &mut theme.variables_css,
93                ),
94                (theme_dir.join("highlight.js"), &mut theme.highlight_js),
95                (theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
96                (theme_dir.join("highlight.css"), &mut theme.highlight_css),
97                (
98                    theme_dir.join("tomorrow-night.css"),
99                    &mut theme.tomorrow_night_css,
100                ),
101                (
102                    theme_dir.join("ayu-highlight.css"),
103                    &mut theme.ayu_highlight_css,
104                ),
105            ];
106
107            let load_with_warn = |filename: &Path, dest| {
108                if !filename.exists() {
109                    // Don't warn if the file doesn't exist.
110                    return false;
111                }
112                if let Err(e) = load_file_contents(filename, dest) {
113                    warn!("Couldn't load custom file, {}: {}", filename.display(), e);
114                    false
115                } else {
116                    true
117                }
118            };
119
120            for (filename, dest) in files {
121                load_with_warn(&filename, dest);
122            }
123
124            // If the user overrides one favicon, but not the other, do not
125            // copy the default for the other.
126            let favicon_png = &mut theme.favicon_png.as_mut().unwrap();
127            let png = load_with_warn(&theme_dir.join("favicon.png"), favicon_png);
128            let favicon_svg = &mut theme.favicon_svg.as_mut().unwrap();
129            let svg = load_with_warn(&theme_dir.join("favicon.svg"), favicon_svg);
130            match (png, svg) {
131                (true, true) | (false, false) => {}
132                (true, false) => {
133                    theme.favicon_svg = None;
134                }
135                (false, true) => {
136                    theme.favicon_png = None;
137                }
138            }
139        }
140
141        theme
142    }
143}
144
145impl Default for Theme {
146    fn default() -> Theme {
147        Theme {
148            index: INDEX.to_owned(),
149            head: HEAD.to_owned(),
150            redirect: REDIRECT.to_owned(),
151            header: HEADER.to_owned(),
152            chrome_css: CHROME_CSS.to_owned(),
153            general_css: GENERAL_CSS.to_owned(),
154            print_css: PRINT_CSS.to_owned(),
155            variables_css: VARIABLES_CSS.to_owned(),
156            favicon_png: Some(FAVICON_PNG.to_owned()),
157            favicon_svg: Some(FAVICON_SVG.to_owned()),
158            js: JS.to_owned(),
159            highlight_css: HIGHLIGHT_CSS.to_owned(),
160            tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
161            ayu_highlight_css: AYU_HIGHLIGHT_CSS.to_owned(),
162            highlight_js: HIGHLIGHT_JS.to_owned(),
163            clipboard_js: CLIPBOARD_JS.to_owned(),
164        }
165    }
166}
167
168/// Checks if a file exists, if so, the destination buffer will be filled with
169/// its contents.
170fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result<()> {
171    let filename = filename.as_ref();
172
173    let mut buffer = Vec::new();
174    File::open(filename)?.read_to_end(&mut buffer)?;
175
176    // We needed the buffer so we'd only overwrite the existing content if we
177    // could successfully load the file into memory.
178    dest.clear();
179    dest.append(&mut buffer);
180
181    Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use std::fs;
188    use std::path::PathBuf;
189    use tempfile::Builder as TempFileBuilder;
190
191    #[test]
192    fn theme_uses_defaults_with_nonexistent_src_dir() {
193        let non_existent = PathBuf::from("/non/existent/directory/");
194        assert!(!non_existent.exists());
195
196        let should_be = Theme::default();
197        let got = Theme::new(&non_existent);
198
199        assert_eq!(got, should_be);
200    }
201
202    #[test]
203    fn theme_dir_overrides_defaults() {
204        let files = [
205            "index.hbs",
206            "head.hbs",
207            "redirect.hbs",
208            "header.hbs",
209            "favicon.png",
210            "favicon.svg",
211            "css/chrome.css",
212            "css/fonts.css",
213            "css/general.css",
214            "css/print.css",
215            "css/variables.css",
216            "book.js",
217            "highlight.js",
218            "tomorrow-night.css",
219            "highlight.css",
220            "ayu-highlight.css",
221            "clipboard.min.js",
222        ];
223
224        let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
225        fs::create_dir(temp.path().join("css")).unwrap();
226
227        // "touch" all of the special files so we have empty copies
228        for file in &files {
229            File::create(&temp.path().join(file)).unwrap();
230        }
231
232        let got = Theme::new(temp.path());
233
234        let empty = Theme {
235            index: Vec::new(),
236            head: Vec::new(),
237            redirect: Vec::new(),
238            header: Vec::new(),
239            chrome_css: Vec::new(),
240            general_css: Vec::new(),
241            print_css: Vec::new(),
242            variables_css: Vec::new(),
243            favicon_png: Some(Vec::new()),
244            favicon_svg: Some(Vec::new()),
245            js: Vec::new(),
246            highlight_css: Vec::new(),
247            tomorrow_night_css: Vec::new(),
248            ayu_highlight_css: Vec::new(),
249            highlight_js: Vec::new(),
250            clipboard_js: Vec::new(),
251        };
252
253        assert_eq!(got, empty);
254    }
255
256    #[test]
257    fn favicon_override() {
258        let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
259        fs::write(temp.path().join("favicon.png"), "1234").unwrap();
260        let got = Theme::new(temp.path());
261        assert_eq!(got.favicon_png.as_ref().unwrap(), b"1234");
262        assert_eq!(got.favicon_svg, None);
263
264        let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
265        fs::write(temp.path().join("favicon.svg"), "4567").unwrap();
266        let got = Theme::new(temp.path());
267        assert_eq!(got.favicon_png, None);
268        assert_eq!(got.favicon_svg.as_ref().unwrap(), b"4567");
269    }
270}