1 //! The internal representation of a book and infrastructure for loading it from
2 //! disk and building it.
4 //! For examples on using `MDBook`, consult the [top-level documentation][1].
8 #[allow(clippy::module_inception)]
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}
;
18 use std
::path
::PathBuf
;
19 use std
::process
::Command
;
20 use std
::string
::ToString
;
21 use tempfile
::Builder
as TempFileBuilder
;
25 use crate::preprocess
::{
26 CmdPreprocessor
, IndexPreprocessor
, LinkPreprocessor
, Preprocessor
, PreprocessorContext
,
28 use crate::renderer
::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer}
;
31 use crate::config
::{Config, RustEdition}
;
33 /// The object used to manage and build a book.
35 /// The book's root directory.
37 /// The configuration used to tweak now a book is built.
39 /// A representation of the book's contents in memory.
41 renderers
: Vec
<Box
<dyn Renderer
>>,
43 /// List of pre-processors to be run on the book
44 preprocessors
: Vec
<Box
<dyn Preprocessor
>>,
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");
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");
63 let mut config
= if config_location
.exists() {
64 debug
!("Loading config from {}", config_location
.display());
65 Config
::from_disk(&config_location
)?
70 config
.update_from_env();
72 if log_enabled
!(log
::Level
::Trace
) {
73 for line
in format
!("Config: {:#?}", config
).lines() {
78 MDBook
::load_with_config(book_root
, config
)
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();
85 let src_dir
= root
.join(&config
.book
.src
);
86 let book
= book
::load_book(&src_dir
, &config
.build
)?
;
88 let renderers
= determine_renderers(&config
);
89 let preprocessors
= determine_preprocessors(&config
)?
;
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
>>(
105 ) -> Result
<MDBook
> {
106 let root
= book_root
.into();
108 let src_dir
= root
.join(&config
.book
.src
);
109 let book
= book
::load_book_from_disk(&summary
, &src_dir
)?
;
111 let renderers
= determine_renderers(&config
);
112 let preprocessors
= determine_preprocessors(&config
)?
;
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)`
128 /// # use mdbook::MDBook;
129 /// # use mdbook::book::BookItem;
130 /// # let book = MDBook::load("mybook").unwrap();
131 /// for item in book.iter() {
133 /// BookItem::Chapter(ref chapter) => {},
134 /// BookItem::Separator => {},
135 /// BookItem::PartTitle(ref title) => {}
139 /// // would print something like this:
141 /// // 1.1 Sub Chapter
142 /// // 1.2 Sub Chapter
147 pub fn iter(&self) -> BookItems
<'_
> {
151 /// `init()` gives you a `BookBuilder` which you can use to setup a new book
152 /// and its accompanying directory structure.
154 /// The `BookBuilder` creates some boilerplate files and directories to get
155 /// you started with your book.
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
)
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");
176 for renderer
in &self.renderers
{
177 self.execute_build_process(&**renderer
)?
;
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(
189 renderer
.name().to_string(),
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
)?
;
199 info
!("Running the {} backend", renderer
.name());
200 self.render(&preprocessed_book
, renderer
)?
;
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
);
209 let render_context
= RenderContext
::new(
211 preprocessed_book
.clone(),
217 .render(&render_context
)
218 .with_context(|| "Rendering failed")
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
));
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
));
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())
239 .zip(library_paths
.into_iter())
240 .flat_map(|x
| vec
![x
.0, x
.1])
243 let temp_dir
= TempFileBuilder
::new().prefix("mdbook-").tempdir()?
;
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());
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.
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
,
261 let path
= self.source_dir().join(&chapter_path
);
262 info
!("Testing file: {:?}", path
);
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())?
;
269 let mut cmd
= Command
::new("rustdoc");
270 cmd
.arg(&path
).arg("--test").args(&library_args
);
272 if let Some(edition
) = self.config
.rust
.edition
{
274 RustEdition
::E2015
=> {
275 cmd
.args(&["--edition", "2015"]);
277 RustEdition
::E2018
=> {
278 cmd
.args(&["--edition", "2018"]);
283 let output
= cmd
.output()?
;
285 if !output
.status
.success() {
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
)
297 bail
!("One or more tests failed");
302 /// The logic for determining where a backend should put its build
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.
309 /// i.e. If there were only one renderer (in this case, the HTML renderer):
315 /// Otherwise if there are multiple:
319 /// - my_awesome_book.epub
324 /// - my_awesome_book.tex
326 pub fn build_dir_for(&self, backend_name
: &str) -> PathBuf
{
327 let build_dir
= self.root
.join(&self.config
.build
.build_dir
);
329 if self.renderers
.len() <= 1 {
332 build_dir
.join(backend_name
)
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
)
341 /// Get the directory containing the theme resources for the book.
342 pub fn theme_dir(&self) -> PathBuf
{
346 .theme_dir(&self.root
)
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();
354 if let Some(output_table
) = config
.get("output").and_then(Value
::as_table
) {
355 renderers
.extend(output_table
.iter().map(|(key
, table
)| {
357 Box
::new(HtmlHandlebars
::new()) as Box
<dyn Renderer
>
358 } else if key
== "markdown" {
359 Box
::new(MarkdownRenderer
::new()) as Box
<dyn Renderer
>
361 interpret_custom_renderer(key
, table
)
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()));
374 fn default_preprocessors() -> Vec
<Box
<dyn Preprocessor
>> {
376 Box
::new(LinkPreprocessor
::new()),
377 Box
::new(IndexPreprocessor
::new()),
381 fn is_default_preprocessor(pre
: &dyn Preprocessor
) -> bool
{
382 let name
= pre
.name();
383 name
== LinkPreprocessor
::NAME
|| name
== IndexPreprocessor
::NAME
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();
390 if config
.build
.use_default_preprocessors
{
391 preprocessors
.extend(default_preprocessors());
394 if let Some(preprocessor_table
) = config
.get("preprocessor").and_then(Value
::as_table
) {
395 for key
in preprocessor_table
.keys() {
397 "links" => preprocessors
.push(Box
::new(LinkPreprocessor
::new())),
398 "index" => preprocessors
.push(Box
::new(IndexPreprocessor
::new())),
399 name
=> preprocessors
.push(interpret_custom_preprocessor(
401 &preprocessor_table
[name
],
410 fn interpret_custom_preprocessor(key
: &str, table
: &Value
) -> Box
<CmdPreprocessor
> {
413 .and_then(Value
::as_str
)
414 .map(ToString
::to_string
)
415 .unwrap_or_else(|| format
!("mdbook-{}", key
));
417 Box
::new(CmdPreprocessor
::new(key
.to_string(), command
))
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
425 .and_then(Value
::as_str
)
426 .map(ToString
::to_string
);
428 let command
= table_dot_command
.unwrap_or_else(|| format
!("mdbook-{}", key
));
430 Box
::new(CmdRenderer
::new(key
.to_string(), command
))
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.
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
,
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());
449 let key
= format
!("preprocessor.{}.renderers", preprocessor
.name());
450 let renderer_name
= renderer
.name();
452 if let Some(Value
::Array(ref explicit_renderers
)) = cfg
.get(&key
) {
453 return explicit_renderers
455 .filter_map(Value
::as_str
)
456 .any(|name
| name
== renderer_name
);
459 preprocessor
.supports_renderer(renderer_name
)
465 use std
::str::FromStr
;
466 use toml
::value
::{Table, Value}
;
469 fn config_defaults_to_html_renderer_if_empty() {
470 let cfg
= Config
::default();
472 // make sure we haven't got anything in the `output` table
473 assert
!(cfg
.get("output").is_none());
475 let got
= determine_renderers(&cfg
);
477 assert_eq
!(got
.len(), 1);
478 assert_eq
!(got
[0].name(), "html");
482 fn add_a_random_renderer_to_the_config() {
483 let mut cfg
= Config
::default();
484 cfg
.set("output.random", Table
::new()).unwrap();
486 let got
= determine_renderers(&cfg
);
488 assert_eq
!(got
.len(), 1);
489 assert_eq
!(got
[0].name(), "random");
493 fn add_a_random_renderer_with_custom_command_to_the_config() {
494 let mut cfg
= Config
::default();
496 let mut table
= Table
::new();
497 table
.insert("command".to_string(), Value
::String("false".to_string()));
498 cfg
.set("output.random", table
).unwrap();
500 let got
= determine_renderers(&cfg
);
502 assert_eq
!(got
.len(), 1);
503 assert_eq
!(got
[0].name(), "random");
507 fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
508 let cfg
= Config
::default();
510 // make sure we haven't got anything in the `preprocessor` table
511 assert
!(cfg
.get("preprocessor").is_none());
513 let got
= determine_preprocessors(&cfg
);
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");
522 fn use_default_preprocessors_works() {
523 let mut cfg
= Config
::default();
524 cfg
.build
.use_default_preprocessors
= false;
526 let got
= determine_preprocessors(&cfg
).unwrap();
528 assert_eq
!(got
.len(), 0);
532 fn can_determine_third_party_preprocessors() {
537 [preprocessor.random]
540 build-dir = "outputs"
541 create-missing = false
544 let cfg
= Config
::from_str(cfg_str
).unwrap();
546 // make sure the `preprocessor.random` table exists
547 assert
!(cfg
.get_preprocessor("random").is_some());
549 let got
= determine_preprocessors(&cfg
).unwrap();
551 assert
!(got
.into_iter().any(|p
| p
.name() == "random"));
555 fn preprocessors_can_provide_their_own_commands() {
557 [preprocessor.random]
558 command = "python random.py"
561 let cfg
= Config
::from_str(cfg_str
).unwrap();
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()));
567 assert_eq
!(random
.cmd(), "python random.py");
571 fn config_respects_preprocessor_selection() {
577 let cfg
= Config
::from_str(cfg_str
).unwrap();
579 // double-check that we can access preprocessor.links.renderers[0]
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
)
587 assert_eq
!(html
, "html");
588 let html_renderer
= HtmlHandlebars
::default();
589 let pre
= LinkPreprocessor
::new();
591 let should_run
= preprocessor_should_run(&pre
, &html_renderer
, &cfg
);
595 struct BoolPreprocessor(bool
);
596 impl Preprocessor
for BoolPreprocessor
{
597 fn name(&self) -> &str {
601 fn run(&self, _ctx
: &PreprocessorContext
, _book
: Book
) -> Result
<Book
> {
605 fn supports_renderer(&self, _renderer
: &str) -> bool
{
611 fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
612 let cfg
= Config
::default();
613 let html
= HtmlHandlebars
::new();
615 let should_be
= true;
616 let got
= preprocessor_should_run(&BoolPreprocessor(should_be
), &html
, &cfg
);
617 assert_eq
!(got
, should_be
);
619 let should_be
= false;
620 let got
= preprocessor_should_run(&BoolPreprocessor(should_be
), &html
, &cfg
);
621 assert_eq
!(got
, should_be
);