Merge pull request #2802 from ehuss/with-replace

Change with_renderer/with_preprocessor to overwrite
This commit is contained in:
Eric Huss 2025-08-18 18:25:35 +00:00 committed by GitHub
commit 21f2435182
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 81 additions and 43 deletions

1
Cargo.lock generated
View file

@ -1335,6 +1335,7 @@ name = "mdbook-driver"
version = "0.5.0-alpha.1" version = "0.5.0-alpha.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"indexmap",
"log", "log",
"mdbook-core", "mdbook-core",
"mdbook-html", "mdbook-html",

View file

@ -37,6 +37,7 @@ font-awesome-as-a-crate = "0.3.0"
futures-util = "0.3.31" futures-util = "0.3.31"
handlebars = "6.3.2" handlebars = "6.3.2"
hex = "0.4.3" hex = "0.4.3"
indexmap = "2.10.0"
ignore = "0.4.23" ignore = "0.4.23"
log = "0.4.27" log = "0.4.27"
mdbook-core = { path = "crates/mdbook-core" } mdbook-core = { path = "crates/mdbook-core" }

View file

@ -9,6 +9,7 @@ rust-version.workspace = true
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
indexmap.workspace = true
log.workspace = true log.workspace = true
mdbook-core.workspace = true mdbook-core.workspace = true
mdbook-html.workspace = true mdbook-html.workspace = true

View file

