mdbook/crates/mdbook-driver/src/mdbook.rs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

570 lines
20 KiB
Rust
Raw Normal View History

2025-07-21 21:42:58 -07:00
//! The high-level interface for loading and rendering books.
use crate::builtin_preprocessors::{CmdPreprocessor, IndexPreprocessor, LinkPreprocessor};
use crate::builtin_renderers::{CmdRenderer, MarkdownRenderer};
use crate::init::BookBuilder;
use crate::load::{load_book, load_book_from_disk};
use anyhow::{Context, Error, Result, bail};
use indexmap::IndexMap;
2025-07-21 21:42:58 -07:00
use mdbook_core::book::{Book, BookItem, BookItems};
use mdbook_core::config::{Config, RustEdition};
use mdbook_core::utils::fs;
use mdbook_html::HtmlHandlebars;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use mdbook_renderer::{RenderContext, Renderer};
2025-07-21 21:42:58 -07:00
use mdbook_summary::Summary;
use serde::Deserialize;
use std::ffi::OsString;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
2016-04-26 23:04:27 +02:00
use std::process::Command;
use tempfile::Builder as TempFileBuilder;
use topological_sort::TopologicalSort;
use tracing::{debug, info, trace, warn};
2016-04-26 23:04:27 +02:00
2025-07-21 21:42:58 -07:00
#[cfg(test)]
mod tests;
/// The object used to manage and build a book.
pub struct MDBook {
/// The book's root directory.
2017-09-30 21:13:00 +08:00
pub root: PathBuf,
/// The configuration used to tweak now a book is built.
pub config: Config,
/// A representation of the book's contents in memory.
pub book: Book,
2016-04-26 23:04:27 +02:00
/// Renderers to execute.
renderers: IndexMap<String, Box<dyn Renderer>>,
/// Pre-processors to be run on the book.
preprocessors: IndexMap<String, Box<dyn Preprocessor>>,
2016-04-26 23:04:27 +02:00
}
impl MDBook {
/// Load a book from its root directory on disk.
pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
let book_root = book_root.into();
let config_location = book_root.join("book.toml");
2016-04-26 23:04:27 +02:00
let mut config = if config_location.exists() {
debug!("Loading config from {}", config_location.display());
Config::from_disk(&config_location)?
} else {
Config::default()
};
2016-04-26 23:04:27 +02:00
config.update_from_env()?;
if tracing::enabled!(tracing::Level::TRACE) {
for line in format!("Config: {config:#?}").lines() {
trace!("{}", line);
}
}
MDBook::load_with_config(book_root, config)
}
/// Load a book from its root directory using a custom `Config`.
pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
let root = book_root.into();
let src_dir = root.join(&config.book.src);
2025-07-21 21:42:58 -07:00
let book = load_book(src_dir, &config.build)?;
let renderers = determine_renderers(&config)?;
let preprocessors = determine_preprocessors(&config, &root)?;
2016-04-26 23:04:27 +02:00
Ok(MDBook {
root,
config,
book,
renderers,
preprocessors,
})
2016-04-26 23:04:27 +02:00
}
/// Load a book from its root directory using a custom `Config` and a custom summary.
pub fn load_with_config_and_summary<P: Into<PathBuf>>(
book_root: P,
config: Config,
2019-05-05 21:57:43 +07:00
summary: Summary,
) -> Result<MDBook> {
let root = book_root.into();
let src_dir = root.join(&config.book.src);
2025-07-21 21:42:58 -07:00
let book = load_book_from_disk(&summary, src_dir)?;
let renderers = determine_renderers(&config)?;
let preprocessors = determine_preprocessors(&config, &root)?;
2016-04-26 23:04:27 +02:00
Ok(MDBook {
root,
config,
book,
renderers,
preprocessors,
})
2016-04-26 23:04:27 +02:00
}
/// Returns a flat depth-first iterator over the [`BookItem`]s of the book.
2016-04-26 23:04:27 +02:00
///
/// ```no_run
2025-07-21 21:42:58 -07:00
/// # use mdbook_driver::MDBook;
/// # use mdbook_driver::book::BookItem;
2017-11-18 22:16:35 +08:00
/// # let book = MDBook::load("mybook").unwrap();
2016-04-26 23:04:27 +02:00
/// for item in book.iter() {
/// match *item {
/// BookItem::Chapter(ref chapter) => {},
2017-11-18 22:16:35 +08:00
/// BookItem::Separator => {},
2020-03-20 21:18:07 -05:00
/// BookItem::PartTitle(ref title) => {}
/// _ => {}
2016-04-26 23:04:27 +02:00
/// }
/// }
///
/// // would print something like this:
/// // 1. Chapter 1
/// // 1.1 Sub Chapter
/// // 1.2 Sub Chapter
/// // 2. Chapter 2
/// //
/// // etc.
/// ```
pub fn iter(&self) -> BookItems<'_> {
self.book.iter()
2016-04-26 23:04:27 +02:00
}
/// `init()` gives you a `BookBuilder` which you can use to setup a new book
/// and its accompanying directory structure.
///
/// The `BookBuilder` creates some boilerplate files and directories to get
/// you started with your book.
2016-04-26 23:04:27 +02:00
///
/// ```text
/// book-test/
/// ├── book
/// └── src
/// ├── chapter_1.md
/// └── SUMMARY.md
/// ```
///
/// It uses the path provided as the root directory for your book, then adds
/// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
/// to get you started.
pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
BookBuilder::new(book_root)
2016-04-26 23:04:27 +02:00
}
2017-11-18 22:07:08 +08:00
/// Tells the renderer to build our book and put it in the build directory.
pub fn build(&self) -> Result<()> {
info!("Book building has started");
2016-04-26 23:04:27 +02:00
for renderer in self.renderers.values() {
self.execute_build_process(&**renderer)?;
}
Ok(())
}
/// Run preprocessors and return the final book.
pub fn preprocess_book(&self, renderer: &dyn Renderer) -> Result<(Book, PreprocessorContext)> {
let preprocess_ctx = PreprocessorContext::new(
self.root.clone(),
self.config.clone(),
renderer.name().to_string(),
);
let mut preprocessed_book = self.book.clone();
for preprocessor in self.preprocessors.values() {
if preprocessor_should_run(&**preprocessor, renderer, &self.config)? {
debug!("Running the {} preprocessor.", preprocessor.name());
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
}
}
Ok((preprocessed_book, preprocess_ctx))
}
/// Run the entire build process for a particular [`Renderer`].
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
let (preprocessed_book, preprocess_ctx) = self.preprocess_book(renderer)?;
let name = renderer.name();
let build_dir = self.build_dir_for(name);
let mut render_context = RenderContext::new(
self.root.clone(),
preprocessed_book,
self.config.clone(),
build_dir,
);
render_context
.chapter_titles
.extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
info!("Running the {} backend", renderer.name());
renderer
.render(&render_context)
.with_context(|| "Rendering failed")
}
2017-11-18 22:16:35 +08:00
/// You can change the default renderer to another one by using this method.
/// The only requirement is that your renderer implement the [`Renderer`]
/// trait.
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
self.renderers
.insert(renderer.name().to_string(), Box::new(renderer));
2016-04-26 23:04:27 +02:00
self
}
/// Register a [`Preprocessor`] to be used when rendering the book.
2019-05-03 19:59:58 +02:00
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
self.preprocessors
.insert(preprocessor.name().to_string(), Box::new(preprocessor));
self
}
/// Run `rustdoc` tests on the book, linking against the provided libraries.
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
// test_chapter with chapter:None will run all tests.
self.test_chapter(library_paths, None)
}
/// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries.
/// If `chapter` is `None`, all tests will be run.
pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
let cwd = std::env::current_dir()?;
let library_args: Vec<OsString> = library_paths
.into_iter()
.flat_map(|path| {
let path = Path::new(path);
let path = if path.is_relative() {
cwd.join(path).into_os_string()
} else {
path.to_path_buf().into_os_string()
};
[OsString::from("-L"), path]
})
.collect();
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
let mut chapter_found = false;
struct TestRenderer;
impl Renderer for TestRenderer {
// FIXME: Is "test" the proper renderer name to use here?
fn name(&self) -> &str {
"test"
}
2018-01-07 16:21:46 +00:00
fn render(&self, _: &RenderContext) -> Result<()> {
Ok(())
}
}
let (book, _) = self.preprocess_book(&TestRenderer)?;
2018-01-07 16:21:46 +00:00
let color_output = std::io::stderr().is_terminal();
let mut failed = false;
for item in book.iter() {
if let BookItem::Chapter(ref ch) = *item {
2020-03-09 22:34:28 +01:00
let chapter_path = match ch.path {
Some(ref path) if !path.as_os_str().is_empty() => path,
_ => continue,
};
if let Some(chapter) = chapter {
if ch.name != chapter && chapter_path.to_str() != Some(chapter) {
if chapter == "?" {
info!("Skipping chapter '{}'...", ch.name);
}
continue;
}
}
chapter_found = true;
info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
2020-03-09 22:34:28 +01:00
// write preprocessed file to tempdir
2023-05-13 09:44:11 -07:00
let path = temp_dir.path().join(chapter_path);
fs::write(&path, &ch.content)?;
2020-03-09 22:34:28 +01:00
let mut cmd = Command::new("rustdoc");
cmd.current_dir(temp_dir.path())
2024-05-13 12:13:50 -07:00
.arg(chapter_path)
.arg("--test")
.args(&library_args);
2020-03-09 22:34:28 +01:00
if let Some(edition) = self.config.rust.edition {
match edition {
RustEdition::E2015 => {
2023-05-13 09:44:11 -07:00
cmd.args(["--edition", "2015"]);
2019-11-17 21:36:10 +03:00
}
2020-03-09 22:34:28 +01:00
RustEdition::E2018 => {
2023-05-13 09:44:11 -07:00
cmd.args(["--edition", "2018"]);
2020-02-29 17:55:45 +01:00
}
2021-07-04 14:44:23 -07:00
RustEdition::E2021 => {
2023-05-13 09:44:11 -07:00
cmd.args(["--edition", "2021"]);
2021-07-04 14:44:23 -07:00
}
2024-06-12 15:53:56 -07:00
RustEdition::E2024 => {
cmd.args(["--edition", "2024"]);
2024-06-12 15:53:56 -07:00
}
_ => panic!("RustEdition {edition:?} not covered"),
2016-04-26 23:04:27 +02:00
}
}
2020-03-09 22:34:28 +01:00
if color_output {
2024-05-13 12:13:50 -07:00
cmd.args(["--color", "always"]);
}
2022-07-04 23:16:31 +08:00
debug!("running {:?}", cmd);
let output = cmd
.output()
.with_context(|| "failed to execute `rustdoc`")?;
2020-03-09 22:34:28 +01:00
if !output.status.success() {
failed = true;
eprintln!(
"ERROR rustdoc returned an error:\n\
\n--- stdout\n{}\n--- stderr\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
2020-03-09 22:34:28 +01:00
}
2016-04-26 23:04:27 +02:00
}
}
if failed {
2020-09-06 09:11:53 -07:00
bail!("One or more tests failed");
}
if let Some(chapter) = chapter {
if !chapter_found {
bail!("Chapter not found: {}", chapter);
}
}
2016-04-26 23:04:27 +02:00
Ok(())
}
/// The logic for determining where a backend should put its build
/// artefacts.
///
/// If there is only 1 renderer, put it in the directory pointed to by the
/// `build.build_dir` key in [`Config`]. If there is more than one then the
/// renderer gets its own directory within the main build dir.
///
/// i.e. If there were only one renderer (in this case, the HTML renderer):
///
/// - build/
/// - index.html
/// - ...
///
/// Otherwise if there are multiple:
///
/// - build/
/// - epub/
/// - my_awesome_book.epub
/// - html/
/// - index.html
/// - ...
/// - latex/
/// - my_awesome_book.tex
///
pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
let build_dir = self.root.join(&self.config.build.build_dir);
if self.renderers.len() <= 1 {
build_dir
} else {
build_dir.join(backend_name)
}
}
/// Get the directory containing this book's source files.
pub fn source_dir(&self) -> PathBuf {
2017-09-30 21:13:00 +08:00
self.root.join(&self.config.book.src)
}
/// Get the directory containing the theme resources for the book.
pub fn theme_dir(&self) -> PathBuf {
self.config
.html_config()
.unwrap_or_default()
.theme_dir(&self.root)
2016-04-26 23:04:27 +02:00
}
}
/// An `output` table.
#[derive(Deserialize)]
struct OutputConfig {
command: Option<String>,
}
/// Look at the `Config` and try to figure out what renderers to use.
fn determine_renderers(config: &Config) -> Result<IndexMap<String, Box<dyn Renderer>>> {
let mut renderers = IndexMap::new();
let outputs = config.outputs::<OutputConfig>()?;
renderers.extend(outputs.into_iter().map(|(key, table)| {
let renderer = if key == "html" {
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
} else if key == "markdown" {
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
} else {
let command = table.command.unwrap_or_else(|| format!("mdbook-{key}"));
Box::new(CmdRenderer::new(key.clone(), command))
};
(key, renderer)
}));
// if we couldn't find anything, add the HTML renderer as a default
if renderers.is_empty() {
renderers.insert("html".to_string(), Box::new(HtmlHandlebars::new()));
}
Ok(renderers)
}
const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
let name = pre.name();
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
}
/// A `preprocessor` table.
#[derive(Deserialize)]
struct PreprocessorConfig {
command: Option<String>,
#[serde(default)]
before: Vec<String>,
#[serde(default)]
after: Vec<String>,
#[serde(default)]
optional: bool,
}
2018-01-07 16:40:17 +00:00
/// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(
config: &Config,
root: &Path,
) -> Result<IndexMap<String, Box<dyn Preprocessor>>> {
// Collect the names of all preprocessors intended to be run, and the order
// in which they should be run.
let mut preprocessor_names = TopologicalSort::<String>::new();
if config.build.use_default_preprocessors {
for name in DEFAULT_PREPROCESSORS {
preprocessor_names.insert(name.to_string());
}
}
let preprocessor_table = config.preprocessors::<PreprocessorConfig>()?;
for (name, table) in preprocessor_table.iter() {
preprocessor_names.insert(name.to_string());
let exists = |name| {
(config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
|| preprocessor_table.contains_key(name)
};
for after in &table.before {
if !exists(&after) {
// Only warn so that preprocessors can be toggled on and off (e.g. for
// troubleshooting) without having to worry about order too much.
warn!(
"preprocessor.{}.after contains \"{}\", which was not found",
name, after
);
} else {
preprocessor_names.add_dependency(name, after);
}
}
for before in &table.after {
if !exists(&before) {
// See equivalent warning above for rationale
warn!(
"preprocessor.{}.before contains \"{}\", which was not found",
name, before
);
} else {
preprocessor_names.add_dependency(before, name);
}
2018-01-07 16:40:17 +00:00
}
}
// Now that all links have been established, queue preprocessors in a suitable order
let mut preprocessors = IndexMap::with_capacity(preprocessor_names.len());
// `pop_all()` returns an empty vector when no more items are not being depended upon
for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
.take_while(|names| !names.is_empty())
{
// The `topological_sort` crate does not guarantee a stable order for ties, even across
// runs of the same program. Thus, we break ties manually by sorting.
// Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
// values ([1]), which may not be an alphabetical sort.
// As mentioned in [1], doing so depends on locale, which is not desirable for deciding
// preprocessor execution order.
// [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
names.sort();
for name in names {
let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
"links" => Box::new(LinkPreprocessor::new()),
"index" => Box::new(IndexPreprocessor::new()),
_ => {
// The only way to request a custom preprocessor is through the `preprocessor`
// table, so it must exist, be a table, and contain the key.
let table = &preprocessor_table[&name];
let command = table
.command
.to_owned()
.unwrap_or_else(|| format!("mdbook-{name}"));
Box::new(CmdPreprocessor::new(
name.clone(),
command,
root.to_owned(),
table.optional,
))
}
};
preprocessors.insert(name, preprocessor);
}
}
// "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
// Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
if preprocessor_names.is_empty() {
Ok(preprocessors)
} else {
Err(Error::msg("Cyclic dependency detected in preprocessors"))
}
2018-01-07 16:21:46 +00:00
}
/// Check whether we should run a particular `Preprocessor` in combination
/// with the renderer, falling back to `Preprocessor::supports_renderer()`
/// method if the user doesn't say anything.
///
/// The `build.use-default-preprocessors` config option can be used to ensure
/// default preprocessors always run if they support the renderer.
fn preprocessor_should_run(
preprocessor: &dyn Preprocessor,
renderer: &dyn Renderer,
cfg: &Config,
) -> Result<bool> {
// default preprocessors should be run by default (if supported)
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
return preprocessor.supports_renderer(renderer.name());
}
let key = format!("preprocessor.{}.renderers", preprocessor.name());
let renderer_name = renderer.name();
match cfg.get::<Vec<String>>(&key) {
Ok(Some(explicit_renderers)) => {
Ok(explicit_renderers.iter().any(|name| name == renderer_name))
}
Ok(None) => preprocessor.supports_renderer(renderer_name),
Err(e) => bail!("failed to get `{key}`: {e}"),
}
}