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 log::{debug, error, info, log_enabled, trace, warn};
18use std::io::Write;
19use std::path::PathBuf;
20use std::process::Command;
21use std::string::ToString;
22use tempfile::Builder as TempFileBuilder;
23use toml::Value;
24use topological_sort::TopologicalSort;
25
26use crate::errors::*;
27use crate::preprocess::{
28 CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
29};
30use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
31use crate::utils;
32
33use crate::config::{Config, RustEdition};
34
35pub struct MDBook {
37 pub root: PathBuf,
39 pub config: Config,
41 pub book: Book,
43 renderers: Vec<Box<dyn Renderer>>,
44
45 preprocessors: Vec<Box<dyn Preprocessor>>,
47}
48
49impl MDBook {
50 pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
52 let book_root = book_root.into();
53 let config_location = book_root.join("book.toml");
54
55 if book_root.join("book.json").exists() {
58 warn!("It appears you are still using book.json for configuration.");
59 warn!("This format is no longer used, so you should migrate to the");
60 warn!("book.toml format.");
61 warn!("Check the user guide for migration information:");
62 warn!("\thttps://rust-lang.github.io/mdBook/format/config.html");
63 }
64
65 let mut config = if config_location.exists() {
66 debug!("Loading config from {}", config_location.display());
67 Config::from_disk(&config_location)?
68 } else {
69 Config::default()
70 };
71
72 config.update_from_env();
73
74 if config
75 .html_config()
76 .map_or(false, |html| html.google_analytics.is_some())
77 {
78 warn!(
79 "The output.html.google-analytics field has been deprecated; \
80 it will be removed in a future release.\n\
81 Consider placing the appropriate site tag code into the \
82 theme/head.hbs file instead.\n\
83 The tracking code may be found in the Google Analytics Admin page.\n\
84 "
85 );
86 }
87
88 if log_enabled!(log::Level::Trace) {
89 for line in format!("Config: {:#?}", config).lines() {
90 trace!("{}", line);
91 }
92 }
93
94 MDBook::load_with_config(book_root, config)
95 }
96
97 pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
99 let root = book_root.into();
100
101 let src_dir = root.join(&config.book.src);
102 let book = book::load_book(&src_dir, &config.build)?;
103
104 let renderers = determine_renderers(&config);
105 let preprocessors = determine_preprocessors(&config)?;
106
107 Ok(MDBook {
108 root,
109 config,
110 book,
111 renderers,
112 preprocessors,
113 })
114 }
115
116 pub fn load_with_config_and_summary<P: Into<PathBuf>>(
118 book_root: P,
119 config: Config,
120 summary: Summary,
121 ) -> Result<MDBook> {
122 let root = book_root.into();
123
124 let src_dir = root.join(&config.book.src);
125 let book = book::load_book_from_disk(&summary, &src_dir)?;
126
127 let renderers = determine_renderers(&config);
128 let preprocessors = determine_preprocessors(&config)?;
129
130 Ok(MDBook {
131 root,
132 config,
133 book,
134 renderers,
135 preprocessors,
136 })
137 }
138
139 pub fn iter(&self) -> BookItems<'_> {
164 self.book.iter()
165 }
166
167 pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
185 BookBuilder::new(book_root)
186 }
187
188 pub fn build(&self) -> Result<()> {
190 info!("Book building has started");
191
192 for renderer in &self.renderers {
193 self.execute_build_process(&**renderer)?;
194 }
195
196 Ok(())
197 }
198
199 pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
201 let mut preprocessed_book = self.book.clone();
202 let preprocess_ctx = PreprocessorContext::new(
203 self.root.clone(),
204 self.config.clone(),
205 renderer.name().to_string(),
206 );
207
208 for preprocessor in &self.preprocessors {
209 if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
210 debug!("Running the {} preprocessor.", preprocessor.name());
211 preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
212 }
213 }
214
215 let name = renderer.name();
216 let build_dir = self.build_dir_for(name);
217
218 let mut render_context = RenderContext::new(
219 self.root.clone(),
220 preprocessed_book,
221 self.config.clone(),
222 build_dir,
223 );
224 render_context
225 .chapter_titles
226 .extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
227
228 info!("Running the {} backend", renderer.name());
229 renderer
230 .render(&render_context)
231 .with_context(|| "Rendering failed")
232 }
233
234 pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
238 self.renderers.push(Box::new(renderer));
239 self
240 }
241
242 pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
244 self.preprocessors.push(Box::new(preprocessor));
245 self
246 }
247
248 pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
250 self.test_chapter(library_paths, None)
252 }
253
254 pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
257 let library_args: Vec<&str> = (0..library_paths.len())
258 .map(|_| "-L")
259 .zip(library_paths.into_iter())
260 .flat_map(|x| vec![x.0, x.1])
261 .collect();
262
263 let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
264
265 let mut chapter_found = false;
266
267 let preprocess_context =
269 PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
270
271 let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
272 let mut failed = false;
276 for item in book.iter() {
277 if let BookItem::Chapter(ref ch) = *item {
278 let chapter_path = match ch.path {
279 Some(ref path) if !path.as_os_str().is_empty() => path,
280 _ => continue,
281 };
282
283 if let Some(chapter) = chapter {
284 if ch.name != chapter && chapter_path.to_str() != Some(chapter) {
285 if chapter == "?" {
286 info!("Skipping chapter '{}'...", ch.name);
287 }
288 continue;
289 }
290 }
291 chapter_found = true;
292 info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
293
294 let path = temp_dir.path().join(&chapter_path);
296 let mut tmpf = utils::fs::create_file(&path)?;
297 tmpf.write_all(ch.content.as_bytes())?;
298
299 let mut cmd = Command::new("rustdoc");
300 cmd.arg(&path).arg("--test").args(&library_args);
301
302 if let Some(edition) = self.config.rust.edition {
303 match edition {
304 RustEdition::E2015 => {
305 cmd.args(&["--edition", "2015"]);
306 }
307 RustEdition::E2018 => {
308 cmd.args(&["--edition", "2018"]);
309 }
310 RustEdition::E2021 => {
311 cmd.args(&["--edition", "2021"]);
312 }
313 }
314 }
315
316 debug!("running {:?}", cmd);
317 let output = cmd.output()?;
318
319 if !output.status.success() {
320 failed = true;
321 error!(
322 "rustdoc returned an error:\n\
323 \n--- stdout\n{}\n--- stderr\n{}",
324 String::from_utf8_lossy(&output.stdout),
325 String::from_utf8_lossy(&output.stderr)
326 );
327 }
328 }
329 }
330 if failed {
331 bail!("One or more tests failed");
332 }
333 if let Some(chapter) = chapter {
334 if !chapter_found {
335 bail!("Chapter not found: {}", chapter);
336 }
337 }
338 Ok(())
339 }
340
341 pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
366 let build_dir = self.root.join(&self.config.build.build_dir);
367
368 if self.renderers.len() <= 1 {
369 build_dir
370 } else {
371 build_dir.join(backend_name)
372 }
373 }
374
375 pub fn source_dir(&self) -> PathBuf {
377 self.root.join(&self.config.book.src)
378 }
379
380 pub fn theme_dir(&self) -> PathBuf {
382 self.config
383 .html_config()
384 .unwrap_or_default()
385 .theme_dir(&self.root)
386 }
387}
388
389fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
391 let mut renderers = Vec::new();
392
393 if let Some(output_table) = config.get("output").and_then(Value::as_table) {
394 renderers.extend(output_table.iter().map(|(key, table)| {
395 if key == "html" {
396 Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
397 } else if key == "markdown" {
398 Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
399 } else {
400 interpret_custom_renderer(key, table)
401 }
402 }));
403 }
404
405 if renderers.is_empty() {
407 renderers.push(Box::new(HtmlHandlebars::new()));
408 }
409
410 renderers
411}
412
413const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
414
415fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
416 let name = pre.name();
417 name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
418}
419
420fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
422 let mut preprocessor_names = TopologicalSort::<String>::new();
425
426 if config.build.use_default_preprocessors {
427 for name in DEFAULT_PREPROCESSORS {
428 preprocessor_names.insert(name.to_string());
429 }
430 }
431
432 if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
433 for (name, table) in preprocessor_table.iter() {
434 preprocessor_names.insert(name.to_string());
435
436 let exists = |name| {
437 (config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
438 || preprocessor_table.contains_key(name)
439 };
440
441 if let Some(before) = table.get("before") {
442 let before = before.as_array().ok_or_else(|| {
443 Error::msg(format!(
444 "Expected preprocessor.{}.before to be an array",
445 name
446 ))
447 })?;
448 for after in before {
449 let after = after.as_str().ok_or_else(|| {
450 Error::msg(format!(
451 "Expected preprocessor.{}.before to contain strings",
452 name
453 ))
454 })?;
455
456 if !exists(after) {
457 warn!(
460 "preprocessor.{}.after contains \"{}\", which was not found",
461 name, after
462 );
463 } else {
464 preprocessor_names.add_dependency(name, after);
465 }
466 }
467 }
468
469 if let Some(after) = table.get("after") {
470 let after = after.as_array().ok_or_else(|| {
471 Error::msg(format!(
472 "Expected preprocessor.{}.after to be an array",
473 name
474 ))
475 })?;
476 for before in after {
477 let before = before.as_str().ok_or_else(|| {
478 Error::msg(format!(
479 "Expected preprocessor.{}.after to contain strings",
480 name
481 ))
482 })?;
483
484 if !exists(before) {
485 warn!(
487 "preprocessor.{}.before contains \"{}\", which was not found",
488 name, before
489 );
490 } else {
491 preprocessor_names.add_dependency(before, name);
492 }
493 }
494 }
495 }
496 }
497
498 let mut preprocessors = Vec::with_capacity(preprocessor_names.len());
500 for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
502 .take_while(|names| !names.is_empty())
503 {
504 names.sort();
512 for name in names {
513 let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
514 "links" => Box::new(LinkPreprocessor::new()),
515 "index" => Box::new(IndexPreprocessor::new()),
516 _ => {
517 let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name];
520 let command = get_custom_preprocessor_cmd(&name, table);
521 Box::new(CmdPreprocessor::new(name, command))
522 }
523 };
524 preprocessors.push(preprocessor);
525 }
526 }
527
528 if preprocessor_names.is_empty() {
531 Ok(preprocessors)
532 } else {
533 Err(Error::msg("Cyclic dependency detected in preprocessors"))
534 }
535}
536
537fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
538 table
539 .get("command")
540 .and_then(Value::as_str)
541 .map(ToString::to_string)
542 .unwrap_or_else(|| format!("mdbook-{}", key))
543}
544
545fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
546 let table_dot_command = table
549 .get("command")
550 .and_then(Value::as_str)
551 .map(ToString::to_string);
552
553 let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
554
555 Box::new(CmdRenderer::new(key.to_string(), command))
556}
557
558fn preprocessor_should_run(
565 preprocessor: &dyn Preprocessor,
566 renderer: &dyn Renderer,
567 cfg: &Config,
568) -> bool {
569 if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
571 return preprocessor.supports_renderer(renderer.name());
572 }
573
574 let key = format!("preprocessor.{}.renderers", preprocessor.name());
575 let renderer_name = renderer.name();
576
577 if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
578 return explicit_renderers
579 .iter()
580 .filter_map(Value::as_str)
581 .any(|name| name == renderer_name);
582 }
583
584 preprocessor.supports_renderer(renderer_name)
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590 use std::str::FromStr;
591 use toml::value::{Table, Value};
592
593 #[test]
594 fn config_defaults_to_html_renderer_if_empty() {
595 let cfg = Config::default();
596
597 assert!(cfg.get("output").is_none());
599
600 let got = determine_renderers(&cfg);
601
602 assert_eq!(got.len(), 1);
603 assert_eq!(got[0].name(), "html");
604 }
605
606 #[test]
607 fn add_a_random_renderer_to_the_config() {
608 let mut cfg = Config::default();
609 cfg.set("output.random", Table::new()).unwrap();
610
611 let got = determine_renderers(&cfg);
612
613 assert_eq!(got.len(), 1);
614 assert_eq!(got[0].name(), "random");
615 }
616
617 #[test]
618 fn add_a_random_renderer_with_custom_command_to_the_config() {
619 let mut cfg = Config::default();
620
621 let mut table = Table::new();
622 table.insert("command".to_string(), Value::String("false".to_string()));
623 cfg.set("output.random", table).unwrap();
624
625 let got = determine_renderers(&cfg);
626
627 assert_eq!(got.len(), 1);
628 assert_eq!(got[0].name(), "random");
629 }
630
631 #[test]
632 fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
633 let cfg = Config::default();
634
635 assert!(cfg.get("preprocessor").is_none());
637
638 let got = determine_preprocessors(&cfg);
639
640 assert!(got.is_ok());
641 assert_eq!(got.as_ref().unwrap().len(), 2);
642 assert_eq!(got.as_ref().unwrap()[0].name(), "index");
643 assert_eq!(got.as_ref().unwrap()[1].name(), "links");
644 }
645
646 #[test]
647 fn use_default_preprocessors_works() {
648 let mut cfg = Config::default();
649 cfg.build.use_default_preprocessors = false;
650
651 let got = determine_preprocessors(&cfg).unwrap();
652
653 assert_eq!(got.len(), 0);
654 }
655
656 #[test]
657 fn can_determine_third_party_preprocessors() {
658 let cfg_str = r#"
659 [book]
660 title = "Some Book"
661
662 [preprocessor.random]
663
664 [build]
665 build-dir = "outputs"
666 create-missing = false
667 "#;
668
669 let cfg = Config::from_str(cfg_str).unwrap();
670
671 assert!(cfg.get_preprocessor("random").is_some());
673
674 let got = determine_preprocessors(&cfg).unwrap();
675
676 assert!(got.into_iter().any(|p| p.name() == "random"));
677 }
678
679 #[test]
680 fn preprocessors_can_provide_their_own_commands() {
681 let cfg_str = r#"
682 [preprocessor.random]
683 command = "python random.py"
684 "#;
685
686 let cfg = Config::from_str(cfg_str).unwrap();
687
688 let random = cfg.get_preprocessor("random").unwrap();
690 let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
691
692 assert_eq!(random, "python random.py");
693 }
694
695 #[test]
696 fn preprocessor_before_must_be_array() {
697 let cfg_str = r#"
698 [preprocessor.random]
699 before = 0
700 "#;
701
702 let cfg = Config::from_str(cfg_str).unwrap();
703
704 assert!(determine_preprocessors(&cfg).is_err());
705 }
706
707 #[test]
708 fn preprocessor_after_must_be_array() {
709 let cfg_str = r#"
710 [preprocessor.random]
711 after = 0
712 "#;
713
714 let cfg = Config::from_str(cfg_str).unwrap();
715
716 assert!(determine_preprocessors(&cfg).is_err());
717 }
718
719 #[test]
720 fn preprocessor_order_is_honored() {
721 let cfg_str = r#"
722 [preprocessor.random]
723 before = [ "last" ]
724 after = [ "index" ]
725
726 [preprocessor.last]
727 after = [ "links", "index" ]
728 "#;
729
730 let cfg = Config::from_str(cfg_str).unwrap();
731
732 let preprocessors = determine_preprocessors(&cfg).unwrap();
733 let index = |name| {
734 preprocessors
735 .iter()
736 .enumerate()
737 .find(|(_, preprocessor)| preprocessor.name() == name)
738 .unwrap()
739 .0
740 };
741 let assert_before = |before, after| {
742 if index(before) >= index(after) {
743 eprintln!("Preprocessor order:");
744 for preprocessor in &preprocessors {
745 eprintln!(" {}", preprocessor.name());
746 }
747 panic!("{} should come before {}", before, after);
748 }
749 };
750
751 assert_before("index", "random");
752 assert_before("index", "last");
753 assert_before("random", "last");
754 assert_before("links", "last");
755 }
756
757 #[test]
758 fn cyclic_dependencies_are_detected() {
759 let cfg_str = r#"
760 [preprocessor.links]
761 before = [ "index" ]
762
763 [preprocessor.index]
764 before = [ "links" ]
765 "#;
766
767 let cfg = Config::from_str(cfg_str).unwrap();
768
769 assert!(determine_preprocessors(&cfg).is_err());
770 }
771
772 #[test]
773 fn dependencies_dont_register_undefined_preprocessors() {
774 let cfg_str = r#"
775 [preprocessor.links]
776 before = [ "random" ]
777 "#;
778
779 let cfg = Config::from_str(cfg_str).unwrap();
780
781 let preprocessors = determine_preprocessors(&cfg).unwrap();
782
783 assert!(!preprocessors
784 .iter()
785 .any(|preprocessor| preprocessor.name() == "random"));
786 }
787
788 #[test]
789 fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
790 let cfg_str = r#"
791 [preprocessor.random]
792 before = [ "links" ]
793
794 [build]
795 use-default-preprocessors = false
796 "#;
797
798 let cfg = Config::from_str(cfg_str).unwrap();
799
800 let preprocessors = determine_preprocessors(&cfg).unwrap();
801
802 assert!(!preprocessors
803 .iter()
804 .any(|preprocessor| preprocessor.name() == "links"));
805 }
806
807 #[test]
808 fn config_respects_preprocessor_selection() {
809 let cfg_str = r#"
810 [preprocessor.links]
811 renderers = ["html"]
812 "#;
813
814 let cfg = Config::from_str(cfg_str).unwrap();
815
816 let html = cfg
818 .get_preprocessor("links")
819 .and_then(|links| links.get("renderers"))
820 .and_then(Value::as_array)
821 .and_then(|renderers| renderers.get(0))
822 .and_then(Value::as_str)
823 .unwrap();
824 assert_eq!(html, "html");
825 let html_renderer = HtmlHandlebars::default();
826 let pre = LinkPreprocessor::new();
827
828 let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
829 assert!(should_run);
830 }
831
832 struct BoolPreprocessor(bool);
833 impl Preprocessor for BoolPreprocessor {
834 fn name(&self) -> &str {
835 "bool-preprocessor"
836 }
837
838 fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
839 unimplemented!()
840 }
841
842 fn supports_renderer(&self, _renderer: &str) -> bool {
843 self.0
844 }
845 }
846
847 #[test]
848 fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
849 let cfg = Config::default();
850 let html = HtmlHandlebars::new();
851
852 let should_be = true;
853 let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
854 assert_eq!(got, should_be);
855
856 let should_be = false;
857 let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
858 assert_eq!(got, should_be);
859 }
860}