@ -5,6 +5,7 @@ use crate::builtin_renderers::{CmdRenderer, MarkdownRenderer};
use crate::init::BookBuilder; use crate::init::BookBuilder;
use crate::load::{load_book, load_book_from_disk}; use crate::load::{load_book, load_book_from_disk};
use anyhow::{Context, Error, Result, bail}; use anyhow::{Context, Error, Result, bail};
use indexmap::IndexMap;
use log::{debug, error, info, log_enabled, trace, warn}; use log::{debug, error, info, log_enabled, trace, warn};
use mdbook_core::book::{Book, BookItem, BookItems}; use mdbook_core::book::{Book, BookItem, BookItems};
use mdbook_core::config::{Config, RustEdition}; use mdbook_core::config::{Config, RustEdition};
@ -28,14 +29,18 @@ mod tests;
pub struct MDBook { pub struct MDBook {
/// The book's root directory. /// The book's root directory.
pub root: PathBuf, pub root: PathBuf,
/// The configuration used to tweak now a book is built. /// The configuration used to tweak now a book is built.
pub config: Config, pub config: Config,
/// A representation of the book's contents in memory. /// A representation of the book's contents in memory.
pub book: Book, pub book: Book,
renderers: Vec<Box<dyn Renderer>>,
/// List of pre-processors to be run on the book. /// Renderers to execute.
preprocessors: Vec<Box<dyn Preprocessor>>, renderers: IndexMap<String, Box<dyn Renderer>>,
/// Pre-processors to be run on the book.
preprocessors: IndexMap<String, Box<dyn Preprocessor>>,
} }
impl MDBook { impl MDBook {
@ -156,7 +161,7 @@ impl MDBook {
pub fn build(&self) -> Result<()> { pub fn build(&self) -> Result<()> {
info!("Book building has started"); info!("Book building has started");
for renderer in &self.renderers { for renderer in self.renderers.values() {
self.execute_build_process(&**renderer)?; self.execute_build_process(&**renderer)?;
} }
@ -171,7 +176,7 @@ impl MDBook {
renderer.name().to_string(), renderer.name().to_string(),
); );
let mut preprocessed_book = self.book.clone(); let mut preprocessed_book = self.book.clone();
for preprocessor in &self.preprocessors { for preprocessor in self.preprocessors.values() {
if preprocessor_should_run(&**preprocessor, renderer, &self.config)? { if preprocessor_should_run(&**preprocessor, renderer, &self.config)? {
debug!("Running the {} preprocessor.", preprocessor.name()); debug!("Running the {} preprocessor.", preprocessor.name());
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?; preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
@ -207,13 +212,15 @@ impl MDBook {
/// The only requirement is that your renderer implement the [`Renderer`] /// The only requirement is that your renderer implement the [`Renderer`]
/// trait. /// trait.
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self { pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
self.renderers.push(Box::new(renderer)); self.renderers
.insert(renderer.name().to_string(), Box::new(renderer));
self self
} }
/// Register a [`Preprocessor`] to be used when rendering the book. /// Register a [`Preprocessor`] to be used when rendering the book.
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self { pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
self.preprocessors.push(Box::new(preprocessor)); self.preprocessors
.insert(preprocessor.name().to_string(), Box::new(preprocessor));
self self
} }
@ -258,10 +265,9 @@ impl MDBook {
// Index Preprocessor is disabled so that chapter paths // Index Preprocessor is disabled so that chapter paths
// continue to point to the actual markdown files. // continue to point to the actual markdown files.
self.preprocessors = determine_preprocessors(&self.config, &self.root)? self.preprocessors = determine_preprocessors(&self.config, &self.root)?;
.into_iter() self.preprocessors
.filter(|pre| pre.name() != IndexPreprocessor::NAME) .shift_remove_entry(IndexPreprocessor::NAME);
.collect();
let (book, _) = self.preprocess_book(&TestRenderer)?; let (book, _) = self.preprocess_book(&TestRenderer)?;
let color_output = std::io::stderr().is_terminal(); let color_output = std::io::stderr().is_terminal();
@ -399,24 +405,25 @@ struct OutputConfig {
} }
/// Look at the `Config` and try to figure out what renderers to use. /// Look at the `Config` and try to figure out what renderers to use.
fn determine_renderers(config: &Config) -> Result<Vec<Box<dyn Renderer>>> { fn determine_renderers(config: &Config) -> Result<IndexMap<String, Box<dyn Renderer>>> {
let mut renderers = Vec::new(); let mut renderers = IndexMap::new();
let outputs = config.outputs::<OutputConfig>()?; let outputs = config.outputs::<OutputConfig>()?;
renderers.extend(outputs.into_iter().map(|(key, table)| { renderers.extend(outputs.into_iter().map(|(key, table)| {
if key == "html" { let renderer = if key == "html" {
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer> Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
} else if key == "markdown" { } else if key == "markdown" {
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer> Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
} else { } else {
let command = table.command.unwrap_or_else(|| format!("mdbook-{key}")); let command = table.command.unwrap_or_else(|| format!("mdbook-{key}"));
Box::new(CmdRenderer::new(key, command)) Box::new(CmdRenderer::new(key.clone(), command))
} };
(key, renderer)
})); }));
// if we couldn't find anything, add the HTML renderer as a default // if we couldn't find anything, add the HTML renderer as a default
if renderers.is_empty() { if renderers.is_empty() {
renderers.push(Box::new(HtmlHandlebars::new())); renderers.insert("html".to_string(), Box::new(HtmlHandlebars::new()));
} }
Ok(renderers) Ok(renderers)
@ -442,7 +449,10 @@ struct PreprocessorConfig {
} }
/// Look at the `MDBook` and try to figure out what preprocessors to run. /// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(config: &Config, root: &Path) -> Result<Vec<Box<dyn Preprocessor>>> { 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 // Collect the names of all preprocessors intended to be run, and the order
// in which they should be run. // in which they should be run.
let mut preprocessor_names = TopologicalSort::<String>::new(); let mut preprocessor_names = TopologicalSort::<String>::new();
@ -490,7 +500,7 @@ fn determine_preprocessors(config: &Config, root: &Path) -> Result<Vec<Box<dyn P
} }
// Now that all links have been established, queue preprocessors in a suitable order // Now that all links have been established, queue preprocessors in a suitable order
let mut preprocessors = Vec::with_capacity(preprocessor_names.len()); let mut preprocessors = IndexMap::with_capacity(preprocessor_names.len());
// `pop_all()` returns an empty vector when no more items are not being depended upon // `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()) for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
.take_while(|names| !names.is_empty()) .take_while(|names| !names.is_empty())
@ -516,14 +526,14 @@ fn determine_preprocessors(config: &Config, root: &Path) -> Result<Vec<Box<dyn P
.to_owned() .to_owned()
.unwrap_or_else(|| format!("mdbook-{name}")); .unwrap_or_else(|| format!("mdbook-{name}"));
Box::new(CmdPreprocessor::new( Box::new(CmdPreprocessor::new(
name, name.clone(),
command, command,
root.to_owned(), root.to_owned(),
table.optional, table.optional,
)) ))
} }
}; };
preprocessors.push(preprocessor); preprocessors.insert(name, preprocessor);
} }
} }

