1 | #![allow (missing_docs)] |
2 | |
3 | pub mod playground_editor; |
4 | |
5 | pub mod fonts; |
6 | |
7 | #[cfg (feature = "search" )] |
8 | pub mod searcher; |
9 | |
10 | use std::fs::File; |
11 | use std::io::Read; |
12 | use std::path::{Path, PathBuf}; |
13 | |
14 | use crate::errors::*; |
15 | use log::warn; |
16 | pub static INDEX: &[u8] = include_bytes!("index.hbs" ); |
17 | pub static HEAD: &[u8] = include_bytes!("head.hbs" ); |
18 | pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs" ); |
19 | pub static HEADER: &[u8] = include_bytes!("header.hbs" ); |
20 | pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css" ); |
21 | pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css" ); |
22 | pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css" ); |
23 | pub static VARIABLES_CSS: &[u8] = include_bytes!("css/variables.css" ); |
24 | pub static FAVICON_PNG: &[u8] = include_bytes!("favicon.png" ); |
25 | pub static FAVICON_SVG: &[u8] = include_bytes!("favicon.svg" ); |
26 | pub static JS: &[u8] = include_bytes!("book.js" ); |
27 | pub static HIGHLIGHT_JS: &[u8] = include_bytes!("highlight.js" ); |
28 | pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("tomorrow-night.css" ); |
29 | pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("highlight.css" ); |
30 | pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("ayu-highlight.css" ); |
31 | pub static CLIPBOARD_JS: &[u8] = include_bytes!("clipboard.min.js" ); |
32 | pub static FONT_AWESOME: &[u8] = include_bytes!("FontAwesome/css/font-awesome.min.css" ); |
33 | pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot" ); |
34 | pub static FONT_AWESOME_SVG: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg" ); |
35 | pub static FONT_AWESOME_TTF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf" ); |
36 | pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff" ); |
37 | pub static FONT_AWESOME_WOFF2: &[u8] = |
38 | include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff2" ); |
39 | pub 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)] |
48 | pub 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 | |
69 | impl 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 | |
170 | impl 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. |
197 | fn 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)] |
212 | mod 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 | |