mdbook_driver/
lib.rs

1//! High-level library for running mdBook.
2//!
3//! This is the high-level library for running
4//! [mdBook](https://rust-lang.github.io/mdBook/). There are several
5//! reasons for using the programmatic API (over the CLI):
6//!
7//! - Integrate mdBook in a current project.
8//! - Extend the capabilities of mdBook.
9//! - Do some processing or test before building your book.
10//! - Accessing the public API to help create a new Renderer.
11//!
12//! ## Additional crates
13//!
14//! In addition to `mdbook-driver`, there are several other crates available
15//! for using and extending mdBook:
16//!
17//! - [`mdbook_preprocessor`]: Provides support for implementing preprocessors.
18//! - [`mdbook_renderer`]: Provides support for implementing renderers.
19//! - [`mdbook_markdown`]: The Markdown renderer.
20//! - [`mdbook_summary`]: The `SUMMARY.md` parser.
21//! - [`mdbook_html`]: The HTML renderer.
22//! - [`mdbook_core`]: An internal library that is used by the other crates
23//!   for shared types. Types from this crate are rexported from the other
24//!   crates as appropriate.
25//!
26//! ## Cargo features
27//!
28//! The following cargo features are available:
29//!
30//! - `search`: Enables the search index in the HTML renderer.
31//!
32//! ## Examples
33//!
34//! If creating a new book from scratch, you'll want to get a [`init::BookBuilder`] via
35//! the [`MDBook::init()`] method.
36//!
37//! ```rust,no_run
38//! use mdbook_driver::MDBook;
39//! use mdbook_driver::config::Config;
40//!
41//! let root_dir = "/path/to/book/root";
42//!
43//! // create a default config and change a couple things
44//! let mut cfg = Config::default();
45//! cfg.book.title = Some("My Book".to_string());
46//! cfg.book.authors.push("Michael-F-Bryan".to_string());
47//!
48//! MDBook::init(root_dir)
49//!     .create_gitignore(true)
50//!     .with_config(cfg)
51//!     .build()
52//!     .expect("Book generation failed");
53//! ```
54//!
55//! You can also load an existing book and build it.
56//!
57//! ```rust,no_run
58//! use mdbook_driver::MDBook;
59//!
60//! let root_dir = "/path/to/book/root";
61//!
62//! let mut md = MDBook::load(root_dir)
63//!     .expect("Unable to load the book");
64//! md.build().expect("Building failed");
65//! ```
66
67pub mod builtin_preprocessors;
68pub mod builtin_renderers;
69pub mod init;
70mod load;
71mod mdbook;
72
73use anyhow::{Context, Result, bail};
74pub use mdbook::MDBook;
75pub use mdbook_core::{book, config, errors};
76use shlex::Shlex;
77use std::path::{Path, PathBuf};
78use std::process::Command;
79use tracing::{error, warn};
80
81/// Creates a [`Command`] for command renderers and preprocessors.
82fn compose_command(cmd: &str, root: &Path) -> Result<Command> {
83    let mut words = Shlex::new(cmd);
84    let exe = match words.next() {
85        Some(e) => PathBuf::from(e),
86        None => bail!("Command string was empty"),
87    };
88
89    let exe = if exe.components().count() == 1 {
90        // Search PATH for the executable.
91        exe
92    } else {
93        // Relative path is relative to book root.
94        root.join(&exe)
95    };
96
97    let mut cmd = Command::new(exe);
98
99    for arg in words {
100        cmd.arg(arg);
101    }
102
103    Ok(cmd)
104}
105
106/// Handles a failure for a preprocessor or renderer.
107fn handle_command_error(
108    error: std::io::Error,
109    optional: bool,
110    key: &str,
111    what: &str,
112    name: &str,
113    cmd: &str,
114) -> Result<()> {
115    if let std::io::ErrorKind::NotFound = error.kind() {
116        if optional {
117            warn!(
118                "The command `{cmd}` for {what} `{name}` was not found, \
119                 but is marked as optional.",
120            );
121            return Ok(());
122        } else {
123            error!(
124                "The command `{cmd}` wasn't found, is the `{name}` {what} installed? \
125                If you want to ignore this error when the `{name}` {what} is not installed, \
126                set `optional = true` in the `[{key}.{name}]` section of the book.toml configuration file.",
127            );
128        }
129    }
130    Err(error).with_context(|| format!("Unable to run the {what} `{name}`"))?
131}