gitbook/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)]
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
33/// The object used to manage and build a book.
34pub 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
47impl 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        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                // write preprocessed file to tempdir
264                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    /// The logic for determining where a backend should put its build
298    /// artefacts.
299    ///
300    /// If there is only 1 renderer, put it in the directory pointed to by the
301    /// `build.build_dir` key in `Config`. If there is more than one then the
302    /// renderer gets its own directory within the main build dir.
303    ///
304    /// i.e. If there were only one renderer (in this case, the HTML renderer):
305    ///
306    /// - build/
307    ///   - index.html
308    ///   - ...
309    ///
310    /// Otherwise if there are multiple:
311    ///
312    /// - build/
313    ///   - epub/
314    ///     - my_awesome_book.epub
315    ///   - html/
316    ///     - index.html
317    ///     - ...
318    ///   - latex/
319    ///     - my_awesome_book.tex
320    ///
321    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    /// Get the directory containing this book's source files.
332    pub fn source_dir(&self) -> PathBuf {
333        self.root.join(&self.config.book.src)
334    }
335
336    /// Get the directory containing the theme resources for the book.
337    pub fn theme_dir(&self) -> PathBuf {
338        self.config
339            .html_config()
340            .unwrap_or_default()
341            .theme_dir(&self.root)
342    }
343}
344
345/// Look at the `Config` and try to figure out what renderers to use.
346fn 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 we couldn't find anything, add the HTML renderer as a default
362    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
381/// Look at the `MDBook` and try to figure out what preprocessors to run.
382fn 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    // look for the `command` field, falling back to using the key
417    // prepended by "mdbook-"
418    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
428/// Check whether we should run a particular `Preprocessor` in combination
429/// with the renderer, falling back to `Preprocessor::supports_renderer()`
430/// method if the user doesn't say anything.
431///
432/// The `build.use-default-preprocessors` config option can be used to ensure
433/// default preprocessors always run if they support the renderer.
434fn preprocessor_should_run(
435    preprocessor: &dyn Preprocessor,
436    renderer: &dyn Renderer,
437    cfg: &Config,
438) -> bool {
439    // default preprocessors should be run by default (if supported)
440    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        // make sure we haven't got anything in the `output` table
468        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        // make sure we haven't got anything in the `preprocessor` table
506        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        // make sure the `preprocessor.random` table exists
542        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        // make sure the `preprocessor.random` table exists
559        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        // double-check that we can access preprocessor.links.renderers[0]
575        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}