mdbook_driver/
mdbook.rs

1//! The high-level interface for loading and rendering books.
2
3use crate::builtin_preprocessors::{CmdPreprocessor, IndexPreprocessor, LinkPreprocessor};
4use crate::builtin_renderers::{CmdRenderer, MarkdownRenderer};
5use crate::init::BookBuilder;
6use crate::load::{load_book, load_book_from_disk};
7use anyhow::{Context, Error, Result, bail};
8use indexmap::IndexMap;
9use mdbook_core::book::{Book, BookItem, BookItems};
10use mdbook_core::config::{Config, RustEdition};
11use mdbook_core::utils::fs;
12use mdbook_html::HtmlHandlebars;
13use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
14use mdbook_renderer::{RenderContext, Renderer};
15use mdbook_summary::Summary;
16use serde::Deserialize;
17use std::ffi::OsString;
18use std::io::IsTerminal;
19use std::path::{Path, PathBuf};
20use std::process::Command;
21use tempfile::Builder as TempFileBuilder;
22use topological_sort::TopologicalSort;
23use tracing::{debug, error, info, trace, warn};
24
25#[cfg(test)]
26mod tests;
27
28/// The object used to manage and build a book.
29pub struct MDBook {
30    /// The book's root directory.
31    pub root: PathBuf,
32
33    /// The configuration used to tweak now a book is built.
34    pub config: Config,
35
36    /// A representation of the book's contents in memory.
37    pub book: Book,
38
39    /// Renderers to execute.
40    renderers: IndexMap<String, Box<dyn Renderer>>,
41
42    /// Pre-processors to be run on the book.
43    preprocessors: IndexMap<String, Box<dyn Preprocessor>>,
44}
45
46impl MDBook {
47    /// Load a book from its root directory on disk.
48    pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
49        let book_root = book_root.into();
50        let config_location = book_root.join("book.toml");
51
52        let mut config = if config_location.exists() {
53            debug!("Loading config from {}", config_location.display());
54            Config::from_disk(&config_location)?
55        } else {
56            Config::default()
57        };
58
59        config.update_from_env()?;
60
61        if tracing::enabled!(tracing::Level::TRACE) {
62            for line in format!("Config: {config:#?}").lines() {
63                trace!("{}", line);
64            }
65        }
66
67        MDBook::load_with_config(book_root, config)
68    }
69
70    /// Load a book from its root directory using a custom `Config`.
71    pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
72        let root = book_root.into();
73
74        let src_dir = root.join(&config.book.src);
75        let book = load_book(src_dir, &config.build)?;
76
77        let renderers = determine_renderers(&config)?;
78        let preprocessors = determine_preprocessors(&config, &root)?;
79
80        Ok(MDBook {
81            root,
82            config,
83            book,
84            renderers,
85            preprocessors,
86        })
87    }
88
89    /// Load a book from its root directory using a custom `Config` and a custom summary.
90    pub fn load_with_config_and_summary<P: Into<PathBuf>>(
91        book_root: P,
92        config: Config,
93        summary: Summary,
94    ) -> Result<MDBook> {
95        let root = book_root.into();
96
97        let src_dir = root.join(&config.book.src);
98        let book = load_book_from_disk(&summary, src_dir)?;
99
100        let renderers = determine_renderers(&config)?;
101        let preprocessors = determine_preprocessors(&config, &root)?;
102
103        Ok(MDBook {
104            root,
105            config,
106            book,
107            renderers,
108            preprocessors,
109        })
110    }
111
112    /// Returns a flat depth-first iterator over the [`BookItem`]s of the book.
113    ///
114    /// ```no_run
115    /// # use mdbook_driver::MDBook;
116    /// # use mdbook_driver::book::BookItem;
117    /// # let book = MDBook::load("mybook").unwrap();
118    /// for item in book.iter() {
119    ///     match *item {
120    ///         BookItem::Chapter(ref chapter) => {},
121    ///         BookItem::Separator => {},
122    ///         BookItem::PartTitle(ref title) => {}
123    ///         _ => {}
124    ///     }
125    /// }
126    ///
127    /// // would print something like this:
128    /// // 1. Chapter 1
129    /// // 1.1 Sub Chapter
130    /// // 1.2 Sub Chapter
131    /// // 2. Chapter 2
132    /// //
133    /// // etc.
134    /// ```
135    pub fn iter(&self) -> BookItems<'_> {
136        self.book.iter()
137    }
138
139    /// `init()` gives you a `BookBuilder` which you can use to setup a new book
140    /// and its accompanying directory structure.
141    ///
142    /// The `BookBuilder` creates some boilerplate files and directories to get
143    /// you started with your book.
144    ///
145    /// ```text
146    /// book-test/
147    /// ├── book
148    /// └── src
149    ///     ├── chapter_1.md
150    ///     └── SUMMARY.md
151    /// ```
152    ///
153    /// It uses the path provided as the root directory for your book, then adds
154    /// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
155    /// to get you started.
156    pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
157        BookBuilder::new(book_root)
158    }
159
160    /// Tells the renderer to build our book and put it in the build directory.
161    pub fn build(&self) -> Result<()> {
162        info!("Book building has started");
163
164        for renderer in self.renderers.values() {
165            self.execute_build_process(&**renderer)?;
166        }
167
168        Ok(())
169    }
170
171    /// Run preprocessors and return the final book.
172    pub fn preprocess_book(&self, renderer: &dyn Renderer) -> Result<(Book, PreprocessorContext)> {
173        let preprocess_ctx = PreprocessorContext::new(
174            self.root.clone(),
175            self.config.clone(),
176            renderer.name().to_string(),
177        );
178        let mut preprocessed_book = self.book.clone();
179        for preprocessor in self.preprocessors.values() {
180            if preprocessor_should_run(&**preprocessor, renderer, &self.config)? {
181                debug!("Running the {} preprocessor.", preprocessor.name());
182                preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
183            }
184        }
185        Ok((preprocessed_book, preprocess_ctx))
186    }
187
188    /// Run the entire build process for a particular [`Renderer`].
189    pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
190        let (preprocessed_book, preprocess_ctx) = self.preprocess_book(renderer)?;
191
192        let name = renderer.name();
193        let build_dir = self.build_dir_for(name);
194
195        let mut render_context = RenderContext::new(
196            self.root.clone(),
197            preprocessed_book,
198            self.config.clone(),
199            build_dir,
200        );
201        render_context
202            .chapter_titles
203            .extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
204
205        info!("Running the {} backend", renderer.name());
206        renderer
207            .render(&render_context)
208            .with_context(|| "Rendering failed")
209    }
210
211    /// You can change the default renderer to another one by using this method.
212    /// The only requirement is that your renderer implement the [`Renderer`]
213    /// trait.
214    pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
215        self.renderers
216            .insert(renderer.name().to_string(), Box::new(renderer));
217        self
218    }
219
220    /// Register a [`Preprocessor`] to be used when rendering the book.
221    pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
222        self.preprocessors
223            .insert(preprocessor.name().to_string(), Box::new(preprocessor));
224        self
225    }
226
227    /// Run `rustdoc` tests on the book, linking against the provided libraries.
228    pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
229        // test_chapter with chapter:None will run all tests.
230        self.test_chapter(library_paths, None)
231    }
232
233    /// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries.
234    /// If `chapter` is `None`, all tests will be run.
235    pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
236        let cwd = std::env::current_dir()?;
237        let library_args: Vec<OsString> = library_paths
238            .into_iter()
239            .flat_map(|path| {
240                let path = Path::new(path);
241                let path = if path.is_relative() {
242                    cwd.join(path).into_os_string()
243                } else {
244                    path.to_path_buf().into_os_string()
245                };
246                [OsString::from("-L"), path]
247            })
248            .collect();
249
250        let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
251
252        let mut chapter_found = false;
253
254        struct TestRenderer;
255        impl Renderer for TestRenderer {
256            // FIXME: Is "test" the proper renderer name to use here?
257            fn name(&self) -> &str {
258                "test"
259            }
260
261            fn render(&self, _: &RenderContext) -> Result<()> {
262                Ok(())
263            }
264        }
265
266        // Index Preprocessor is disabled so that chapter paths
267        // continue to point to the actual markdown files.
268        self.preprocessors = determine_preprocessors(&self.config, &self.root)?;
269        self.preprocessors
270            .shift_remove_entry(IndexPreprocessor::NAME);
271        let (book, _) = self.preprocess_book(&TestRenderer)?;
272
273        let color_output = std::io::stderr().is_terminal();
274        let mut failed = false;
275        for item in book.iter() {
276            if let BookItem::Chapter(ref ch) = *item {
277                let chapter_path = match ch.path {
278                    Some(ref path) if !path.as_os_str().is_empty() => path,
279                    _ => continue,
280                };
281
282                if let Some(chapter) = chapter {
283                    if ch.name != chapter && chapter_path.to_str() != Some(chapter) {
284                        if chapter == "?" {
285                            info!("Skipping chapter '{}'...", ch.name);
286                        }
287                        continue;
288                    }
289                }
290                chapter_found = true;
291                info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
292
293                // write preprocessed file to tempdir
294                let path = temp_dir.path().join(chapter_path);
295                fs::write(&path, &ch.content)?;
296
297                let mut cmd = Command::new("rustdoc");
298                cmd.current_dir(temp_dir.path())
299                    .arg(chapter_path)
300                    .arg("--test")
301                    .args(&library_args);
302
303                if let Some(edition) = self.config.rust.edition {
304                    match edition {
305                        RustEdition::E2015 => {
306                            cmd.args(["--edition", "2015"]);
307                        }
308                        RustEdition::E2018 => {
309                            cmd.args(["--edition", "2018"]);
310                        }
311                        RustEdition::E2021 => {
312                            cmd.args(["--edition", "2021"]);
313                        }
314                        RustEdition::E2024 => {
315                            cmd.args(["--edition", "2024"]);
316                        }
317                        _ => panic!("RustEdition {edition:?} not covered"),
318                    }
319                }
320
321                if color_output {
322                    cmd.args(["--color", "always"]);
323                }
324
325                debug!("running {:?}", cmd);
326                let output = cmd
327                    .output()
328                    .with_context(|| "failed to execute `rustdoc`")?;
329
330                if !output.status.success() {
331                    failed = true;
332                    error!(
333                        "rustdoc returned an error:\n\
334                        \n--- stdout\n{}\n--- stderr\n{}",
335                        String::from_utf8_lossy(&output.stdout),
336                        String::from_utf8_lossy(&output.stderr)
337                    );
338                }
339            }
340        }
341        if failed {
342            bail!("One or more tests failed");
343        }
344        if let Some(chapter) = chapter {
345            if !chapter_found {
346                bail!("Chapter not found: {}", chapter);
347            }
348        }
349        Ok(())
350    }
351
352    /// The logic for determining where a backend should put its build
353    /// artefacts.
354    ///
355    /// If there is only 1 renderer, put it in the directory pointed to by the
356    /// `build.build_dir` key in [`Config`]. If there is more than one then the
357    /// renderer gets its own directory within the main build dir.
358    ///
359    /// i.e. If there were only one renderer (in this case, the HTML renderer):
360    ///
361    /// - build/
362    ///   - index.html
363    ///   - ...
364    ///
365    /// Otherwise if there are multiple:
366    ///
367    /// - build/
368    ///   - epub/
369    ///     - my_awesome_book.epub
370    ///   - html/
371    ///     - index.html
372    ///     - ...
373    ///   - latex/
374    ///     - my_awesome_book.tex
375    ///
376    pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
377        let build_dir = self.root.join(&self.config.build.build_dir);
378
379        if self.renderers.len() <= 1 {
380            build_dir
381        } else {
382            build_dir.join(backend_name)
383        }
384    }
385
386    /// Get the directory containing this book's source files.
387    pub fn source_dir(&self) -> PathBuf {
388        self.root.join(&self.config.book.src)
389    }
390
391    /// Get the directory containing the theme resources for the book.
392    pub fn theme_dir(&self) -> PathBuf {
393        self.config
394            .html_config()
395            .unwrap_or_default()
396            .theme_dir(&self.root)
397    }
398}
399
400/// An `output` table.
401#[derive(Deserialize)]
402struct OutputConfig {
403    command: Option<String>,
404}
405
406/// Look at the `Config` and try to figure out what renderers to use.
407fn determine_renderers(config: &Config) -> Result<IndexMap<String, Box<dyn Renderer>>> {
408    let mut renderers = IndexMap::new();
409
410    let outputs = config.outputs::<OutputConfig>()?;
411    renderers.extend(outputs.into_iter().map(|(key, table)| {
412        let renderer = 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            let command = table.command.unwrap_or_else(|| format!("mdbook-{key}"));
418            Box::new(CmdRenderer::new(key.clone(), command))
419        };
420        (key, renderer)
421    }));
422
423    // if we couldn't find anything, add the HTML renderer as a default
424    if renderers.is_empty() {
425        renderers.insert("html".to_string(), Box::new(HtmlHandlebars::new()));
426    }
427
428    Ok(renderers)
429}
430
431const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
432
433fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
434    let name = pre.name();
435    name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
436}
437
438/// A `preprocessor` table.
439#[derive(Deserialize)]
440struct PreprocessorConfig {
441    command: Option<String>,
442    #[serde(default)]
443    before: Vec<String>,
444    #[serde(default)]
445    after: Vec<String>,
446    #[serde(default)]
447    optional: bool,
448}
449
450/// Look at the `MDBook` and try to figure out what preprocessors to run.
451fn determine_preprocessors(
452    config: &Config,
453    root: &Path,
454) -> Result<IndexMap<String, Box<dyn Preprocessor>>> {
455    // Collect the names of all preprocessors intended to be run, and the order
456    // in which they should be run.
457    let mut preprocessor_names = TopologicalSort::<String>::new();
458
459    if config.build.use_default_preprocessors {
460        for name in DEFAULT_PREPROCESSORS {
461            preprocessor_names.insert(name.to_string());
462        }
463    }
464
465    let preprocessor_table = config.preprocessors::<PreprocessorConfig>()?;
466
467    for (name, table) in preprocessor_table.iter() {
468        preprocessor_names.insert(name.to_string());
469
470        let exists = |name| {
471            (config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
472                || preprocessor_table.contains_key(name)
473        };
474
475        for after in &table.before {
476            if !exists(&after) {
477                // Only warn so that preprocessors can be toggled on and off (e.g. for
478                // troubleshooting) without having to worry about order too much.
479                warn!(
480                    "preprocessor.{}.after contains \"{}\", which was not found",
481                    name, after
482                );
483            } else {
484                preprocessor_names.add_dependency(name, after);
485            }
486        }
487
488        for before in &table.after {
489            if !exists(&before) {
490                // See equivalent warning above for rationale
491                warn!(
492                    "preprocessor.{}.before contains \"{}\", which was not found",
493                    name, before
494                );
495            } else {
496                preprocessor_names.add_dependency(before, name);
497            }
498        }
499    }
500
501    // Now that all links have been established, queue preprocessors in a suitable order
502    let mut preprocessors = IndexMap::with_capacity(preprocessor_names.len());
503    // `pop_all()` returns an empty vector when no more items are not being depended upon
504    for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
505        .take_while(|names| !names.is_empty())
506    {
507        // The `topological_sort` crate does not guarantee a stable order for ties, even across
508        // runs of the same program. Thus, we break ties manually by sorting.
509        // Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
510        // values ([1]), which may not be an alphabetical sort.
511        // As mentioned in [1], doing so depends on locale, which is not desirable for deciding
512        // preprocessor execution order.
513        // [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
514        names.sort();
515        for name in names {
516            let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
517                "links" => Box::new(LinkPreprocessor::new()),
518                "index" => Box::new(IndexPreprocessor::new()),
519                _ => {
520                    // The only way to request a custom preprocessor is through the `preprocessor`
521                    // table, so it must exist, be a table, and contain the key.
522                    let table = &preprocessor_table[&name];
523                    let command = table
524                        .command
525                        .to_owned()
526                        .unwrap_or_else(|| format!("mdbook-{name}"));
527                    Box::new(CmdPreprocessor::new(
528                        name.clone(),
529                        command,
530                        root.to_owned(),
531                        table.optional,
532                    ))
533                }
534            };
535            preprocessors.insert(name, preprocessor);
536        }
537    }
538
539    // "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
540    // Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
541    if preprocessor_names.is_empty() {
542        Ok(preprocessors)
543    } else {
544        Err(Error::msg("Cyclic dependency detected in preprocessors"))
545    }
546}
547
548/// Check whether we should run a particular `Preprocessor` in combination
549/// with the renderer, falling back to `Preprocessor::supports_renderer()`
550/// method if the user doesn't say anything.
551///
552/// The `build.use-default-preprocessors` config option can be used to ensure
553/// default preprocessors always run if they support the renderer.
554fn preprocessor_should_run(
555    preprocessor: &dyn Preprocessor,
556    renderer: &dyn Renderer,
557    cfg: &Config,
558) -> Result<bool> {
559    // default preprocessors should be run by default (if supported)
560    if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
561        return preprocessor.supports_renderer(renderer.name());
562    }
563
564    let key = format!("preprocessor.{}.renderers", preprocessor.name());
565    let renderer_name = renderer.name();
566
567    match cfg.get::<Vec<String>>(&key) {
568        Ok(Some(explicit_renderers)) => {
569            Ok(explicit_renderers.iter().any(|name| name == renderer_name))
570        }
571        Ok(None) => preprocessor.supports_renderer(renderer_name),
572        Err(e) => bail!("failed to get `{key}`: {e}"),
573    }
574}