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 
8 #[allow(clippy::module_inception)]
9 mod book;
10 mod init;
11 mod summary;
12 
13 pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
14 pub use self::init::BookBuilder;
15 pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
16 
17 use std::io::Write;
18 use std::path::PathBuf;
19 use std::process::Command;
20 use std::string::ToString;
21 use tempfile::Builder as TempFileBuilder;
22 use toml::Value;
23 use topological_sort::TopologicalSort;
24 
25 use crate::errors::*;
26 use crate::preprocess::{
27     CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
28 };
29 use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
30 use crate::utils;
31 
32 use crate::config::{Config, RustEdition};
33 
34 /// The object used to manage and build a book.
35 pub struct MDBook {
36     /// The book's root directory.
37     pub root: PathBuf,
38     /// The configuration used to tweak now a book is built.
39     pub config: Config,
40     /// A representation of the book's contents in memory.
41     pub book: Book,
42     renderers: Vec<Box<dyn Renderer>>,
43 
44     /// List of pre-processors to be run on the book.
45     preprocessors: Vec<Box<dyn Preprocessor>>,
46 }
47 
48 impl MDBook {
49     /// Load a book from its root directory on disk.
load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook>50     pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
51         let book_root = book_root.into();
52         let config_location = book_root.join("book.toml");
53 
54         // the book.json file is no longer used, so we should emit a warning to
55         // let people know to migrate to book.toml
56         if book_root.join("book.json").exists() {
57             warn!("It appears you are still using book.json for configuration.");
58             warn!("This format is no longer used, so you should migrate to the");
59             warn!("book.toml format.");
60             warn!("Check the user guide for migration information:");
61             warn!("\thttps://rust-lang.github.io/mdBook/format/config.html");
62         }
63 
64         let mut config = if config_location.exists() {
65             debug!("Loading config from {}", config_location.display());
66             Config::from_disk(&config_location)?
67         } else {
68             Config::default()
69         };
70 
71         config.update_from_env();
72 
73         if log_enabled!(log::Level::Trace) {
74             for line in format!("Config: {:#?}", config).lines() {
75                 trace!("{}", line);
76             }
77         }
78 
79         MDBook::load_with_config(book_root, config)
80     }
81 
82     /// Load a book from its root directory using a custom `Config`.
load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook>83     pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
84         let root = book_root.into();
85 
86         let src_dir = root.join(&config.book.src);
87         let book = book::load_book(&src_dir, &config.build)?;
88 
89         let renderers = determine_renderers(&config);
90         let preprocessors = determine_preprocessors(&config)?;
91 
92         Ok(MDBook {
93             root,
94             config,
95             book,
96             renderers,
97             preprocessors,
98         })
99     }
100 
101     /// Load a book from its root directory using a custom `Config` and a custom summary.
load_with_config_and_summary<P: Into<PathBuf>>( book_root: P, config: Config, summary: Summary, ) -> Result<MDBook>102     pub fn load_with_config_and_summary<P: Into<PathBuf>>(
103         book_root: P,
104         config: Config,
105         summary: Summary,
106     ) -> Result<MDBook> {
107         let root = book_root.into();
108 
109         let src_dir = root.join(&config.book.src);
110         let book = book::load_book_from_disk(&summary, &src_dir)?;
111 
112         let renderers = determine_renderers(&config);
113         let preprocessors = determine_preprocessors(&config)?;
114 
115         Ok(MDBook {
116             root,
117             config,
118             book,
119             renderers,
120             preprocessors,
121         })
122     }
123 
124     /// Returns a flat depth-first iterator over the elements of the book,
125     /// it returns a [`BookItem`] enum:
126     /// `(section: String, bookitem: &BookItem)`
127     ///
128     /// ```no_run
129     /// # use mdbook::MDBook;
130     /// # use mdbook::book::BookItem;
131     /// # let book = MDBook::load("mybook").unwrap();
132     /// for item in book.iter() {
133     ///     match *item {
134     ///         BookItem::Chapter(ref chapter) => {},
135     ///         BookItem::Separator => {},
136     ///         BookItem::PartTitle(ref title) => {}
137     ///     }
138     /// }
139     ///
140     /// // would print something like this:
141     /// // 1. Chapter 1
142     /// // 1.1 Sub Chapter
143     /// // 1.2 Sub Chapter
144     /// // 2. Chapter 2
145     /// //
146     /// // etc.
147     /// ```
iter(&self) -> BookItems<'_>148     pub fn iter(&self) -> BookItems<'_> {
149         self.book.iter()
150     }
151 
152     /// `init()` gives you a `BookBuilder` which you can use to setup a new book
153     /// and its accompanying directory structure.
154     ///
155     /// The `BookBuilder` creates some boilerplate files and directories to get
156     /// you started with your book.
157     ///
158     /// ```text
159     /// book-test/
160     /// ├── book
161     /// └── src
162     ///     ├── chapter_1.md
163     ///     └── SUMMARY.md
164     /// ```
165     ///
166     /// It uses the path provided as the root directory for your book, then adds
167     /// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
168     /// to get you started.
init<P: Into<PathBuf>>(book_root: P) -> BookBuilder169     pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
170         BookBuilder::new(book_root)
171     }
172 
173     /// Tells the renderer to build our book and put it in the build directory.
build(&self) -> Result<()>174     pub fn build(&self) -> Result<()> {
175         info!("Book building has started");
176 
177         for renderer in &self.renderers {
178             self.execute_build_process(&**renderer)?;
179         }
180 
181         Ok(())
182     }
183 
184     /// Run the entire build process for a particular [`Renderer`].
execute_build_process(&self, renderer: &dyn Renderer) -> Result<()>185     pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
186         let mut preprocessed_book = self.book.clone();
187         let preprocess_ctx = PreprocessorContext::new(
188             self.root.clone(),
189             self.config.clone(),
190             renderer.name().to_string(),
191         );
192 
193         for preprocessor in &self.preprocessors {
194             if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
195                 debug!("Running the {} preprocessor.", preprocessor.name());
196                 preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
197             }
198         }
199 
200         let name = renderer.name();
201         let build_dir = self.build_dir_for(name);
202 
203         let mut render_context = RenderContext::new(
204             self.root.clone(),
205             preprocessed_book,
206             self.config.clone(),
207             build_dir,
208         );
209         render_context
210             .chapter_titles
211             .extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
212 
213         info!("Running the {} backend", renderer.name());
214         renderer
215             .render(&render_context)
216             .with_context(|| "Rendering failed")
217     }
218 
219     /// You can change the default renderer to another one by using this method.
220     /// The only requirement is that your renderer implement the [`Renderer`]
221     /// trait.
with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self222     pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
223         self.renderers.push(Box::new(renderer));
224         self
225     }
226 
227     /// Register a [`Preprocessor`] to be used when rendering the book.
with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self228     pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
229         self.preprocessors.push(Box::new(preprocessor));
230         self
231     }
232 
233     /// Run `rustdoc` tests on the book, linking against the provided libraries.
test(&mut self, library_paths: Vec<&str>) -> Result<()>234     pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
235         let library_args: Vec<&str> = (0..library_paths.len())
236             .map(|_| "-L")
237             .zip(library_paths.into_iter())
238             .flat_map(|x| vec![x.0, x.1])
239             .collect();
240 
241         let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
242 
243         // FIXME: Is "test" the proper renderer name to use here?
244         let preprocess_context =
245             PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
246 
247         let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
248         // Index Preprocessor is disabled so that chapter paths continue to point to the
249         // actual markdown files.
250 
251         let mut failed = false;
252         for item in book.iter() {
253             if let BookItem::Chapter(ref ch) = *item {
254                 let chapter_path = match ch.path {
255                     Some(ref path) if !path.as_os_str().is_empty() => path,
256                     _ => continue,
257                 };
258 
259                 let path = self.source_dir().join(&chapter_path);
260                 info!("Testing file: {:?}", path);
261 
262                 // write preprocessed file to tempdir
263                 let path = temp_dir.path().join(&chapter_path);
264                 let mut tmpf = utils::fs::create_file(&path)?;
265                 tmpf.write_all(ch.content.as_bytes())?;
266 
267                 let mut cmd = Command::new("rustdoc");
268                 cmd.arg(&path).arg("--test").args(&library_args);
269 
270                 if let Some(edition) = self.config.rust.edition {
271                     match edition {
272                         RustEdition::E2015 => {
273                             cmd.args(&["--edition", "2015"]);
274                         }
275                         RustEdition::E2018 => {
276                             cmd.args(&["--edition", "2018"]);
277                         }
278                         RustEdition::E2021 => {
279                             cmd.args(&["--edition", "2021"])
280                                 .args(&["-Z", "unstable-options"]);
281                         }
282                     }
283                 }
284 
285                 let output = cmd.output()?;
286 
287                 if !output.status.success() {
288                     failed = true;
289                     error!(
290                         "rustdoc returned an error:\n\
291                         \n--- stdout\n{}\n--- stderr\n{}",
292                         String::from_utf8_lossy(&output.stdout),
293                         String::from_utf8_lossy(&output.stderr)
294                     );
295                 }
296             }
297         }
298         if failed {
299             bail!("One or more tests failed");
300         }
301         Ok(())
302     }
303 
304     /// The logic for determining where a backend should put its build
305     /// artefacts.
306     ///
307     /// If there is only 1 renderer, put it in the directory pointed to by the
308     /// `build.build_dir` key in [`Config`]. If there is more than one then the
309     /// renderer gets its own directory within the main build dir.
310     ///
311     /// i.e. If there were only one renderer (in this case, the HTML renderer):
312     ///
313     /// - build/
314     ///   - index.html
315     ///   - ...
316     ///
317     /// Otherwise if there are multiple:
318     ///
319     /// - build/
320     ///   - epub/
321     ///     - my_awesome_book.epub
322     ///   - html/
323     ///     - index.html
324     ///     - ...
325     ///   - latex/
326     ///     - my_awesome_book.tex
327     ///
build_dir_for(&self, backend_name: &str) -> PathBuf328     pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
329         let build_dir = self.root.join(&self.config.build.build_dir);
330 
331         if self.renderers.len() <= 1 {
332             build_dir
333         } else {
334             build_dir.join(backend_name)
335         }
336     }
337 
338     /// Get the directory containing this book's source files.
source_dir(&self) -> PathBuf339     pub fn source_dir(&self) -> PathBuf {
340         self.root.join(&self.config.book.src)
341     }
342 
343     /// Get the directory containing the theme resources for the book.
theme_dir(&self) -> PathBuf344     pub fn theme_dir(&self) -> PathBuf {
345         self.config
346             .html_config()
347             .unwrap_or_default()
348             .theme_dir(&self.root)
349     }
350 }
351 
352 /// Look at the `Config` and try to figure out what renderers to use.
determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>>353 fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
354     let mut renderers = Vec::new();
355 
356     if let Some(output_table) = config.get("output").and_then(Value::as_table) {
357         renderers.extend(output_table.iter().map(|(key, table)| {
358             if key == "html" {
359                 Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
360             } else if key == "markdown" {
361                 Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
362             } else {
363                 interpret_custom_renderer(key, table)
364             }
365         }));
366     }
367 
368     // if we couldn't find anything, add the HTML renderer as a default
369     if renderers.is_empty() {
370         renderers.push(Box::new(HtmlHandlebars::new()));
371     }
372 
373     renderers
374 }
375 
376 const DEFAULT_PREPROCESSORS: &[&'static str] = &["links", "index"];
377 
is_default_preprocessor(pre: &dyn Preprocessor) -> bool378 fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
379     let name = pre.name();
380     name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
381 }
382 
383 /// Look at the `MDBook` and try to figure out what preprocessors to run.
determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>>384 fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
385     // Collect the names of all preprocessors intended to be run, and the order
386     // in which they should be run.
387     let mut preprocessor_names = TopologicalSort::<String>::new();
388 
389     if config.build.use_default_preprocessors {
390         for name in DEFAULT_PREPROCESSORS {
391             preprocessor_names.insert(name.to_string());
392         }
393     }
394 
395     if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
396         for (name, table) in preprocessor_table.iter() {
397             preprocessor_names.insert(name.to_string());
398 
399             let exists = |name| {
400                 (config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
401                     || preprocessor_table.contains_key(name)
402             };
403 
404             if let Some(before) = table.get("before") {
405                 let before = before.as_array().ok_or_else(|| {
406                     Error::msg(format!(
407                         "Expected preprocessor.{}.before to be an array",
408                         name
409                     ))
410                 })?;
411                 for after in before {
412                     let after = after.as_str().ok_or_else(|| {
413                         Error::msg(format!(
414                             "Expected preprocessor.{}.before to contain strings",
415                             name
416                         ))
417                     })?;
418 
419                     if !exists(after) {
420                         // Only warn so that preprocessors can be toggled on and off (e.g. for
421                         // troubleshooting) without having to worry about order too much.
422                         warn!(
423                             "preprocessor.{}.after contains \"{}\", which was not found",
424                             name, after
425                         );
426                     } else {
427                         preprocessor_names.add_dependency(name, after);
428                     }
429                 }
430             }
431 
432             if let Some(after) = table.get("after") {
433                 let after = after.as_array().ok_or_else(|| {
434                     Error::msg(format!(
435                         "Expected preprocessor.{}.after to be an array",
436                         name
437                     ))
438                 })?;
439                 for before in after {
440                     let before = before.as_str().ok_or_else(|| {
441                         Error::msg(format!(
442                             "Expected preprocessor.{}.after to contain strings",
443                             name
444                         ))
445                     })?;
446 
447                     if !exists(before) {
448                         // See equivalent warning above for rationale
449                         warn!(
450                             "preprocessor.{}.before contains \"{}\", which was not found",
451                             name, before
452                         );
453                     } else {
454                         preprocessor_names.add_dependency(before, name);
455                     }
456                 }
457             }
458         }
459     }
460 
461     // Now that all links have been established, queue preprocessors in a suitable order
462     let mut preprocessors = Vec::with_capacity(preprocessor_names.len());
463     // `pop_all()` returns an empty vector when no more items are not being depended upon
464     for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
465         .take_while(|names| !names.is_empty())
466     {
467         // The `topological_sort` crate does not guarantee a stable order for ties, even across
468         // runs of the same program. Thus, we break ties manually by sorting.
469         // Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
470         // values ([1]), which may not be an alphabetical sort.
471         // As mentioned in [1], doing so depends on locale, which is not desirable for deciding
472         // preprocessor execution order.
473         // [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
474         names.sort();
475         for name in names {
476             let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
477                 "links" => Box::new(LinkPreprocessor::new()),
478                 "index" => Box::new(IndexPreprocessor::new()),
479                 _ => {
480                     // The only way to request a custom preprocessor is through the `preprocessor`
481                     // table, so it must exist, be a table, and contain the key.
482                     let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name];
483                     let command = get_custom_preprocessor_cmd(&name, table);
484                     Box::new(CmdPreprocessor::new(name, command))
485                 }
486             };
487             preprocessors.push(preprocessor);
488         }
489     }
490 
491     // "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
492     // Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
493     if preprocessor_names.is_empty() {
494         Ok(preprocessors)
495     } else {
496         Err(Error::msg("Cyclic dependency detected in preprocessors"))
497     }
498 }
499 
get_custom_preprocessor_cmd(key: &str, table: &Value) -> String500 fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
501     table
502         .get("command")
503         .and_then(Value::as_str)
504         .map(ToString::to_string)
505         .unwrap_or_else(|| format!("mdbook-{}", key))
506 }
507 
interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer>508 fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
509     // look for the `command` field, falling back to using the key
510     // prepended by "mdbook-"
511     let table_dot_command = table
512         .get("command")
513         .and_then(Value::as_str)
514         .map(ToString::to_string);
515 
516     let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
517 
518     Box::new(CmdRenderer::new(key.to_string(), command))
519 }
520 
521 /// Check whether we should run a particular `Preprocessor` in combination
522 /// with the renderer, falling back to `Preprocessor::supports_renderer()`
523 /// method if the user doesn't say anything.
524 ///
525 /// The `build.use-default-preprocessors` config option can be used to ensure
526 /// default preprocessors always run if they support the renderer.
preprocessor_should_run( preprocessor: &dyn Preprocessor, renderer: &dyn Renderer, cfg: &Config, ) -> bool527 fn preprocessor_should_run(
528     preprocessor: &dyn Preprocessor,
529     renderer: &dyn Renderer,
530     cfg: &Config,
531 ) -> bool {
532     // default preprocessors should be run by default (if supported)
533     if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
534         return preprocessor.supports_renderer(renderer.name());
535     }
536 
537     let key = format!("preprocessor.{}.renderers", preprocessor.name());
538     let renderer_name = renderer.name();
539 
540     if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
541         return explicit_renderers
542             .iter()
543             .filter_map(Value::as_str)
544             .any(|name| name == renderer_name);
545     }
546 
547     preprocessor.supports_renderer(renderer_name)
548 }
549 
550 #[cfg(test)]
551 mod tests {
552     use super::*;
553     use std::str::FromStr;
554     use toml::value::{Table, Value};
555 
556     #[test]
config_defaults_to_html_renderer_if_empty()557     fn config_defaults_to_html_renderer_if_empty() {
558         let cfg = Config::default();
559 
560         // make sure we haven't got anything in the `output` table
561         assert!(cfg.get("output").is_none());
562 
563         let got = determine_renderers(&cfg);
564 
565         assert_eq!(got.len(), 1);
566         assert_eq!(got[0].name(), "html");
567     }
568 
569     #[test]
add_a_random_renderer_to_the_config()570     fn add_a_random_renderer_to_the_config() {
571         let mut cfg = Config::default();
572         cfg.set("output.random", Table::new()).unwrap();
573 
574         let got = determine_renderers(&cfg);
575 
576         assert_eq!(got.len(), 1);
577         assert_eq!(got[0].name(), "random");
578     }
579 
580     #[test]
add_a_random_renderer_with_custom_command_to_the_config()581     fn add_a_random_renderer_with_custom_command_to_the_config() {
582         let mut cfg = Config::default();
583 
584         let mut table = Table::new();
585         table.insert("command".to_string(), Value::String("false".to_string()));
586         cfg.set("output.random", table).unwrap();
587 
588         let got = determine_renderers(&cfg);
589 
590         assert_eq!(got.len(), 1);
591         assert_eq!(got[0].name(), "random");
592     }
593 
594     #[test]
config_defaults_to_link_and_index_preprocessor_if_not_set()595     fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
596         let cfg = Config::default();
597 
598         // make sure we haven't got anything in the `preprocessor` table
599         assert!(cfg.get("preprocessor").is_none());
600 
601         let got = determine_preprocessors(&cfg);
602 
603         assert!(got.is_ok());
604         assert_eq!(got.as_ref().unwrap().len(), 2);
605         assert_eq!(got.as_ref().unwrap()[0].name(), "index");
606         assert_eq!(got.as_ref().unwrap()[1].name(), "links");
607     }
608 
609     #[test]
use_default_preprocessors_works()610     fn use_default_preprocessors_works() {
611         let mut cfg = Config::default();
612         cfg.build.use_default_preprocessors = false;
613 
614         let got = determine_preprocessors(&cfg).unwrap();
615 
616         assert_eq!(got.len(), 0);
617     }
618 
619     #[test]
can_determine_third_party_preprocessors()620     fn can_determine_third_party_preprocessors() {
621         let cfg_str = r#"
622         [book]
623         title = "Some Book"
624 
625         [preprocessor.random]
626 
627         [build]
628         build-dir = "outputs"
629         create-missing = false
630         "#;
631 
632         let cfg = Config::from_str(cfg_str).unwrap();
633 
634         // make sure the `preprocessor.random` table exists
635         assert!(cfg.get_preprocessor("random").is_some());
636 
637         let got = determine_preprocessors(&cfg).unwrap();
638 
639         assert!(got.into_iter().any(|p| p.name() == "random"));
640     }
641 
642     #[test]
preprocessors_can_provide_their_own_commands()643     fn preprocessors_can_provide_their_own_commands() {
644         let cfg_str = r#"
645         [preprocessor.random]
646         command = "python random.py"
647         "#;
648 
649         let cfg = Config::from_str(cfg_str).unwrap();
650 
651         // make sure the `preprocessor.random` table exists
652         let random = cfg.get_preprocessor("random").unwrap();
653         let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
654 
655         assert_eq!(random, "python random.py");
656     }
657 
658     #[test]
preprocessor_before_must_be_array()659     fn preprocessor_before_must_be_array() {
660         let cfg_str = r#"
661         [preprocessor.random]
662         before = 0
663         "#;
664 
665         let cfg = Config::from_str(cfg_str).unwrap();
666 
667         assert!(determine_preprocessors(&cfg).is_err());
668     }
669 
670     #[test]
preprocessor_after_must_be_array()671     fn preprocessor_after_must_be_array() {
672         let cfg_str = r#"
673         [preprocessor.random]
674         after = 0
675         "#;
676 
677         let cfg = Config::from_str(cfg_str).unwrap();
678 
679         assert!(determine_preprocessors(&cfg).is_err());
680     }
681 
682     #[test]
preprocessor_order_is_honored()683     fn preprocessor_order_is_honored() {
684         let cfg_str = r#"
685         [preprocessor.random]
686         before = [ "last" ]
687         after = [ "index" ]
688 
689         [preprocessor.last]
690         after = [ "links", "index" ]
691         "#;
692 
693         let cfg = Config::from_str(cfg_str).unwrap();
694 
695         let preprocessors = determine_preprocessors(&cfg).unwrap();
696         let index = |name| {
697             preprocessors
698                 .iter()
699                 .enumerate()
700                 .find(|(_, preprocessor)| preprocessor.name() == name)
701                 .unwrap()
702                 .0
703         };
704         let assert_before = |before, after| {
705             if index(before) >= index(after) {
706                 eprintln!("Preprocessor order:");
707                 for preprocessor in &preprocessors {
708                     eprintln!("  {}", preprocessor.name());
709                 }
710                 panic!("{} should come before {}", before, after);
711             }
712         };
713 
714         assert_before("index", "random");
715         assert_before("index", "last");
716         assert_before("random", "last");
717         assert_before("links", "last");
718     }
719 
720     #[test]
cyclic_dependencies_are_detected()721     fn cyclic_dependencies_are_detected() {
722         let cfg_str = r#"
723         [preprocessor.links]
724         before = [ "index" ]
725 
726         [preprocessor.index]
727         before = [ "links" ]
728         "#;
729 
730         let cfg = Config::from_str(cfg_str).unwrap();
731 
732         assert!(determine_preprocessors(&cfg).is_err());
733     }
734 
735     #[test]
dependencies_dont_register_undefined_preprocessors()736     fn dependencies_dont_register_undefined_preprocessors() {
737         let cfg_str = r#"
738         [preprocessor.links]
739         before = [ "random" ]
740         "#;
741 
742         let cfg = Config::from_str(cfg_str).unwrap();
743 
744         let preprocessors = determine_preprocessors(&cfg).unwrap();
745 
746         assert!(preprocessors
747             .iter()
748             .find(|preprocessor| preprocessor.name() == "random")
749             .is_none());
750     }
751 
752     #[test]
dependencies_dont_register_builtin_preprocessors_if_disabled()753     fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
754         let cfg_str = r#"
755         [preprocessor.random]
756         before = [ "links" ]
757 
758         [build]
759         use-default-preprocessors = false
760         "#;
761 
762         let cfg = Config::from_str(cfg_str).unwrap();
763 
764         let preprocessors = determine_preprocessors(&cfg).unwrap();
765 
766         assert!(preprocessors
767             .iter()
768             .find(|preprocessor| preprocessor.name() == "links")
769             .is_none());
770     }
771 
772     #[test]
config_respects_preprocessor_selection()773     fn config_respects_preprocessor_selection() {
774         let cfg_str = r#"
775         [preprocessor.links]
776         renderers = ["html"]
777         "#;
778 
779         let cfg = Config::from_str(cfg_str).unwrap();
780 
781         // double-check that we can access preprocessor.links.renderers[0]
782         let html = cfg
783             .get_preprocessor("links")
784             .and_then(|links| links.get("renderers"))
785             .and_then(Value::as_array)
786             .and_then(|renderers| renderers.get(0))
787             .and_then(Value::as_str)
788             .unwrap();
789         assert_eq!(html, "html");
790         let html_renderer = HtmlHandlebars::default();
791         let pre = LinkPreprocessor::new();
792 
793         let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
794         assert!(should_run);
795     }
796 
797     struct BoolPreprocessor(bool);
798     impl Preprocessor for BoolPreprocessor {
name(&self) -> &str799         fn name(&self) -> &str {
800             "bool-preprocessor"
801         }
802 
run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book>803         fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
804             unimplemented!()
805         }
806 
supports_renderer(&self, _renderer: &str) -> bool807         fn supports_renderer(&self, _renderer: &str) -> bool {
808             self.0
809         }
810     }
811 
812     #[test]
preprocessor_should_run_falls_back_to_supports_renderer_method()813     fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
814         let cfg = Config::default();
815         let html = HtmlHandlebars::new();
816 
817         let should_be = true;
818         let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
819         assert_eq!(got, should_be);
820 
821         let should_be = false;
822         let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
823         assert_eq!(got, should_be);
824     }
825 }
826