]> git.proxmox.com Git - rustc.git/blob - vendor/mdbook/src/book/mod.rs
New upstream version 1.48.0~beta.8+dfsg1
[rustc.git] / vendor / mdbook / src / book / mod.rs
1 //! The internal representation of a book and infrastructure for loading it from
2 //! disk and building it.
3 //!
4 //! For examples on using `MDBook`, consult the [top-level documentation][1].
5 //!
6 //! [1]: ../index.html
7
8 #[allow(clippy::module_inception)]
9 mod book;
10 mod init;
11 mod summary;
12
13 pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
14 pub use self::init::BookBuilder;
15 pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
16
17 use std::io::Write;
18 use std::path::PathBuf;
19 use std::process::Command;
20 use std::string::ToString;
21 use tempfile::Builder as TempFileBuilder;
22 use toml::Value;
23
24 use crate::errors::*;
25 use crate::preprocess::{
26 CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
27 };
28 use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
29 use crate::utils;
30
31 use crate::config::{Config, RustEdition};
32
33 /// The object used to manage and build a book.
34 pub struct MDBook {
35 /// The book's root directory.
36 pub root: PathBuf,
37 /// The configuration used to tweak now a book is built.
38 pub config: Config,
39 /// A representation of the book's contents in memory.
40 pub book: Book,
41 renderers: Vec<Box<dyn Renderer>>,
42
43 /// List of pre-processors to be run on the book
44 preprocessors: Vec<Box<dyn Preprocessor>>,
45 }
46
47 impl MDBook {
48 /// Load a book from its root directory on disk.
49 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 // the book.json file is no longer used, so we should emit a warning to
54 // let people know to migrate to book.toml
55 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 /// Load a book from its root directory using a custom config.
82 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 /// Load a book from its root directory using a custom config and a custom summary.
101 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 /// Returns a flat depth-first iterator over the elements of the book,
124 /// it returns an [BookItem enum](bookitem.html):
125 /// `(section: String, bookitem: &BookItem)`
126 ///
127 /// ```no_run
128 /// # use mdbook::MDBook;
129 /// # use mdbook::book::BookItem;
130 /// # let book = MDBook::load("mybook").unwrap();
131 /// for item in book.iter() {
132 /// match *item {
133 /// BookItem::Chapter(ref chapter) => {},
134 /// BookItem::Separator => {},
135 /// BookItem::PartTitle(ref title) => {}
136 /// }
137 /// }
138 ///
139 /// // would print something like this:
140 /// // 1. Chapter 1
141 /// // 1.1 Sub Chapter
142 /// // 1.2 Sub Chapter
143 /// // 2. Chapter 2
144 /// //
145 /// // etc.
146 /// ```
147 pub fn iter(&self) -> BookItems<'_> {
148 self.book.iter()
149 }
150
151 /// `init()` gives you a `BookBuilder` which you can use to setup a new book
152 /// and its accompanying directory structure.
153 ///
154 /// The `BookBuilder` creates some boilerplate files and directories to get
155 /// you started with your book.
156 ///
157 /// ```text
158 /// book-test/
159 /// ├── book
160 /// └── src
161 /// ├── chapter_1.md
162 /// └── SUMMARY.md
163 /// ```
164 ///
165 /// It uses the path provided as the root directory for your book, then adds
166 /// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
167 /// to get you started.
168 pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
169 BookBuilder::new(book_root)
170 }
171
172 /// Tells the renderer to build our book and put it in the build directory.
173 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 /// Run the entire build process for a particular `Renderer`.
184 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 /// You can change the default renderer to another one by using this method.
222 /// The only requirement is for your renderer to implement the [`Renderer`
223 /// trait](../renderer/trait.Renderer.html)
224 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 /// Register a [`Preprocessor`](../preprocess/trait.Preprocessor.html) to be used when rendering the book.
230 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 /// Run `rustdoc` tests on the book, linking against the provided libraries.
236 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 // FIXME: Is "test" the proper renderer name to use here?
246 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 // Index Preprocessor is disabled so that chapter paths continue to point to the
251 // actual markdown files.
252
253 let mut failed = false;
254 for item in book.iter() {
255 if let BookItem::Chapter(ref ch) = *item {
256 let chapter_path = match ch.path {
257 Some(ref path) if !path.as_os_str().is_empty() => path,
258 _ => continue,
259 };
260
261 let path = self.source_dir().join(&chapter_path);
262 info!("Testing file: {:?}", path);
263
264 // write preprocessed file to tempdir
265 let path = temp_dir.path().join(&chapter_path);
266 let mut tmpf = utils::fs::create_file(&path)?;
267 tmpf.write_all(ch.content.as_bytes())?;
268
269 let mut cmd = Command::new("rustdoc");
270 cmd.arg(&path).arg("--test").args(&library_args);
271
272 if let Some(edition) = self.config.rust.edition {
273 match edition {
274 RustEdition::E2015 => {
275 cmd.args(&["--edition", "2015"]);
276 }
277 RustEdition::E2018 => {
278 cmd.args(&["--edition", "2018"]);
279 }
280 }
281 }
282
283 let output = cmd.output()?;
284
285 if !output.status.success() {
286 failed = true;
287 error!(
288 "rustdoc returned an error:\n\
289 \n--- stdout\n{}\n--- stderr\n{}",
290 String::from_utf8_lossy(&output.stdout),
291 String::from_utf8_lossy(&output.stderr)
292 );
293 }
294 }
295 }
296 if failed {
297 bail!("One or more tests failed");
298 }
299 Ok(())
300 }
301
302 /// The logic for determining where a backend should put its build
303 /// artefacts.
304 ///
305 /// If there is only 1 renderer, put it in the directory pointed to by the
306 /// `build.build_dir` key in `Config`. If there is more than one then the
307 /// renderer gets its own directory within the main build dir.
308 ///
309 /// i.e. If there were only one renderer (in this case, the HTML renderer):
310 ///
311 /// - build/
312 /// - index.html
313 /// - ...
314 ///
315 /// Otherwise if there are multiple:
316 ///
317 /// - build/
318 /// - epub/
319 /// - my_awesome_book.epub
320 /// - html/
321 /// - index.html
322 /// - ...
323 /// - latex/
324 /// - my_awesome_book.tex
325 ///
326 pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
327 let build_dir = self.root.join(&self.config.build.build_dir);
328
329 if self.renderers.len() <= 1 {
330 build_dir
331 } else {
332 build_dir.join(backend_name)
333 }
334 }
335
336 /// Get the directory containing this book's source files.
337 pub fn source_dir(&self) -> PathBuf {
338 self.root.join(&self.config.book.src)
339 }
340
341 /// Get the directory containing the theme resources for the book.
342 pub fn theme_dir(&self) -> PathBuf {
343 self.config
344 .html_config()
345 .unwrap_or_default()
346 .theme_dir(&self.root)
347 }
348 }
349
350 /// Look at the `Config` and try to figure out what renderers to use.
351 fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
352 let mut renderers = Vec::new();
353
354 if let Some(output_table) = config.get("output").and_then(Value::as_table) {
355 renderers.extend(output_table.iter().map(|(key, table)| {
356 if key == "html" {
357 Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
358 } else if key == "markdown" {
359 Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
360 } else {
361 interpret_custom_renderer(key, table)
362 }
363 }));
364 }
365
366 // if we couldn't find anything, add the HTML renderer as a default
367 if renderers.is_empty() {
368 renderers.push(Box::new(HtmlHandlebars::new()));
369 }
370
371 renderers
372 }
373
374 fn default_preprocessors() -> Vec<Box<dyn Preprocessor>> {
375 vec![
376 Box::new(LinkPreprocessor::new()),
377 Box::new(IndexPreprocessor::new()),
378 ]
379 }
380
381 fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
382 let name = pre.name();
383 name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
384 }
385
386 /// Look at the `MDBook` and try to figure out what preprocessors to run.
387 fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
388 let mut preprocessors = Vec::new();
389
390 if config.build.use_default_preprocessors {
391 preprocessors.extend(default_preprocessors());
392 }
393
394 if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
395 for key in preprocessor_table.keys() {
396 match key.as_ref() {
397 "links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
398 "index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
399 name => preprocessors.push(interpret_custom_preprocessor(
400 name,
401 &preprocessor_table[name],
402 )),
403 }
404 }
405 }
406
407 Ok(preprocessors)
408 }
409
410 fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocessor> {
411 let command = table
412 .get("command")
413 .and_then(Value::as_str)
414 .map(ToString::to_string)
415 .unwrap_or_else(|| format!("mdbook-{}", key));
416
417 Box::new(CmdPreprocessor::new(key.to_string(), command))
418 }
419
420 fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
421 // look for the `command` field, falling back to using the key
422 // prepended by "mdbook-"
423 let table_dot_command = table
424 .get("command")
425 .and_then(Value::as_str)
426 .map(ToString::to_string);
427
428 let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
429
430 Box::new(CmdRenderer::new(key.to_string(), command))
431 }
432
433 /// Check whether we should run a particular `Preprocessor` in combination
434 /// with the renderer, falling back to `Preprocessor::supports_renderer()`
435 /// method if the user doesn't say anything.
436 ///
437 /// The `build.use-default-preprocessors` config option can be used to ensure
438 /// default preprocessors always run if they support the renderer.
439 fn preprocessor_should_run(
440 preprocessor: &dyn Preprocessor,
441 renderer: &dyn Renderer,
442 cfg: &Config,
443 ) -> bool {
444 // default preprocessors should be run by default (if supported)
445 if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
446 return preprocessor.supports_renderer(renderer.name());
447 }
448
449 let key = format!("preprocessor.{}.renderers", preprocessor.name());
450 let renderer_name = renderer.name();
451
452 if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
453 return explicit_renderers
454 .iter()
455 .filter_map(Value::as_str)
456 .any(|name| name == renderer_name);
457 }
458
459 preprocessor.supports_renderer(renderer_name)
460 }
461
462 #[cfg(test)]
463 mod tests {
464 use super::*;
465 use std::str::FromStr;
466 use toml::value::{Table, Value};
467
468 #[test]
469 fn config_defaults_to_html_renderer_if_empty() {
470 let cfg = Config::default();
471
472 // make sure we haven't got anything in the `output` table
473 assert!(cfg.get("output").is_none());
474
475 let got = determine_renderers(&cfg);
476
477 assert_eq!(got.len(), 1);
478 assert_eq!(got[0].name(), "html");
479 }
480
481 #[test]
482 fn add_a_random_renderer_to_the_config() {
483 let mut cfg = Config::default();
484 cfg.set("output.random", Table::new()).unwrap();
485
486 let got = determine_renderers(&cfg);
487
488 assert_eq!(got.len(), 1);
489 assert_eq!(got[0].name(), "random");
490 }
491
492 #[test]
493 fn add_a_random_renderer_with_custom_command_to_the_config() {
494 let mut cfg = Config::default();
495
496 let mut table = Table::new();
497 table.insert("command".to_string(), Value::String("false".to_string()));
498 cfg.set("output.random", table).unwrap();
499
500 let got = determine_renderers(&cfg);
501
502 assert_eq!(got.len(), 1);
503 assert_eq!(got[0].name(), "random");
504 }
505
506 #[test]
507 fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
508 let cfg = Config::default();
509
510 // make sure we haven't got anything in the `preprocessor` table
511 assert!(cfg.get("preprocessor").is_none());
512
513 let got = determine_preprocessors(&cfg);
514
515 assert!(got.is_ok());
516 assert_eq!(got.as_ref().unwrap().len(), 2);
517 assert_eq!(got.as_ref().unwrap()[0].name(), "links");
518 assert_eq!(got.as_ref().unwrap()[1].name(), "index");
519 }
520
521 #[test]
522 fn use_default_preprocessors_works() {
523 let mut cfg = Config::default();
524 cfg.build.use_default_preprocessors = false;
525
526 let got = determine_preprocessors(&cfg).unwrap();
527
528 assert_eq!(got.len(), 0);
529 }
530
531 #[test]
532 fn can_determine_third_party_preprocessors() {
533 let cfg_str = r#"
534 [book]
535 title = "Some Book"
536
537 [preprocessor.random]
538
539 [build]
540 build-dir = "outputs"
541 create-missing = false
542 "#;
543
544 let cfg = Config::from_str(cfg_str).unwrap();
545
546 // make sure the `preprocessor.random` table exists
547 assert!(cfg.get_preprocessor("random").is_some());
548
549 let got = determine_preprocessors(&cfg).unwrap();
550
551 assert!(got.into_iter().any(|p| p.name() == "random"));
552 }
553
554 #[test]
555 fn preprocessors_can_provide_their_own_commands() {
556 let cfg_str = r#"
557 [preprocessor.random]
558 command = "python random.py"
559 "#;
560
561 let cfg = Config::from_str(cfg_str).unwrap();
562
563 // make sure the `preprocessor.random` table exists
564 let random = cfg.get_preprocessor("random").unwrap();
565 let random = interpret_custom_preprocessor("random", &Value::Table(random.clone()));
566
567 assert_eq!(random.cmd(), "python random.py");
568 }
569
570 #[test]
571 fn config_respects_preprocessor_selection() {
572 let cfg_str = r#"
573 [preprocessor.links]
574 renderers = ["html"]
575 "#;
576
577 let cfg = Config::from_str(cfg_str).unwrap();
578
579 // double-check that we can access preprocessor.links.renderers[0]
580 let html = cfg
581 .get_preprocessor("links")
582 .and_then(|links| links.get("renderers"))
583 .and_then(Value::as_array)
584 .and_then(|renderers| renderers.get(0))
585 .and_then(Value::as_str)
586 .unwrap();
587 assert_eq!(html, "html");
588 let html_renderer = HtmlHandlebars::default();
589 let pre = LinkPreprocessor::new();
590
591 let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
592 assert!(should_run);
593 }
594
595 struct BoolPreprocessor(bool);
596 impl Preprocessor for BoolPreprocessor {
597 fn name(&self) -> &str {
598 "bool-preprocessor"
599 }
600
601 fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
602 unimplemented!()
603 }
604
605 fn supports_renderer(&self, _renderer: &str) -> bool {
606 self.0
607 }
608 }
609
610 #[test]
611 fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
612 let cfg = Config::default();
613 let html = HtmlHandlebars::new();
614
615 let should_be = true;
616 let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
617 assert_eq!(got, should_be);
618
619 let should_be = false;
620 let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
621 assert_eq!(got, should_be);
622 }
623 }