mdbook/book/
mod.rs

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 the entire build process for a particular [`Renderer`].
200    pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
201        let mut preprocessed_book = self.book.clone();
202        let preprocess_ctx = PreprocessorContext::new(
203            self.root.clone(),
204            self.config.clone(),
205            renderer.name().to_string(),
206        );
207
208        for preprocessor in &self.preprocessors {
209            if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
210                debug!("Running the {} preprocessor.", preprocessor.name());
211                preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
212            }
213        }
214
215        let name = renderer.name();
216        let build_dir = self.build_dir_for(name);
217
218        let mut render_context = RenderContext::new(
219            self.root.clone(),
220            preprocessed_book,
221            self.config.clone(),
222            build_dir,
223        );
224        render_context
225            .chapter_titles
226            .extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
227
228        info!("Running the {} backend", renderer.name());
229        renderer
230            .render(&render_context)
231            .with_context(|| "Rendering failed")
232    }
233
234    /// You can change the default renderer to another one by using this method.
235    /// The only requirement is that your renderer implement the [`Renderer`]
236    /// trait.
237    pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
238        self.renderers.push(Box::new(renderer));
239        self
240    }
241
242    /// Register a [`Preprocessor`] to be used when rendering the book.
243    pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
244        self.preprocessors.push(Box::new(preprocessor));
245        self
246    }
247
248    /// Run `rustdoc` tests on the book, linking against the provided libraries.
249    pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
250        // test_chapter with chapter:None will run all tests.
251        self.test_chapter(library_paths, None)
252    }
253
254    /// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries.
255    /// If `chapter` is `None`, all tests will be run.
256    pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
257        let library_args: Vec<&str> = (0..library_paths.len())
258            .map(|_| "-L")
259            .zip(library_paths.into_iter())
260            .flat_map(|x| vec![x.0, x.1])
261            .collect();
262
263        let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
264
265        let mut chapter_found = false;
266
267        // FIXME: Is "test" the proper renderer name to use here?
268        let preprocess_context =
269            PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
270
271        let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
272        // Index Preprocessor is disabled so that chapter paths continue to point to the
273        // actual markdown files.
274
275        let mut failed = false;
276        for item in book.iter() {
277            if let BookItem::Chapter(ref ch) = *item {
278                let chapter_path = match ch.path {
279                    Some(ref path) if !path.as_os_str().is_empty() => path,
280                    _ => continue,
281                };
282
283                if let Some(chapter) = chapter {
284                    if ch.name != chapter && chapter_path.to_str() != Some(chapter) {
285                        if chapter == "?" {
286                            info!("Skipping chapter '{}'...", ch.name);
287                        }
288                        continue;
289                    }
290                }
291                chapter_found = true;
292                info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
293
294                // write preprocessed file to tempdir
295                let path = temp_dir.path().join(&chapter_path);
296                let mut tmpf = utils::fs::create_file(&path)?;
297                tmpf.write_all(ch.content.as_bytes())?;
298
299                let mut cmd = Command::new("rustdoc");
300                cmd.arg(&path).arg("--test").args(&library_args);
301
302                if let Some(edition) = self.config.rust.edition {
303                    match edition {
304                        RustEdition::E2015 => {
305                            cmd.args(&["--edition", "2015"]);
306                        }
307                        RustEdition::E2018 => {
308                            cmd.args(&["--edition", "2018"]);
309                        }
310                        RustEdition::E2021 => {
311                            cmd.args(&["--edition", "2021"]);
312                        }
313                    }
314                }
315
316                debug!("running {:?}", cmd);
317                let output = cmd.output()?;
318
319                if !output.status.success() {
320                    failed = true;
321                    error!(
322                        "rustdoc returned an error:\n\
323                        \n--- stdout\n{}\n--- stderr\n{}",
324                        String::from_utf8_lossy(&output.stdout),
325                        String::from_utf8_lossy(&output.stderr)
326                    );
327                }
328            }
329        }
330        if failed {
331            bail!("One or more tests failed");
332        }
333        if let Some(chapter) = chapter {
334            if !chapter_found {
335                bail!("Chapter not found: {}", chapter);
336            }
337        }
338        Ok(())
339    }
340
341    /// The logic for determining where a backend should put its build
342    /// artefacts.
343    ///
344    /// If there is only 1 renderer, put it in the directory pointed to by the
345    /// `build.build_dir` key in [`Config`]. If there is more than one then the
346    /// renderer gets its own directory within the main build dir.
347    ///
348    /// i.e. If there were only one renderer (in this case, the HTML renderer):
349    ///
350    /// - build/
351    ///   - index.html
352    ///   - ...
353    ///
354    /// Otherwise if there are multiple:
355    ///
356    /// - build/
357    ///   - epub/
358    ///     - my_awesome_book.epub
359    ///   - html/
360    ///     - index.html
361    ///     - ...
362    ///   - latex/
363    ///     - my_awesome_book.tex
364    ///
365    pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
366        let build_dir = self.root.join(&self.config.build.build_dir);
367
368        if self.renderers.len() <= 1 {
369            build_dir
370        } else {
371            build_dir.join(backend_name)
372        }
373    }
374
375    /// Get the directory containing this book's source files.
376    pub fn source_dir(&self) -> PathBuf {
377        self.root.join(&self.config.book.src)
378    }
379
380    /// Get the directory containing the theme resources for the book.
381    pub fn theme_dir(&self) -> PathBuf {
382        self.config
383            .html_config()
384            .unwrap_or_default()
385            .theme_dir(&self.root)
386    }
387}
388
389/// Look at the `Config` and try to figure out what renderers to use.
390fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
391    let mut renderers = Vec::new();
392
393    if let Some(output_table) = config.get("output").and_then(Value::as_table) {
394        renderers.extend(output_table.iter().map(|(key, table)| {
395            if key == "html" {
396                Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
397            } else if key == "markdown" {
398                Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
399            } else {
400                interpret_custom_renderer(key, table)
401            }
402        }));
403    }
404
405    // if we couldn't find anything, add the HTML renderer as a default
406    if renderers.is_empty() {
407        renderers.push(Box::new(HtmlHandlebars::new()));
408    }
409
410    renderers
411}
412
413const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
414
415fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
416    let name = pre.name();
417    name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
418}
419
420/// Look at the `MDBook` and try to figure out what preprocessors to run.
421fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
422    // Collect the names of all preprocessors intended to be run, and the order
423    // in which they should be run.
424    let mut preprocessor_names = TopologicalSort::<String>::new();
425
426    if config.build.use_default_preprocessors {
427        for name in DEFAULT_PREPROCESSORS {
428            preprocessor_names.insert(name.to_string());
429        }
430    }
431
432    if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
433        for (name, table) in preprocessor_table.iter() {
434            preprocessor_names.insert(name.to_string());
435
436            let exists = |name| {
437                (config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
438                    || preprocessor_table.contains_key(name)
439            };
440
441            if let Some(before) = table.get("before") {
442                let before = before.as_array().ok_or_else(|| {
443                    Error::msg(format!(
444                        "Expected preprocessor.{}.before to be an array",
445                        name
446                    ))
447                })?;
448                for after in before {
449                    let after = after.as_str().ok_or_else(|| {
450                        Error::msg(format!(
451                            "Expected preprocessor.{}.before to contain strings",
452                            name
453                        ))
454                    })?;
455
456                    if !exists(after) {
457                        // Only warn so that preprocessors can be toggled on and off (e.g. for
458                        // troubleshooting) without having to worry about order too much.
459                        warn!(
460                            "preprocessor.{}.after contains \"{}\", which was not found",
461                            name, after
462                        );
463                    } else {
464                        preprocessor_names.add_dependency(name, after);
465                    }
466                }
467            }
468
469            if let Some(after) = table.get("after") {
470                let after = after.as_array().ok_or_else(|| {
471                    Error::msg(format!(
472                        "Expected preprocessor.{}.after to be an array",
473                        name
474                    ))
475                })?;
476                for before in after {
477                    let before = before.as_str().ok_or_else(|| {
478                        Error::msg(format!(
479                            "Expected preprocessor.{}.after to contain strings",
480                            name
481                        ))
482                    })?;
483
484                    if !exists(before) {
485                        // See equivalent warning above for rationale
486                        warn!(
487                            "preprocessor.{}.before contains \"{}\", which was not found",
488                            name, before
489                        );
490                    } else {
491                        preprocessor_names.add_dependency(before, name);
492                    }
493                }
494            }
495        }
496    }
497
498    // Now that all links have been established, queue preprocessors in a suitable order
499    let mut preprocessors = Vec::with_capacity(preprocessor_names.len());
500    // `pop_all()` returns an empty vector when no more items are not being depended upon
501    for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
502        .take_while(|names| !names.is_empty())
503    {
504        // The `topological_sort` crate does not guarantee a stable order for ties, even across
505        // runs of the same program. Thus, we break ties manually by sorting.
506        // Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
507        // values ([1]), which may not be an alphabetical sort.
508        // As mentioned in [1], doing so depends on locale, which is not desirable for deciding
509        // preprocessor execution order.
510        // [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
511        names.sort();
512        for name in names {
513            let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
514                "links" => Box::new(LinkPreprocessor::new()),
515                "index" => Box::new(IndexPreprocessor::new()),
516                _ => {
517                    // The only way to request a custom preprocessor is through the `preprocessor`
518                    // table, so it must exist, be a table, and contain the key.
519                    let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name];
520                    let command = get_custom_preprocessor_cmd(&name, table);
521                    Box::new(CmdPreprocessor::new(name, command))
522                }
523            };
524            preprocessors.push(preprocessor);
525        }
526    }
527
528    // "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
529    // Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
530    if preprocessor_names.is_empty() {
531        Ok(preprocessors)
532    } else {
533        Err(Error::msg("Cyclic dependency detected in preprocessors"))
534    }
535}
536
537fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
538    table
539        .get("command")
540        .and_then(Value::as_str)
541        .map(ToString::to_string)
542        .unwrap_or_else(|| format!("mdbook-{}", key))
543}
544
545fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
546    // look for the `command` field, falling back to using the key
547    // prepended by "mdbook-"
548    let table_dot_command = table
549        .get("command")
550        .and_then(Value::as_str)
551        .map(ToString::to_string);
552
553    let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
554
555    Box::new(CmdRenderer::new(key.to_string(), command))
556}
557
558/// Check whether we should run a particular `Preprocessor` in combination
559/// with the renderer, falling back to `Preprocessor::supports_renderer()`
560/// method if the user doesn't say anything.
561///
562/// The `build.use-default-preprocessors` config option can be used to ensure
563/// default preprocessors always run if they support the renderer.
564fn preprocessor_should_run(
565    preprocessor: &dyn Preprocessor,
566    renderer: &dyn Renderer,
567    cfg: &Config,
568) -> bool {
569    // default preprocessors should be run by default (if supported)
570    if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
571        return preprocessor.supports_renderer(renderer.name());
572    }
573
574    let key = format!("preprocessor.{}.renderers", preprocessor.name());
575    let renderer_name = renderer.name();
576
577    if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
578        return explicit_renderers
579            .iter()
580            .filter_map(Value::as_str)
581            .any(|name| name == renderer_name);
582    }
583
584    preprocessor.supports_renderer(renderer_name)
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590    use std::str::FromStr;
591    use toml::value::{Table, Value};
592
593    #[test]
594    fn config_defaults_to_html_renderer_if_empty() {
595        let cfg = Config::default();
596
597        // make sure we haven't got anything in the `output` table
598        assert!(cfg.get("output").is_none());
599
600        let got = determine_renderers(&cfg);
601
602        assert_eq!(got.len(), 1);
603        assert_eq!(got[0].name(), "html");
604    }
605
606    #[test]
607    fn add_a_random_renderer_to_the_config() {
608        let mut cfg = Config::default();
609        cfg.set("output.random", Table::new()).unwrap();
610
611        let got = determine_renderers(&cfg);
612
613        assert_eq!(got.len(), 1);
614        assert_eq!(got[0].name(), "random");
615    }
616
617    #[test]
618    fn add_a_random_renderer_with_custom_command_to_the_config() {
619        let mut cfg = Config::default();
620
621        let mut table = Table::new();
622        table.insert("command".to_string(), Value::String("false".to_string()));
623        cfg.set("output.random", table).unwrap();
624
625        let got = determine_renderers(&cfg);
626
627        assert_eq!(got.len(), 1);
628        assert_eq!(got[0].name(), "random");
629    }
630
631    #[test]
632    fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
633        let cfg = Config::default();
634
635        // make sure we haven't got anything in the `preprocessor` table
636        assert!(cfg.get("preprocessor").is_none());
637
638        let got = determine_preprocessors(&cfg);
639
640        assert!(got.is_ok());
641        assert_eq!(got.as_ref().unwrap().len(), 2);
642        assert_eq!(got.as_ref().unwrap()[0].name(), "index");
643        assert_eq!(got.as_ref().unwrap()[1].name(), "links");
644    }
645
646    #[test]
647    fn use_default_preprocessors_works() {
648        let mut cfg = Config::default();
649        cfg.build.use_default_preprocessors = false;
650
651        let got = determine_preprocessors(&cfg).unwrap();
652
653        assert_eq!(got.len(), 0);
654    }
655
656    #[test]
657    fn can_determine_third_party_preprocessors() {
658        let cfg_str = r#"
659        [book]
660        title = "Some Book"
661
662        [preprocessor.random]
663
664        [build]
665        build-dir = "outputs"
666        create-missing = false
667        "#;
668
669        let cfg = Config::from_str(cfg_str).unwrap();
670
671        // make sure the `preprocessor.random` table exists
672        assert!(cfg.get_preprocessor("random").is_some());
673
674        let got = determine_preprocessors(&cfg).unwrap();
675
676        assert!(got.into_iter().any(|p| p.name() == "random"));
677    }
678
679    #[test]
680    fn preprocessors_can_provide_their_own_commands() {
681        let cfg_str = r#"
682        [preprocessor.random]
683        command = "python random.py"
684        "#;
685
686        let cfg = Config::from_str(cfg_str).unwrap();
687
688        // make sure the `preprocessor.random` table exists
689        let random = cfg.get_preprocessor("random").unwrap();
690        let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
691
692        assert_eq!(random, "python random.py");
693    }
694
695    #[test]
696    fn preprocessor_before_must_be_array() {
697        let cfg_str = r#"
698        [preprocessor.random]
699        before = 0
700        "#;
701
702        let cfg = Config::from_str(cfg_str).unwrap();
703
704        assert!(determine_preprocessors(&cfg).is_err());
705    }
706
707    #[test]
708    fn preprocessor_after_must_be_array() {
709        let cfg_str = r#"
710        [preprocessor.random]
711        after = 0
712        "#;
713
714        let cfg = Config::from_str(cfg_str).unwrap();
715
716        assert!(determine_preprocessors(&cfg).is_err());
717    }
718
719    #[test]
720    fn preprocessor_order_is_honored() {
721        let cfg_str = r#"
722        [preprocessor.random]
723        before = [ "last" ]
724        after = [ "index" ]
725
726        [preprocessor.last]
727        after = [ "links", "index" ]
728        "#;
729
730        let cfg = Config::from_str(cfg_str).unwrap();
731
732        let preprocessors = determine_preprocessors(&cfg).unwrap();
733        let index = |name| {
734            preprocessors
735                .iter()
736                .enumerate()
737                .find(|(_, preprocessor)| preprocessor.name() == name)
738                .unwrap()
739                .0
740        };
741        let assert_before = |before, after| {
742            if index(before) >= index(after) {
743                eprintln!("Preprocessor order:");
744                for preprocessor in &preprocessors {
745                    eprintln!("  {}", preprocessor.name());
746                }
747                panic!("{} should come before {}", before, after);
748            }
749        };
750
751        assert_before("index", "random");
752        assert_before("index", "last");
753        assert_before("random", "last");
754        assert_before("links", "last");
755    }
756
757    #[test]
758    fn cyclic_dependencies_are_detected() {
759        let cfg_str = r#"
760        [preprocessor.links]
761        before = [ "index" ]
762
763        [preprocessor.index]
764        before = [ "links" ]
765        "#;
766
767        let cfg = Config::from_str(cfg_str).unwrap();
768
769        assert!(determine_preprocessors(&cfg).is_err());
770    }
771
772    #[test]
773    fn dependencies_dont_register_undefined_preprocessors() {
774        let cfg_str = r#"
775        [preprocessor.links]
776        before = [ "random" ]
777        "#;
778
779        let cfg = Config::from_str(cfg_str).unwrap();
780
781        let preprocessors = determine_preprocessors(&cfg).unwrap();
782
783        assert!(!preprocessors
784            .iter()
785            .any(|preprocessor| preprocessor.name() == "random"));
786    }
787
788    #[test]
789    fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
790        let cfg_str = r#"
791        [preprocessor.random]
792        before = [ "links" ]
793
794        [build]
795        use-default-preprocessors = false
796        "#;
797
798        let cfg = Config::from_str(cfg_str).unwrap();
799
800        let preprocessors = determine_preprocessors(&cfg).unwrap();
801
802        assert!(!preprocessors
803            .iter()
804            .any(|preprocessor| preprocessor.name() == "links"));
805    }
806
807    #[test]
808    fn config_respects_preprocessor_selection() {
809        let cfg_str = r#"
810        [preprocessor.links]
811        renderers = ["html"]
812        "#;
813
814        let cfg = Config::from_str(cfg_str).unwrap();
815
816        // double-check that we can access preprocessor.links.renderers[0]
817        let html = cfg
818            .get_preprocessor("links")
819            .and_then(|links| links.get("renderers"))
820            .and_then(Value::as_array)
821            .and_then(|renderers| renderers.get(0))
822            .and_then(Value::as_str)
823            .unwrap();
824        assert_eq!(html, "html");
825        let html_renderer = HtmlHandlebars::default();
826        let pre = LinkPreprocessor::new();
827
828        let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
829        assert!(should_run);
830    }
831
832    struct BoolPreprocessor(bool);
833    impl Preprocessor for BoolPreprocessor {
834        fn name(&self) -> &str {
835            "bool-preprocessor"
836        }
837
838        fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
839            unimplemented!()
840        }
841
842        fn supports_renderer(&self, _renderer: &str) -> bool {
843            self.0
844        }
845    }
846
847    #[test]
848    fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
849        let cfg = Config::default();
850        let html = HtmlHandlebars::new();
851
852        let should_be = true;
853        let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
854        assert_eq!(got, should_be);
855
856        let should_be = false;
857        let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
858        assert_eq!(got, should_be);
859    }
860}