1#[allow(clippy::module_inception)]
9mod book;
10mod init;
11mod summary;
12
13pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
14pub use self::init::BookBuilder;
15pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
16
17use std::io::Write;
18use std::path::PathBuf;
19use std::process::Command;
20use std::string::ToString;
21use tempfile::Builder as TempFileBuilder;
22use toml::Value;
23
24use crate::errors::*;
25use crate::preprocess::{
26 CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
27};
28use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
29use crate::utils;
30
31use crate::config::{Config, RustEdition};
32
33pub struct MDBook {
35 pub root: PathBuf,
37 pub config: Config,
39 pub book: Book,
41 renderers: Vec<Box<dyn Renderer>>,
42
43 preprocessors: Vec<Box<dyn Preprocessor>>,
45}
46
47impl MDBook {
48 pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
50 let book_root = book_root.into();
51 let config_location = book_root.join("book.toml");
52
53 if book_root.join("book.json").exists() {
56 warn!("It appears you are still using book.json for configuration.");
57 warn!("This format is no longer used, so you should migrate to the");
58 warn!("book.toml format.");
59 warn!("Check the user guide for migration information:");
60 warn!("\thttps://rust-lang.github.io/mdBook/format/config.html");
61 }
62
63 let mut config = if config_location.exists() {
64 debug!("Loading config from {}", config_location.display());
65 Config::from_disk(&config_location)?
66 } else {
67 Config::default()
68 };
69
70 config.update_from_env();
71
72 if log_enabled!(log::Level::Trace) {
73 for line in format!("Config: {:#?}", config).lines() {
74 trace!("{}", line);
75 }
76 }
77
78 MDBook::load_with_config(book_root, config)
79 }
80
81 pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
83 let root = book_root.into();
84
85 let src_dir = root.join(&config.book.src);
86 let book = book::load_book(&src_dir, &config.build)?;
87
88 let renderers = determine_renderers(&config);
89 let preprocessors = determine_preprocessors(&config)?;
90
91 Ok(MDBook {
92 root,
93 config,
94 book,
95 renderers,
96 preprocessors,
97 })
98 }
99
100 pub fn load_with_config_and_summary<P: Into<PathBuf>>(
102 book_root: P,
103 config: Config,
104 summary: Summary,
105 ) -> Result<MDBook> {
106 let root = book_root.into();
107
108 let src_dir = root.join(&config.book.src);
109 let book = book::load_book_from_disk(&summary, &src_dir)?;
110
111 let renderers = determine_renderers(&config);
112 let preprocessors = determine_preprocessors(&config)?;
113
114 Ok(MDBook {
115 root,
116 config,
117 book,
118 renderers,
119 preprocessors,
120 })
121 }
122
123 pub fn iter(&self) -> BookItems<'_> {
148 self.book.iter()
149 }
150
151 pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
169 BookBuilder::new(book_root)
170 }
171
172 pub fn build(&self) -> Result<()> {
174 info!("Book building has started");
175
176 for renderer in &self.renderers {
177 self.execute_build_process(&**renderer)?;
178 }
179
180 Ok(())
181 }
182
183 pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
185 let mut preprocessed_book = self.book.clone();
186 let preprocess_ctx = PreprocessorContext::new(
187 self.root.clone(),
188 self.config.clone(),
189 renderer.name().to_string(),
190 );
191
192 for preprocessor in &self.preprocessors {
193 if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
194 debug!("Running the {} preprocessor.", preprocessor.name());
195 preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
196 }
197 }
198
199 info!("Running the {} backend", renderer.name());
200 self.render(&preprocessed_book, renderer)?;
201
202 Ok(())
203 }
204
205 fn render(&self, preprocessed_book: &Book, renderer: &dyn Renderer) -> Result<()> {
206 let name = renderer.name();
207 let build_dir = self.build_dir_for(name);
208
209 let render_context = RenderContext::new(
210 self.root.clone(),
211 preprocessed_book.clone(),
212 self.config.clone(),
213 build_dir,
214 );
215
216 renderer
217 .render(&render_context)
218 .with_context(|| "Rendering failed")
219 }
220
221 pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
225 self.renderers.push(Box::new(renderer));
226 self
227 }
228
229 pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
231 self.preprocessors.push(Box::new(preprocessor));
232 self
233 }
234
235 pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
237 let library_args: Vec<&str> = (0..library_paths.len())
238 .map(|_| "-L")
239 .zip(library_paths.into_iter())
240 .flat_map(|x| vec![x.0, x.1])
241 .collect();
242
243 let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
244
245 let preprocess_context =
247 PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
248
249 let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
250 for item in book.iter() {
254 if let BookItem::Chapter(ref ch) = *item {
255 let chapter_path = match ch.path {
256 Some(ref path) if !path.as_os_str().is_empty() => path,
257 _ => continue,
258 };
259
260 let path = self.source_dir().join(&chapter_path);
261 info!("Testing file: {:?}", path);
262
263 let path = temp_dir.path().join(&chapter_path);
265 let mut tmpf = utils::fs::create_file(&path)?;
266 tmpf.write_all(ch.content.as_bytes())?;
267
268 let mut cmd = Command::new("rustdoc");
269 cmd.arg(&path).arg("--test").args(&library_args);
270
271 if let Some(edition) = self.config.rust.edition {
272 match edition {
273 RustEdition::E2015 => {
274 cmd.args(&["--edition", "2015"]);
275 }
276 RustEdition::E2018 => {
277 cmd.args(&["--edition", "2018"]);
278 }
279 }
280 }
281
282 let output = cmd.output()?;
283
284 if !output.status.success() {
285 bail!(
286 "rustdoc returned an error:\n\
287 \n--- stdout\n{}\n--- stderr\n{}",
288 String::from_utf8_lossy(&output.stdout),
289 String::from_utf8_lossy(&output.stderr)
290 );
291 }
292 }
293 }
294 Ok(())
295 }
296
297 pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
322 let build_dir = self.root.join(&self.config.build.build_dir);
323
324 if self.renderers.len() <= 1 {
325 build_dir
326 } else {
327 build_dir.join(backend_name)
328 }
329 }
330
331 pub fn source_dir(&self) -> PathBuf {
333 self.root.join(&self.config.book.src)
334 }
335
336 pub fn theme_dir(&self) -> PathBuf {
338 self.config
339 .html_config()
340 .unwrap_or_default()
341 .theme_dir(&self.root)
342 }
343}
344
345fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
347 let mut renderers = Vec::new();
348
349 if let Some(output_table) = config.get("output").and_then(Value::as_table) {
350 renderers.extend(output_table.iter().map(|(key, table)| {
351 if key == "html" {
352 Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
353 } else if key == "markdown" {
354 Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
355 } else {
356 interpret_custom_renderer(key, table)
357 }
358 }));
359 }
360
361 if renderers.is_empty() {
363 renderers.push(Box::new(HtmlHandlebars::new()));
364 }
365
366 renderers
367}
368
369fn default_preprocessors() -> Vec<Box<dyn Preprocessor>> {
370 vec![
371 Box::new(LinkPreprocessor::new()),
372 Box::new(IndexPreprocessor::new()),
373 ]
374}
375
376fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
377 let name = pre.name();
378 name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
379}
380
381fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
383 let mut preprocessors = Vec::new();
384
385 if config.build.use_default_preprocessors {
386 preprocessors.extend(default_preprocessors());
387 }
388
389 if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
390 for key in preprocessor_table.keys() {
391 match key.as_ref() {
392 "links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
393 "index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
394 name => preprocessors.push(interpret_custom_preprocessor(
395 name,
396 &preprocessor_table[name],
397 )),
398 }
399 }
400 }
401
402 Ok(preprocessors)
403}
404
405fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocessor> {
406 let command = table
407 .get("command")
408 .and_then(Value::as_str)
409 .map(ToString::to_string)
410 .unwrap_or_else(|| format!("mdbook-{}", key));
411
412 Box::new(CmdPreprocessor::new(key.to_string(), command))
413}
414
415fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
416 let table_dot_command = table
419 .get("command")
420 .and_then(Value::as_str)
421 .map(ToString::to_string);
422
423 let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
424
425 Box::new(CmdRenderer::new(key.to_string(), command))
426}
427
428fn preprocessor_should_run(
435 preprocessor: &dyn Preprocessor,
436 renderer: &dyn Renderer,
437 cfg: &Config,
438) -> bool {
439 if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
441 return preprocessor.supports_renderer(renderer.name());
442 }
443
444 let key = format!("preprocessor.{}.renderers", preprocessor.name());
445 let renderer_name = renderer.name();
446
447 if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
448 return explicit_renderers
449 .iter()
450 .filter_map(Value::as_str)
451 .any(|name| name == renderer_name);
452 }
453
454 preprocessor.supports_renderer(renderer_name)
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460 use std::str::FromStr;
461 use toml::value::{Table, Value};
462
463 #[test]
464 fn config_defaults_to_html_renderer_if_empty() {
465 let cfg = Config::default();
466
467 assert!(cfg.get("output").is_none());
469
470 let got = determine_renderers(&cfg);
471
472 assert_eq!(got.len(), 1);
473 assert_eq!(got[0].name(), "html");
474 }
475
476 #[test]
477 fn add_a_random_renderer_to_the_config() {
478 let mut cfg = Config::default();
479 cfg.set("output.random", Table::new()).unwrap();
480
481 let got = determine_renderers(&cfg);
482
483 assert_eq!(got.len(), 1);
484 assert_eq!(got[0].name(), "random");
485 }
486
487 #[test]
488 fn add_a_random_renderer_with_custom_command_to_the_config() {
489 let mut cfg = Config::default();
490
491 let mut table = Table::new();
492 table.insert("command".to_string(), Value::String("false".to_string()));
493 cfg.set("output.random", table).unwrap();
494
495 let got = determine_renderers(&cfg);
496
497 assert_eq!(got.len(), 1);
498 assert_eq!(got[0].name(), "random");
499 }
500
501 #[test]
502 fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
503 let cfg = Config::default();
504
505 assert!(cfg.get("preprocessor").is_none());
507
508 let got = determine_preprocessors(&cfg);
509
510 assert!(got.is_ok());
511 assert_eq!(got.as_ref().unwrap().len(), 2);
512 assert_eq!(got.as_ref().unwrap()[0].name(), "links");
513 assert_eq!(got.as_ref().unwrap()[1].name(), "index");
514 }
515
516 #[test]
517 fn use_default_preprocessors_works() {
518 let mut cfg = Config::default();
519 cfg.build.use_default_preprocessors = false;
520
521 let got = determine_preprocessors(&cfg).unwrap();
522
523 assert_eq!(got.len(), 0);
524 }
525
526 #[test]
527 fn can_determine_third_party_preprocessors() {
528 let cfg_str = r#"
529 [book]
530 title = "Some Book"
531
532 [preprocessor.random]
533
534 [build]
535 build-dir = "outputs"
536 create-missing = false
537 "#;
538
539 let cfg = Config::from_str(cfg_str).unwrap();
540
541 assert!(cfg.get_preprocessor("random").is_some());
543
544 let got = determine_preprocessors(&cfg).unwrap();
545
546 assert!(got.into_iter().any(|p| p.name() == "random"));
547 }
548
549 #[test]
550 fn preprocessors_can_provide_their_own_commands() {
551 let cfg_str = r#"
552 [preprocessor.random]
553 command = "python random.py"
554 "#;
555
556 let cfg = Config::from_str(cfg_str).unwrap();
557
558 let random = cfg.get_preprocessor("random").unwrap();
560 let random = interpret_custom_preprocessor("random", &Value::Table(random.clone()));
561
562 assert_eq!(random.cmd(), "python random.py");
563 }
564
565 #[test]
566 fn config_respects_preprocessor_selection() {
567 let cfg_str = r#"
568 [preprocessor.links]
569 renderers = ["html"]
570 "#;
571
572 let cfg = Config::from_str(cfg_str).unwrap();
573
574 let html = cfg
576 .get_preprocessor("links")
577 .and_then(|links| links.get("renderers"))
578 .and_then(Value::as_array)
579 .and_then(|renderers| renderers.get(0))
580 .and_then(Value::as_str)
581 .unwrap();
582 assert_eq!(html, "html");
583 let html_renderer = HtmlHandlebars::default();
584 let pre = LinkPreprocessor::new();
585
586 let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
587 assert!(should_run);
588 }
589
590 struct BoolPreprocessor(bool);
591 impl Preprocessor for BoolPreprocessor {
592 fn name(&self) -> &str {
593 "bool-preprocessor"
594 }
595
596 fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
597 unimplemented!()
598 }
599
600 fn supports_renderer(&self, _renderer: &str) -> bool {
601 self.0
602 }
603 }
604
605 #[test]
606 fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
607 let cfg = Config::default();
608 let html = HtmlHandlebars::new();
609
610 let should_be = true;
611 let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
612 assert_eq!(got, should_be);
613
614 let should_be = false;
615 let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
616 assert_eq!(got, should_be);
617 }
618}