1use 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
28pub struct MDBook {
30 pub root: PathBuf,
32
33 pub config: Config,
35
36 pub book: Book,
38
39 renderers: IndexMap<String, Box<dyn Renderer>>,
41
42 preprocessors: IndexMap<String, Box<dyn Preprocessor>>,
44}
45
46impl MDBook {
47 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 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 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 pub fn iter(&self) -> BookItems<'_> {
136 self.book.iter()
137 }
138
139 pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
157 BookBuilder::new(book_root)
158 }
159
160 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 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 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 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 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 pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
229 self.test_chapter(library_paths, None)
231 }
232
233 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 fn name(&self) -> &str {
258 "test"
259 }
260
261 fn render(&self, _: &RenderContext) -> Result<()> {
262 Ok(())
263 }
264 }
265
266 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 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 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 pub fn source_dir(&self) -> PathBuf {
388 self.root.join(&self.config.book.src)
389 }
390
391 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#[derive(Deserialize)]
402struct OutputConfig {
403 command: Option<String>,
404}
405
406fn 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 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#[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
450fn determine_preprocessors(
452 config: &Config,
453 root: &Path,
454) -> Result<IndexMap<String, Box<dyn Preprocessor>>> {
455 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 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 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 let mut preprocessors = IndexMap::with_capacity(preprocessor_names.len());
503 for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
505 .take_while(|names| !names.is_empty())
506 {
507 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 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 preprocessor_names.is_empty() {
542 Ok(preprocessors)
543 } else {
544 Err(Error::msg("Cyclic dependency detected in preprocessors"))
545 }
546}
547
548fn preprocessor_should_run(
555 preprocessor: &dyn Preprocessor,
556 renderer: &dyn Renderer,
557 cfg: &Config,
558) -> Result<bool> {
559 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}