]>
Commit | Line | Data |
---|---|---|
2c00a5a8 XL |
1 | //! The internal representation of a book and infrastructure for loading it from |
2 | //! disk and building it. | |
3 | //! | |
4 | //! For examples on using `MDBook`, consult the [top-level documentation][1]. | |
5 | //! | |
6 | //! [1]: ../index.html | |
7 | ||
f035d41b | 8 | #[allow(clippy::module_inception)] |
2c00a5a8 XL |
9 | mod book; |
10 | mod init; | |
9fa01778 | 11 | mod summary; |
2c00a5a8 XL |
12 | |
13 | pub use self::book::{load_book, Book, BookItem, BookItems, Chapter}; | |
2c00a5a8 | 14 | pub use self::init::BookBuilder; |
9fa01778 | 15 | pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; |
2c00a5a8 | 16 | |
2c00a5a8 | 17 | use std::io::Write; |
9fa01778 | 18 | use std::path::PathBuf; |
cc61c64b | 19 | use std::process::Command; |
dc9dc135 | 20 | use std::string::ToString; |
83c7162d | 21 | use tempfile::Builder as TempFileBuilder; |
2c00a5a8 | 22 | use toml::Value; |
cc61c64b | 23 | |
dc9dc135 XL |
24 | use crate::errors::*; |
25 | use crate::preprocess::{ | |
9fa01778 XL |
26 | CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext, |
27 | }; | |
e74abb32 | 28 | use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer}; |
dc9dc135 | 29 | use crate::utils; |
cc61c64b | 30 | |
f035d41b | 31 | use crate::config::{Config, RustEdition}; |
cc61c64b | 32 | |
2c00a5a8 | 33 | /// The object used to manage and build a book. |
cc61c64b | 34 | pub struct MDBook { |
2c00a5a8 XL |
35 | /// The book's root directory. |
36 | pub root: PathBuf, | |
37 | /// The configuration used to tweak now a book is built. | |
38 | pub config: Config, | |
39 | /// A representation of the book's contents in memory. | |
40 | pub book: Book, | |
dc9dc135 | 41 | renderers: Vec<Box<dyn Renderer>>, |
2c00a5a8 XL |
42 | |
43 | /// List of pre-processors to be run on the book | |
dc9dc135 | 44 | preprocessors: Vec<Box<dyn Preprocessor>>, |
cc61c64b XL |
45 | } |
46 | ||
47 | impl MDBook { | |
2c00a5a8 XL |
48 | /// Load a book from its root directory on disk. |
49 | pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> { | |
50 | let book_root = book_root.into(); | |
51 | let config_location = book_root.join("book.toml"); | |
52 | ||
53 | // the book.json file is no longer used, so we should emit a warning to | |
54 | // let people know to migrate to book.toml | |
55 | if book_root.join("book.json").exists() { | |
56 | warn!("It appears you are still using book.json for configuration."); | |
57 | warn!("This format is no longer used, so you should migrate to the"); | |
58 | warn!("book.toml format."); | |
59 | warn!("Check the user guide for migration information:"); | |
e74abb32 | 60 | warn!("\thttps://rust-lang.github.io/mdBook/format/config.html"); |
2c00a5a8 | 61 | } |
cc61c64b | 62 | |
2c00a5a8 XL |
63 | let mut config = if config_location.exists() { |
64 | debug!("Loading config from {}", config_location.display()); | |
65 | Config::from_disk(&config_location)? | |
66 | } else { | |
67 | Config::default() | |
68 | }; | |
cc61c64b | 69 | |
2c00a5a8 XL |
70 | config.update_from_env(); |
71 | ||
dc9dc135 | 72 | if log_enabled!(log::Level::Trace) { |
2c00a5a8 XL |
73 | for line in format!("Config: {:#?}", config).lines() { |
74 | trace!("{}", line); | |
75 | } | |
cc61c64b XL |
76 | } |
77 | ||
2c00a5a8 XL |
78 | MDBook::load_with_config(book_root, config) |
79 | } | |
cc61c64b | 80 | |
2c00a5a8 XL |
81 | /// Load a book from its root directory using a custom config. |
82 | pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> { | |
83 | let root = book_root.into(); | |
cc61c64b | 84 | |
2c00a5a8 XL |
85 | let src_dir = root.join(&config.book.src); |
86 | let book = book::load_book(&src_dir, &config.build)?; | |
87 | ||
88 | let renderers = determine_renderers(&config); | |
89 | let preprocessors = determine_preprocessors(&config)?; | |
90 | ||
91 | Ok(MDBook { | |
92 | root, | |
93 | config, | |
94 | book, | |
95 | renderers, | |
96 | preprocessors, | |
97 | }) | |
cc61c64b XL |
98 | } |
99 | ||
dc9dc135 XL |
100 | /// Load a book from its root directory using a custom config and a custom summary. |
101 | pub fn load_with_config_and_summary<P: Into<PathBuf>>( | |
102 | book_root: P, | |
103 | config: Config, | |
104 | summary: Summary, | |
105 | ) -> Result<MDBook> { | |
106 | let root = book_root.into(); | |
107 | ||
108 | let src_dir = root.join(&config.book.src); | |
109 | let book = book::load_book_from_disk(&summary, &src_dir)?; | |
110 | ||
111 | let renderers = determine_renderers(&config); | |
112 | let preprocessors = determine_preprocessors(&config)?; | |
113 | ||
114 | Ok(MDBook { | |
115 | root, | |
116 | config, | |
117 | book, | |
118 | renderers, | |
119 | preprocessors, | |
120 | }) | |
121 | } | |
122 | ||
041b39d2 XL |
123 | /// Returns a flat depth-first iterator over the elements of the book, |
124 | /// it returns an [BookItem enum](bookitem.html): | |
cc61c64b XL |
125 | /// `(section: String, bookitem: &BookItem)` |
126 | /// | |
127 | /// ```no_run | |
cc61c64b | 128 | /// # use mdbook::MDBook; |
2c00a5a8 | 129 | /// # use mdbook::book::BookItem; |
2c00a5a8 | 130 | /// # let book = MDBook::load("mybook").unwrap(); |
cc61c64b | 131 | /// for item in book.iter() { |
2c00a5a8 XL |
132 | /// match *item { |
133 | /// BookItem::Chapter(ref chapter) => {}, | |
134 | /// BookItem::Separator => {}, | |
f035d41b | 135 | /// BookItem::PartTitle(ref title) => {} |
cc61c64b XL |
136 | /// } |
137 | /// } | |
138 | /// | |
139 | /// // would print something like this: | |
140 | /// // 1. Chapter 1 | |
141 | /// // 1.1 Sub Chapter | |
142 | /// // 1.2 Sub Chapter | |
143 | /// // 2. Chapter 2 | |
144 | /// // | |
145 | /// // etc. | |
cc61c64b | 146 | /// ``` |
dc9dc135 | 147 | pub fn iter(&self) -> BookItems<'_> { |
2c00a5a8 | 148 | self.book.iter() |
cc61c64b XL |
149 | } |
150 | ||
2c00a5a8 XL |
151 | /// `init()` gives you a `BookBuilder` which you can use to setup a new book |
152 | /// and its accompanying directory structure. | |
153 | /// | |
154 | /// The `BookBuilder` creates some boilerplate files and directories to get | |
155 | /// you started with your book. | |
cc61c64b XL |
156 | /// |
157 | /// ```text | |
158 | /// book-test/ | |
159 | /// ├── book | |
160 | /// └── src | |
161 | /// ├── chapter_1.md | |
162 | /// └── SUMMARY.md | |
163 | /// ``` | |
164 | /// | |
2c00a5a8 XL |
165 | /// It uses the path provided as the root directory for your book, then adds |
166 | /// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file | |
167 | /// to get you started. | |
168 | pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder { | |
169 | BookBuilder::new(book_root) | |
170 | } | |
cc61c64b | 171 | |
2c00a5a8 XL |
172 | /// Tells the renderer to build our book and put it in the build directory. |
173 | pub fn build(&self) -> Result<()> { | |
174 | info!("Book building has started"); | |
cc61c64b | 175 | |
9fa01778 XL |
176 | for renderer in &self.renderers { |
177 | self.execute_build_process(&**renderer)?; | |
178 | } | |
179 | ||
180 | Ok(()) | |
181 | } | |
182 | ||
183 | /// Run the entire build process for a particular `Renderer`. | |
f9f354fc | 184 | pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> { |
2c00a5a8 | 185 | let mut preprocessed_book = self.book.clone(); |
9fa01778 XL |
186 | let preprocess_ctx = PreprocessorContext::new( |
187 | self.root.clone(), | |
188 | self.config.clone(), | |
189 | renderer.name().to_string(), | |
190 | ); | |
cc61c64b | 191 | |
2c00a5a8 | 192 | for preprocessor in &self.preprocessors { |
9fa01778 XL |
193 | if preprocessor_should_run(&**preprocessor, renderer, &self.config) { |
194 | debug!("Running the {} preprocessor.", preprocessor.name()); | |
195 | preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?; | |
196 | } | |
cc61c64b XL |
197 | } |
198 | ||
9fa01778 XL |
199 | info!("Running the {} backend", renderer.name()); |
200 | self.render(&preprocessed_book, renderer)?; | |
cc61c64b | 201 | |
2c00a5a8 XL |
202 | Ok(()) |
203 | } | |
cc61c64b | 204 | |
dc9dc135 | 205 | fn render(&self, preprocessed_book: &Book, renderer: &dyn Renderer) -> Result<()> { |
2c00a5a8 XL |
206 | let name = renderer.name(); |
207 | let build_dir = self.build_dir_for(name); | |
cc61c64b | 208 | |
2c00a5a8 XL |
209 | let render_context = RenderContext::new( |
210 | self.root.clone(), | |
211 | preprocessed_book.clone(), | |
212 | self.config.clone(), | |
213 | build_dir, | |
214 | ); | |
cc61c64b | 215 | |
2c00a5a8 XL |
216 | renderer |
217 | .render(&render_context) | |
f035d41b | 218 | .with_context(|| "Rendering failed") |
2c00a5a8 | 219 | } |
7cac9316 | 220 | |
2c00a5a8 XL |
221 | /// You can change the default renderer to another one by using this method. |
222 | /// The only requirement is for your renderer to implement the [`Renderer` | |
223 | /// trait](../renderer/trait.Renderer.html) | |
224 | pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self { | |
225 | self.renderers.push(Box::new(renderer)); | |
226 | self | |
227 | } | |
cc61c64b | 228 | |
2c00a5a8 | 229 | /// Register a [`Preprocessor`](../preprocess/trait.Preprocessor.html) to be used when rendering the book. |
dc9dc135 | 230 | pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self { |
2c00a5a8 XL |
231 | self.preprocessors.push(Box::new(preprocessor)); |
232 | self | |
cc61c64b XL |
233 | } |
234 | ||
2c00a5a8 XL |
235 | /// Run `rustdoc` tests on the book, linking against the provided libraries. |
236 | pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> { | |
237 | let library_args: Vec<&str> = (0..library_paths.len()) | |
238 | .map(|_| "-L") | |
239 | .zip(library_paths.into_iter()) | |
240 | .flat_map(|x| vec![x.0, x.1]) | |
241 | .collect(); | |
cc61c64b | 242 | |
9fa01778 | 243 | let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?; |
cc61c64b | 244 | |
9fa01778 XL |
245 | // FIXME: Is "test" the proper renderer name to use here? |
246 | let preprocess_context = | |
247 | PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string()); | |
cc61c64b | 248 | |
9fa01778 XL |
249 | let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?; |
250 | // Index Preprocessor is disabled so that chapter paths continue to point to the | |
251 | // actual markdown files. | |
cc61c64b | 252 | |
1b1a35ee | 253 | let mut failed = false; |
9fa01778 | 254 | for item in book.iter() { |
2c00a5a8 | 255 | if let BookItem::Chapter(ref ch) = *item { |
f035d41b XL |
256 | let chapter_path = match ch.path { |
257 | Some(ref path) if !path.as_os_str().is_empty() => path, | |
258 | _ => continue, | |
259 | }; | |
260 | ||
261 | let path = self.source_dir().join(&chapter_path); | |
262 | info!("Testing file: {:?}", path); | |
263 | ||
264 | // write preprocessed file to tempdir | |
265 | let path = temp_dir.path().join(&chapter_path); | |
266 | let mut tmpf = utils::fs::create_file(&path)?; | |
267 | tmpf.write_all(ch.content.as_bytes())?; | |
268 | ||
269 | let mut cmd = Command::new("rustdoc"); | |
270 | cmd.arg(&path).arg("--test").args(&library_args); | |
271 | ||
272 | if let Some(edition) = self.config.rust.edition { | |
273 | match edition { | |
274 | RustEdition::E2015 => { | |
275 | cmd.args(&["--edition", "2015"]); | |
276 | } | |
277 | RustEdition::E2018 => { | |
278 | cmd.args(&["--edition", "2018"]); | |
279 | } | |
2c00a5a8 XL |
280 | } |
281 | } | |
f035d41b XL |
282 | |
283 | let output = cmd.output()?; | |
284 | ||
285 | if !output.status.success() { | |
1b1a35ee XL |
286 | failed = true; |
287 | error!( | |
f035d41b XL |
288 | "rustdoc returned an error:\n\ |
289 | \n--- stdout\n{}\n--- stderr\n{}", | |
290 | String::from_utf8_lossy(&output.stdout), | |
291 | String::from_utf8_lossy(&output.stderr) | |
292 | ); | |
293 | } | |
2c00a5a8 | 294 | } |
cc61c64b | 295 | } |
1b1a35ee XL |
296 | if failed { |
297 | bail!("One or more tests failed"); | |
298 | } | |
2c00a5a8 | 299 | Ok(()) |
cc61c64b XL |
300 | } |
301 | ||
2c00a5a8 XL |
302 | /// The logic for determining where a backend should put its build |
303 | /// artefacts. | |
cc61c64b | 304 | /// |
2c00a5a8 XL |
305 | /// If there is only 1 renderer, put it in the directory pointed to by the |
306 | /// `build.build_dir` key in `Config`. If there is more than one then the | |
307 | /// renderer gets its own directory within the main build dir. | |
308 | /// | |
309 | /// i.e. If there were only one renderer (in this case, the HTML renderer): | |
310 | /// | |
311 | /// - build/ | |
312 | /// - index.html | |
313 | /// - ... | |
314 | /// | |
315 | /// Otherwise if there are multiple: | |
316 | /// | |
317 | /// - build/ | |
318 | /// - epub/ | |
319 | /// - my_awesome_book.epub | |
320 | /// - html/ | |
321 | /// - index.html | |
322 | /// - ... | |
323 | /// - latex/ | |
324 | /// - my_awesome_book.tex | |
325 | /// | |
326 | pub fn build_dir_for(&self, backend_name: &str) -> PathBuf { | |
327 | let build_dir = self.root.join(&self.config.build.build_dir); | |
cc61c64b | 328 | |
2c00a5a8 XL |
329 | if self.renderers.len() <= 1 { |
330 | build_dir | |
331 | } else { | |
332 | build_dir.join(backend_name) | |
333 | } | |
cc61c64b XL |
334 | } |
335 | ||
2c00a5a8 XL |
336 | /// Get the directory containing this book's source files. |
337 | pub fn source_dir(&self) -> PathBuf { | |
338 | self.root.join(&self.config.book.src) | |
cc61c64b XL |
339 | } |
340 | ||
83c7162d | 341 | /// Get the directory containing the theme resources for the book. |
2c00a5a8 | 342 | pub fn theme_dir(&self) -> PathBuf { |
83c7162d XL |
343 | self.config |
344 | .html_config() | |
345 | .unwrap_or_default() | |
346 | .theme_dir(&self.root) | |
cc61c64b | 347 | } |
2c00a5a8 | 348 | } |
cc61c64b | 349 | |
2c00a5a8 | 350 | /// Look at the `Config` and try to figure out what renderers to use. |
dc9dc135 | 351 | fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> { |
416331ca | 352 | let mut renderers = Vec::new(); |
2c00a5a8 | 353 | |
dc9dc135 | 354 | if let Some(output_table) = config.get("output").and_then(Value::as_table) { |
416331ca | 355 | renderers.extend(output_table.iter().map(|(key, table)| { |
2c00a5a8 | 356 | if key == "html" { |
416331ca | 357 | Box::new(HtmlHandlebars::new()) as Box<dyn Renderer> |
e74abb32 XL |
358 | } else if key == "markdown" { |
359 | Box::new(MarkdownRenderer::new()) as Box<dyn Renderer> | |
2c00a5a8 | 360 | } else { |
416331ca | 361 | interpret_custom_renderer(key, table) |
2c00a5a8 | 362 | } |
416331ca | 363 | })); |
cc61c64b XL |
364 | } |
365 | ||
2c00a5a8 XL |
366 | // if we couldn't find anything, add the HTML renderer as a default |
367 | if renderers.is_empty() { | |
368 | renderers.push(Box::new(HtmlHandlebars::new())); | |
369 | } | |
cc61c64b | 370 | |
2c00a5a8 XL |
371 | renderers |
372 | } | |
cc61c64b | 373 | |
dc9dc135 | 374 | fn default_preprocessors() -> Vec<Box<dyn Preprocessor>> { |
9fa01778 XL |
375 | vec![ |
376 | Box::new(LinkPreprocessor::new()), | |
377 | Box::new(IndexPreprocessor::new()), | |
378 | ] | |
379 | } | |
380 | ||
dc9dc135 | 381 | fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool { |
9fa01778 XL |
382 | let name = pre.name(); |
383 | name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME | |
2c00a5a8 | 384 | } |
cc61c64b | 385 | |
2c00a5a8 | 386 | /// Look at the `MDBook` and try to figure out what preprocessors to run. |
dc9dc135 | 387 | fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> { |
9fa01778 XL |
388 | let mut preprocessors = Vec::new(); |
389 | ||
390 | if config.build.use_default_preprocessors { | |
391 | preprocessors.extend(default_preprocessors()); | |
392 | } | |
393 | ||
dc9dc135 | 394 | if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) { |
9fa01778 XL |
395 | for key in preprocessor_table.keys() { |
396 | match key.as_ref() { | |
397 | "links" => preprocessors.push(Box::new(LinkPreprocessor::new())), | |
398 | "index" => preprocessors.push(Box::new(IndexPreprocessor::new())), | |
399 | name => preprocessors.push(interpret_custom_preprocessor( | |
400 | name, | |
401 | &preprocessor_table[name], | |
402 | )), | |
403 | } | |
ea8adc8c | 404 | } |
cc61c64b XL |
405 | } |
406 | ||
2c00a5a8 XL |
407 | Ok(preprocessors) |
408 | } | |
cc61c64b | 409 | |
9fa01778 XL |
410 | fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocessor> { |
411 | let command = table | |
412 | .get("command") | |
dc9dc135 XL |
413 | .and_then(Value::as_str) |
414 | .map(ToString::to_string) | |
9fa01778 XL |
415 | .unwrap_or_else(|| format!("mdbook-{}", key)); |
416 | ||
f035d41b | 417 | Box::new(CmdPreprocessor::new(key.to_string(), command)) |
9fa01778 XL |
418 | } |
419 | ||
420 | fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> { | |
2c00a5a8 XL |
421 | // look for the `command` field, falling back to using the key |
422 | // prepended by "mdbook-" | |
423 | let table_dot_command = table | |
424 | .get("command") | |
dc9dc135 XL |
425 | .and_then(Value::as_str) |
426 | .map(ToString::to_string); | |
cc61c64b | 427 | |
2c00a5a8 | 428 | let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key)); |
cc61c64b | 429 | |
f035d41b | 430 | Box::new(CmdRenderer::new(key.to_string(), command)) |
2c00a5a8 | 431 | } |
ea8adc8c | 432 | |
9fa01778 XL |
433 | /// Check whether we should run a particular `Preprocessor` in combination |
434 | /// with the renderer, falling back to `Preprocessor::supports_renderer()` | |
435 | /// method if the user doesn't say anything. | |
436 | /// | |
437 | /// The `build.use-default-preprocessors` config option can be used to ensure | |
438 | /// default preprocessors always run if they support the renderer. | |
dc9dc135 XL |
439 | fn preprocessor_should_run( |
440 | preprocessor: &dyn Preprocessor, | |
441 | renderer: &dyn Renderer, | |
442 | cfg: &Config, | |
443 | ) -> bool { | |
9fa01778 XL |
444 | // default preprocessors should be run by default (if supported) |
445 | if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) { | |
446 | return preprocessor.supports_renderer(renderer.name()); | |
447 | } | |
448 | ||
449 | let key = format!("preprocessor.{}.renderers", preprocessor.name()); | |
450 | let renderer_name = renderer.name(); | |
451 | ||
452 | if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) { | |
453 | return explicit_renderers | |
454 | .iter() | |
dc9dc135 | 455 | .filter_map(Value::as_str) |
9fa01778 XL |
456 | .any(|name| name == renderer_name); |
457 | } | |
458 | ||
459 | preprocessor.supports_renderer(renderer_name) | |
460 | } | |
461 | ||
2c00a5a8 XL |
462 | #[cfg(test)] |
463 | mod tests { | |
464 | use super::*; | |
dc9dc135 | 465 | use std::str::FromStr; |
2c00a5a8 | 466 | use toml::value::{Table, Value}; |
cc61c64b | 467 | |
2c00a5a8 XL |
468 | #[test] |
469 | fn config_defaults_to_html_renderer_if_empty() { | |
470 | let cfg = Config::default(); | |
ea8adc8c | 471 | |
2c00a5a8 XL |
472 | // make sure we haven't got anything in the `output` table |
473 | assert!(cfg.get("output").is_none()); | |
cc61c64b | 474 | |
2c00a5a8 | 475 | let got = determine_renderers(&cfg); |
cc61c64b | 476 | |
2c00a5a8 XL |
477 | assert_eq!(got.len(), 1); |
478 | assert_eq!(got[0].name(), "html"); | |
cc61c64b XL |
479 | } |
480 | ||
2c00a5a8 XL |
481 | #[test] |
482 | fn add_a_random_renderer_to_the_config() { | |
483 | let mut cfg = Config::default(); | |
484 | cfg.set("output.random", Table::new()).unwrap(); | |
cc61c64b | 485 | |
2c00a5a8 | 486 | let got = determine_renderers(&cfg); |
cc61c64b | 487 | |
2c00a5a8 XL |
488 | assert_eq!(got.len(), 1); |
489 | assert_eq!(got[0].name(), "random"); | |
cc61c64b XL |
490 | } |
491 | ||
2c00a5a8 XL |
492 | #[test] |
493 | fn add_a_random_renderer_with_custom_command_to_the_config() { | |
494 | let mut cfg = Config::default(); | |
cc61c64b | 495 | |
2c00a5a8 XL |
496 | let mut table = Table::new(); |
497 | table.insert("command".to_string(), Value::String("false".to_string())); | |
498 | cfg.set("output.random", table).unwrap(); | |
cc61c64b | 499 | |
2c00a5a8 | 500 | let got = determine_renderers(&cfg); |
cc61c64b | 501 | |
2c00a5a8 XL |
502 | assert_eq!(got.len(), 1); |
503 | assert_eq!(got[0].name(), "random"); | |
cc61c64b XL |
504 | } |
505 | ||
2c00a5a8 | 506 | #[test] |
9fa01778 | 507 | fn config_defaults_to_link_and_index_preprocessor_if_not_set() { |
2c00a5a8 | 508 | let cfg = Config::default(); |
cc61c64b | 509 | |
9fa01778 XL |
510 | // make sure we haven't got anything in the `preprocessor` table |
511 | assert!(cfg.get("preprocessor").is_none()); | |
cc61c64b | 512 | |
2c00a5a8 | 513 | let got = determine_preprocessors(&cfg); |
cc61c64b | 514 | |
2c00a5a8 | 515 | assert!(got.is_ok()); |
9fa01778 | 516 | assert_eq!(got.as_ref().unwrap().len(), 2); |
2c00a5a8 | 517 | assert_eq!(got.as_ref().unwrap()[0].name(), "links"); |
9fa01778 XL |
518 | assert_eq!(got.as_ref().unwrap()[1].name(), "index"); |
519 | } | |
520 | ||
521 | #[test] | |
522 | fn use_default_preprocessors_works() { | |
523 | let mut cfg = Config::default(); | |
524 | cfg.build.use_default_preprocessors = false; | |
525 | ||
526 | let got = determine_preprocessors(&cfg).unwrap(); | |
527 | ||
528 | assert_eq!(got.len(), 0); | |
cc61c64b XL |
529 | } |
530 | ||
2c00a5a8 | 531 | #[test] |
9fa01778 | 532 | fn can_determine_third_party_preprocessors() { |
dc9dc135 | 533 | let cfg_str = r#" |
2c00a5a8 XL |
534 | [book] |
535 | title = "Some Book" | |
cc61c64b | 536 | |
9fa01778 XL |
537 | [preprocessor.random] |
538 | ||
2c00a5a8 XL |
539 | [build] |
540 | build-dir = "outputs" | |
541 | create-missing = false | |
2c00a5a8 | 542 | "#; |
ea8adc8c | 543 | |
2c00a5a8 | 544 | let cfg = Config::from_str(cfg_str).unwrap(); |
ea8adc8c | 545 | |
9fa01778 XL |
546 | // make sure the `preprocessor.random` table exists |
547 | assert!(cfg.get_preprocessor("random").is_some()); | |
ea8adc8c | 548 | |
9fa01778 | 549 | let got = determine_preprocessors(&cfg).unwrap(); |
ea8adc8c | 550 | |
9fa01778 | 551 | assert!(got.into_iter().any(|p| p.name() == "random")); |
ea8adc8c XL |
552 | } |
553 | ||
2c00a5a8 | 554 | #[test] |
9fa01778 XL |
555 | fn preprocessors_can_provide_their_own_commands() { |
556 | let cfg_str = r#" | |
557 | [preprocessor.random] | |
558 | command = "python random.py" | |
559 | "#; | |
ea8adc8c | 560 | |
9fa01778 XL |
561 | let cfg = Config::from_str(cfg_str).unwrap(); |
562 | ||
563 | // make sure the `preprocessor.random` table exists | |
564 | let random = cfg.get_preprocessor("random").unwrap(); | |
565 | let random = interpret_custom_preprocessor("random", &Value::Table(random.clone())); | |
566 | ||
567 | assert_eq!(random.cmd(), "python random.py"); | |
568 | } | |
569 | ||
570 | #[test] | |
571 | fn config_respects_preprocessor_selection() { | |
dc9dc135 | 572 | let cfg_str = r#" |
9fa01778 XL |
573 | [preprocessor.links] |
574 | renderers = ["html"] | |
2c00a5a8 | 575 | "#; |
ea8adc8c | 576 | |
2c00a5a8 | 577 | let cfg = Config::from_str(cfg_str).unwrap(); |
ea8adc8c | 578 | |
9fa01778 XL |
579 | // double-check that we can access preprocessor.links.renderers[0] |
580 | let html = cfg | |
581 | .get_preprocessor("links") | |
582 | .and_then(|links| links.get("renderers")) | |
dc9dc135 | 583 | .and_then(Value::as_array) |
9fa01778 | 584 | .and_then(|renderers| renderers.get(0)) |
dc9dc135 | 585 | .and_then(Value::as_str) |
9fa01778 XL |
586 | .unwrap(); |
587 | assert_eq!(html, "html"); | |
588 | let html_renderer = HtmlHandlebars::default(); | |
589 | let pre = LinkPreprocessor::new(); | |
590 | ||
591 | let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg); | |
592 | assert!(should_run); | |
593 | } | |
ea8adc8c | 594 | |
9fa01778 XL |
595 | struct BoolPreprocessor(bool); |
596 | impl Preprocessor for BoolPreprocessor { | |
597 | fn name(&self) -> &str { | |
598 | "bool-preprocessor" | |
599 | } | |
600 | ||
601 | fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> { | |
602 | unimplemented!() | |
603 | } | |
604 | ||
605 | fn supports_renderer(&self, _renderer: &str) -> bool { | |
606 | self.0 | |
607 | } | |
608 | } | |
609 | ||
610 | #[test] | |
611 | fn preprocessor_should_run_falls_back_to_supports_renderer_method() { | |
612 | let cfg = Config::default(); | |
613 | let html = HtmlHandlebars::new(); | |
614 | ||
615 | let should_be = true; | |
616 | let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg); | |
617 | assert_eq!(got, should_be); | |
cc61c64b | 618 | |
9fa01778 XL |
619 | let should_be = false; |
620 | let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg); | |
621 | assert_eq!(got, should_be); | |
cc61c64b XL |
622 | } |
623 | } |