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