View file

@ -85,7 +85,7 @@ fn can_determine_third_party_preprocessors() {
let got = determine_preprocessors(&cfg, Path::new("")).unwrap(); let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
assert!(got.into_iter().any(|p| p.name() == "random")); assert!(got.contains_key("random"));
} }
#[test] #[test]
@ -143,19 +143,12 @@ fn preprocessor_order_is_honored() {
let cfg = Config::from_str(cfg_str).unwrap(); let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap(); let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
let index = |name| { let index = |name| preprocessors.get_index_of(name).unwrap();
preprocessors
.iter()
.enumerate()
.find(|(_, preprocessor)| preprocessor.name() == name)
.unwrap()
.0
};
let assert_before = |before, after| { let assert_before = |before, after| {
if index(before) >= index(after) { if index(before) >= index(after) {
eprintln!("Preprocessor order:"); eprintln!("Preprocessor order:");
for preprocessor in &preprocessors { for preprocessor in preprocessors.keys() {
eprintln!(" {}", preprocessor.name()); eprintln!(" {}", preprocessor);
} }
panic!("{before} should come before {after}"); panic!("{before} should come before {after}");
} }
@ -193,11 +186,8 @@ fn dependencies_dont_register_undefined_preprocessors() {
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap(); let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
assert!( // Does not contain "random"
!preprocessors assert_eq!(preprocessors.keys().collect::<Vec<_>>(), ["index", "links"]);
.iter()
.any(|preprocessor| preprocessor.name() == "random")
);
} }
#[test] #[test]
@ -214,11 +204,8 @@ fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap(); let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
assert!( // Does not contain "links"
!preprocessors assert_eq!(preprocessors.keys().collect::<Vec<_>>(), ["random"]);
.iter()
.any(|preprocessor| preprocessor.name() == "links")
);
} }
#[test] #[test]

View file

@ -22,7 +22,8 @@ enum StatusCode {
pub struct BookTest { pub struct BookTest {
/// The temp directory where the test should perform its work. /// The temp directory where the test should perform its work.
pub dir: PathBuf, pub dir: PathBuf,
assert: snapbox::Assert, /// Snapshot assertion support.
pub assert: snapbox::Assert,
/// This indicates whether or not the book has been built. /// This indicates whether or not the book has been built.
built: bool, built: bool,
} }

View file

@ -180,3 +180,22 @@ fn missing_optional_not_fatal() {
"#]]); "#]]);
}); });
} }
// with_preprocessor of an existing name.
#[test]
fn with_preprocessor_same_name() {
let mut test = BookTest::init(|_| {});
test.change_file(
"book.toml",
"[preprocessor.dummy]\n\
command = 'mdbook-preprocessor-does-not-exist'\n",
);
let spy: Arc<Mutex<Inner>> = Default::default();
let mut book = test.load_book();
book.with_preprocessor(Spy(Arc::clone(&spy)));
// Unfortunately this is unable to capture the output when using the API.
book.build().unwrap();
let inner = spy.lock().unwrap();
assert_eq!(inner.run_count, 1);
assert_eq!(inner.rendered_with, ["html"]);
}

View file

@ -241,3 +241,21 @@ fn relative_command_path() {
}) })
.check_file("book/output", "test"); .check_file("book/output", "test");
} }
// with_renderer of an existing name.
#[test]
fn with_renderer_same_name() {
let mut test = BookTest::init(|_| {});
test.change_file(
"book.toml",
"[output.dummy]\n\
command = 'mdbook-renderer-does-not-exist'\n",
);
let spy: Arc<Mutex<Inner>> = Default::default();
let mut book = test.load_book();
book.with_renderer(Spy(Arc::clone(&spy)));
// Unfortunately this is unable to capture the output when using the API.
book.build().unwrap();
let inner = spy.lock().unwrap();
assert_eq!(inner.run_count, 1);
}