]>
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 | |
9fa01778 | 253 | for item in book.iter() { |
2c00a5a8 | 254 | if let BookItem::Chapter(ref ch) = *item { |
f035d41b XL |
255 | let chapter_path = match ch.path { |
256 | Some(ref path) if !path.as_os_str().is_empty() => path, | |
257 | _ => continue, | |
258 | }; | |
259 | ||
260 | let path = self.source_dir().join(&chapter_path); | |
261 | info!("Testing file: {:?}", path); | |
262 | ||
263 | // write preprocessed file to tempdir | |
264 | let path = temp_dir.path().join(&chapter_path); | |
265 | let mut tmpf = utils::fs::create_file(&path)?; | |
266 | tmpf.write_all(ch.content.as_bytes())?; | |
267 | ||
268 | let mut cmd = Command::new("rustdoc"); | |
269 | cmd.arg(&path).arg("--test").args(&library_args); | |
270 | ||
271 | if let Some(edition) = self.config.rust.edition { | |
272 | match edition { | |
273 | RustEdition::E2015 => { | |
274 | cmd.args(&["--edition", "2015"]); | |
275 | } | |
276 | RustEdition::E2018 => { | |
277 | cmd.args(&["--edition", "2018"]); | |
278 | } | |
2c00a5a8 XL |
279 | } |
280 | } | |
f035d41b XL |
281 | |
282 | let output = cmd.output()?; | |
283 | ||
284 | if !output.status.success() { | |
285 | bail!( | |
286 | "rustdoc returned an error:\n\ | |
287 | \n--- stdout\n{}\n--- stderr\n{}", | |
288 | String::from_utf8_lossy(&output.stdout), | |
289 | String::from_utf8_lossy(&output.stderr) | |
290 | ); | |
291 | } | |
2c00a5a8 | 292 | } |
cc61c64b | 293 | } |
2c00a5a8 | 294 | Ok(()) |
cc61c64b XL |
295 | } |
296 | ||
2c00a5a8 XL |
297 | /// The logic for determining where a backend should put its build |
298 | /// artefacts. | |
cc61c64b | 299 | /// |
2c00a5a8 XL |
300 | /// If there is only 1 renderer, put it in the directory pointed to by the |
301 | /// `build.build_dir` key in `Config`. If there is more than one then the | |
302 | /// renderer gets its own directory within the main build dir. | |
303 | /// | |
304 | /// i.e. If there were only one renderer (in this case, the HTML renderer): | |
305 | /// | |
306 | /// - build/ | |
307 | /// - index.html | |
308 | /// - ... | |
309 | /// | |
310 | /// Otherwise if there are multiple: | |
311 | /// | |
312 | /// - build/ | |
313 | /// - epub/ | |
314 | /// - my_awesome_book.epub | |
315 | /// - html/ | |
316 | /// - index.html | |
317 | /// - ... | |
318 | /// - latex/ | |
319 | /// - my_awesome_book.tex | |
320 | /// | |
321 | pub fn build_dir_for(&self, backend_name: &str) -> PathBuf { | |
322 | let build_dir = self.root.join(&self.config.build.build_dir); | |
cc61c64b | 323 | |
2c00a5a8 XL |
324 | if self.renderers.len() <= 1 { |
325 | build_dir | |
326 | } else { | |
327 | build_dir.join(backend_name) | |
328 | } | |
cc61c64b XL |
329 | } |
330 | ||
2c00a5a8 XL |
331 | /// Get the directory containing this book's source files. |
332 | pub fn source_dir(&self) -> PathBuf { | |
333 | self.root.join(&self.config.book.src) | |
cc61c64b XL |
334 | } |
335 | ||
83c7162d | 336 | /// Get the directory containing the theme resources for the book. |
2c00a5a8 | 337 | pub fn theme_dir(&self) -> PathBuf { |
83c7162d XL |
338 | self.config |
339 | .html_config() | |
340 | .unwrap_or_default() | |
341 | .theme_dir(&self.root) | |
cc61c64b | 342 | } |
2c00a5a8 | 343 | } |
cc61c64b | 344 | |
2c00a5a8 | 345 | /// Look at the `Config` and try to figure out what renderers to use. |
dc9dc135 | 346 | fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> { |
416331ca | 347 | let mut renderers = Vec::new(); |
2c00a5a8 | 348 | |
dc9dc135 | 349 | if let Some(output_table) = config.get("output").and_then(Value::as_table) { |
416331ca | 350 | renderers.extend(output_table.iter().map(|(key, table)| { |
2c00a5a8 | 351 | if key == "html" { |
416331ca | 352 | Box::new(HtmlHandlebars::new()) as Box<dyn Renderer> |
e74abb32 XL |
353 | } else if key == "markdown" { |
354 | Box::new(MarkdownRenderer::new()) as Box<dyn Renderer> | |
2c00a5a8 | 355 | } else { |
416331ca | 356 | interpret_custom_renderer(key, table) |
2c00a5a8 | 357 | } |
416331ca | 358 | })); |
cc61c64b XL |
359 | } |
360 | ||
2c00a5a8 XL |
361 | // if we couldn't find anything, add the HTML renderer as a default |
362 | if renderers.is_empty() { | |
363 | renderers.push(Box::new(HtmlHandlebars::new())); | |
364 | } | |
cc61c64b | 365 | |
2c00a5a8 XL |
366 | renderers |
367 | } | |
cc61c64b | 368 | |
dc9dc135 | 369 | fn default_preprocessors() -> Vec<Box<dyn Preprocessor>> { |
9fa01778 XL |
370 | vec![ |
371 | Box::new(LinkPreprocessor::new()), | |
372 | Box::new(IndexPreprocessor::new()), | |
373 | ] | |
374 | } | |
375 | ||
dc9dc135 | 376 | fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool { |
9fa01778 XL |
377 | let name = pre.name(); |
378 | name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME | |
2c00a5a8 | 379 | } |
cc61c64b | 380 | |
2c00a5a8 | 381 | /// Look at the `MDBook` and try to figure out what preprocessors to run. |
dc9dc135 | 382 | fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> { |
9fa01778 XL |
383 | let mut preprocessors = Vec::new(); |
384 | ||
385 | if config.build.use_default_preprocessors { | |
386 | preprocessors.extend(default_preprocessors()); | |
387 | } | |
388 | ||
dc9dc135 | 389 | if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) { |
9fa01778 XL |
390 | for key in preprocessor_table.keys() { |
391 | match key.as_ref() { | |
392 | "links" => preprocessors.push(Box::new(LinkPreprocessor::new())), | |
393 | "index" => preprocessors.push(Box::new(IndexPreprocessor::new())), | |
394 | name => preprocessors.push(interpret_custom_preprocessor( | |
395 | name, | |
396 | &preprocessor_table[name], | |
397 | )), | |
398 | } | |
ea8adc8c | 399 | } |
cc61c64b XL |
400 | } |
401 | ||
2c00a5a8 XL |
402 | Ok(preprocessors) |
403 | } | |
cc61c64b | 404 | |
9fa01778 XL |
405 | fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocessor> { |
406 | let command = table | |
407 | .get("command") | |
dc9dc135 XL |
408 | .and_then(Value::as_str) |
409 | .map(ToString::to_string) | |
9fa01778 XL |
410 | .unwrap_or_else(|| format!("mdbook-{}", key)); |
411 | ||
f035d41b | 412 | Box::new(CmdPreprocessor::new(key.to_string(), command)) |
9fa01778 XL |
413 | } |
414 | ||
415 | fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> { | |
2c00a5a8 XL |
416 | // look for the `command` field, falling back to using the key |
417 | // prepended by "mdbook-" | |
418 | let table_dot_command = table | |
419 | .get("command") | |
dc9dc135 XL |
420 | .and_then(Value::as_str) |
421 | .map(ToString::to_string); | |
cc61c64b | 422 | |
2c00a5a8 | 423 | let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key)); |
cc61c64b | 424 | |
f035d41b | 425 | Box::new(CmdRenderer::new(key.to_string(), command)) |
2c00a5a8 | 426 | } |
ea8adc8c | 427 | |
9fa01778 XL |
428 | /// Check whether we should run a particular `Preprocessor` in combination |
429 | /// with the renderer, falling back to `Preprocessor::supports_renderer()` | |
430 | /// method if the user doesn't say anything. | |
431 | /// | |
432 | /// The `build.use-default-preprocessors` config option can be used to ensure | |
433 | /// default preprocessors always run if they support the renderer. | |
dc9dc135 XL |
434 | fn preprocessor_should_run( |
435 | preprocessor: &dyn Preprocessor, | |
436 | renderer: &dyn Renderer, | |
437 | cfg: &Config, | |
438 | ) -> bool { | |
9fa01778 XL |
439 | // default preprocessors should be run by default (if supported) |
440 | if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) { | |
441 | return preprocessor.supports_renderer(renderer.name()); | |
442 | } | |
443 | ||
444 | let key = format!("preprocessor.{}.renderers", preprocessor.name()); | |
445 | let renderer_name = renderer.name(); | |
446 | ||
447 | if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) { | |
448 | return explicit_renderers | |
449 | .iter() | |
dc9dc135 | 450 | .filter_map(Value::as_str) |
9fa01778 XL |
451 | .any(|name| name == renderer_name); |
452 | } | |
453 | ||
454 | preprocessor.supports_renderer(renderer_name) | |
455 | } | |
456 | ||
2c00a5a8 XL |
457 | #[cfg(test)] |
458 | mod tests { | |
459 | use super::*; | |
dc9dc135 | 460 | use std::str::FromStr; |
2c00a5a8 | 461 | use toml::value::{Table, Value}; |
cc61c64b | 462 | |
2c00a5a8 XL |
463 | #[test] |
464 | fn config_defaults_to_html_renderer_if_empty() { | |
465 | let cfg = Config::default(); | |
ea8adc8c | 466 | |
2c00a5a8 XL |
467 | // make sure we haven't got anything in the `output` table |
468 | assert!(cfg.get("output").is_none()); | |
cc61c64b | 469 | |
2c00a5a8 | 470 | let got = determine_renderers(&cfg); |
cc61c64b | 471 | |
2c00a5a8 XL |
472 | assert_eq!(got.len(), 1); |
473 | assert_eq!(got[0].name(), "html"); | |
cc61c64b XL |
474 | } |
475 | ||
2c00a5a8 XL |
476 | #[test] |
477 | fn add_a_random_renderer_to_the_config() { | |
478 | let mut cfg = Config::default(); | |
479 | cfg.set("output.random", Table::new()).unwrap(); | |
cc61c64b | 480 | |
2c00a5a8 | 481 | let got = determine_renderers(&cfg); |
cc61c64b | 482 | |
2c00a5a8 XL |
483 | assert_eq!(got.len(), 1); |
484 | assert_eq!(got[0].name(), "random"); | |
cc61c64b XL |
485 | } |
486 | ||
2c00a5a8 XL |
487 | #[test] |
488 | fn add_a_random_renderer_with_custom_command_to_the_config() { | |
489 | let mut cfg = Config::default(); | |
cc61c64b | 490 | |
2c00a5a8 XL |
491 | let mut table = Table::new(); |
492 | table.insert("command".to_string(), Value::String("false".to_string())); | |
493 | cfg.set("output.random", table).unwrap(); | |
cc61c64b | 494 | |
2c00a5a8 | 495 | let got = determine_renderers(&cfg); |
cc61c64b | 496 | |
2c00a5a8 XL |
497 | assert_eq!(got.len(), 1); |
498 | assert_eq!(got[0].name(), "random"); | |
cc61c64b XL |
499 | } |
500 | ||
2c00a5a8 | 501 | #[test] |
9fa01778 | 502 | fn config_defaults_to_link_and_index_preprocessor_if_not_set() { |
2c00a5a8 | 503 | let cfg = Config::default(); |
cc61c64b | 504 | |
9fa01778 XL |
505 | // make sure we haven't got anything in the `preprocessor` table |
506 | assert!(cfg.get("preprocessor").is_none()); | |
cc61c64b | 507 | |
2c00a5a8 | 508 | let got = determine_preprocessors(&cfg); |
cc61c64b | 509 | |
2c00a5a8 | 510 | assert!(got.is_ok()); |
9fa01778 | 511 | assert_eq!(got.as_ref().unwrap().len(), 2); |
2c00a5a8 | 512 | assert_eq!(got.as_ref().unwrap()[0].name(), "links"); |
9fa01778 XL |
513 | assert_eq!(got.as_ref().unwrap()[1].name(), "index"); |
514 | } | |
515 | ||
516 | #[test] | |
517 | fn use_default_preprocessors_works() { | |
518 | let mut cfg = Config::default(); | |
519 | cfg.build.use_default_preprocessors = false; | |
520 | ||
521 | let got = determine_preprocessors(&cfg).unwrap(); | |
522 | ||
523 | assert_eq!(got.len(), 0); | |
cc61c64b XL |
524 | } |
525 | ||
2c00a5a8 | 526 | #[test] |
9fa01778 | 527 | fn can_determine_third_party_preprocessors() { |
dc9dc135 | 528 | let cfg_str = r#" |
2c00a5a8 XL |
529 | [book] |
530 | title = "Some Book" | |
cc61c64b | 531 | |
9fa01778 XL |
532 | [preprocessor.random] |
533 | ||
2c00a5a8 XL |
534 | [build] |
535 | build-dir = "outputs" | |
536 | create-missing = false | |
2c00a5a8 | 537 | "#; |
ea8adc8c | 538 | |
2c00a5a8 | 539 | let cfg = Config::from_str(cfg_str).unwrap(); |
ea8adc8c | 540 | |
9fa01778 XL |
541 | // make sure the `preprocessor.random` table exists |
542 | assert!(cfg.get_preprocessor("random").is_some()); | |
ea8adc8c | 543 | |
9fa01778 | 544 | let got = determine_preprocessors(&cfg).unwrap(); |
ea8adc8c | 545 | |
9fa01778 | 546 | assert!(got.into_iter().any(|p| p.name() == "random")); |
ea8adc8c XL |
547 | } |
548 | ||
2c00a5a8 | 549 | #[test] |
9fa01778 XL |
550 | fn preprocessors_can_provide_their_own_commands() { |
551 | let cfg_str = r#" | |
552 | [preprocessor.random] | |
553 | command = "python random.py" | |
554 | "#; | |
ea8adc8c | 555 | |
9fa01778 XL |
556 | let cfg = Config::from_str(cfg_str).unwrap(); |
557 | ||
558 | // make sure the `preprocessor.random` table exists | |
559 | let random = cfg.get_preprocessor("random").unwrap(); | |
560 | let random = interpret_custom_preprocessor("random", &Value::Table(random.clone())); | |
561 | ||
562 | assert_eq!(random.cmd(), "python random.py"); | |
563 | } | |
564 | ||
565 | #[test] | |
566 | fn config_respects_preprocessor_selection() { | |
dc9dc135 | 567 | let cfg_str = r#" |
9fa01778 XL |
568 | [preprocessor.links] |
569 | renderers = ["html"] | |
2c00a5a8 | 570 | "#; |
ea8adc8c | 571 | |
2c00a5a8 | 572 | let cfg = Config::from_str(cfg_str).unwrap(); |
ea8adc8c | 573 | |
9fa01778 XL |
574 | // double-check that we can access preprocessor.links.renderers[0] |
575 | let html = cfg | |
576 | .get_preprocessor("links") | |
577 | .and_then(|links| links.get("renderers")) | |
dc9dc135 | 578 | .and_then(Value::as_array) |
9fa01778 | 579 | .and_then(|renderers| renderers.get(0)) |
dc9dc135 | 580 | .and_then(Value::as_str) |
9fa01778 XL |
581 | .unwrap(); |
582 | assert_eq!(html, "html"); | |
583 | let html_renderer = HtmlHandlebars::default(); | |
584 | let pre = LinkPreprocessor::new(); | |
585 | ||
586 | let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg); | |
587 | assert!(should_run); | |
588 | } | |
ea8adc8c | 589 | |
9fa01778 XL |
590 | struct BoolPreprocessor(bool); |
591 | impl Preprocessor for BoolPreprocessor { | |
592 | fn name(&self) -> &str { | |
593 | "bool-preprocessor" | |
594 | } | |
595 | ||
596 | fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> { | |
597 | unimplemented!() | |
598 | } | |
599 | ||
600 | fn supports_renderer(&self, _renderer: &str) -> bool { | |
601 | self.0 | |
602 | } | |
603 | } | |
604 | ||
605 | #[test] | |
606 | fn preprocessor_should_run_falls_back_to_supports_renderer_method() { | |
607 | let cfg = Config::default(); | |
608 | let html = HtmlHandlebars::new(); | |
609 | ||
610 | let should_be = true; | |
611 | let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg); | |
612 | assert_eq!(got, should_be); | |
cc61c64b | 613 | |
9fa01778 XL |
614 | let should_be = false; |
615 | let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg); | |
616 | assert_eq!(got, should_be); | |
cc61c64b XL |
617 | } |
618 | } |