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