66 | ```
67 |
68 | If you would like to make a toc with another structure, you have access to the
69 | chapters property containing all the data. The only limitation at the moment
70 | is that you would have to do it with JavaScript instead of with a handlebars
71 | helper.
72 |
73 | ```html
74 |
78 | ```
79 |
80 | ### 2. previous / next
81 |
82 | The previous and next helpers expose a `link` and `name` property to the
83 | previous and next chapters.
84 |
85 | They are used like this
86 |
87 | ```handlebars
88 | {{#previous}}
89 |
90 |
91 |
92 | {{/previous}}
93 | ```
94 |
95 | The inner html will only be rendered if the previous / next chapter exists.
96 | Of course the inner html can be changed to your liking.
97 |
98 | ------
99 |
100 | *If you would like other properties or helpers exposed, please [create a new
101 | issue](https://github.com/rust-lang/mdBook/issues)*
102 |
--------------------------------------------------------------------------------
/guide/src/format/configuration/general.md:
--------------------------------------------------------------------------------
1 | # General Configuration
2 |
3 | You can configure the parameters for your book in the ***book.toml*** file.
4 |
5 | Here is an example of what a ***book.toml*** file might look like:
6 |
7 | ```toml
8 | [book]
9 | title = "Example book"
10 | author = "John Doe"
11 | description = "The example book covers examples."
12 |
13 | [rust]
14 | edition = "2018"
15 |
16 | [build]
17 | build-dir = "my-example-book"
18 | create-missing = false
19 |
20 | [preprocessor.index]
21 |
22 | [preprocessor.links]
23 |
24 | [output.html]
25 | additional-css = ["custom.css"]
26 |
27 | [output.html.search]
28 | limit-results = 15
29 | ```
30 |
31 | ## Supported configuration options
32 |
33 | It is important to note that **any** relative path specified in the
34 | configuration will always be taken relative from the root of the book where the
35 | configuration file is located.
36 |
37 | ### General metadata
38 |
39 | This is general information about your book.
40 |
41 | - **title:** The title of the book
42 | - **authors:** The author(s) of the book
43 | - **description:** A description for the book, which is added as meta
44 | information in the html `` of each page
45 | - **src:** By default, the source directory is found in the directory named
46 | `src` directly under the root folder. But this is configurable with the `src`
47 | key in the configuration file.
48 | - **language:** The main language of the book, which is used as a language attribute `` for example.
49 |
50 | **book.toml**
51 | ```toml
52 | [book]
53 | title = "Example book"
54 | authors = ["John Doe", "Jane Doe"]
55 | description = "The example book covers examples."
56 | src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
57 | language = "en"
58 | ```
59 |
60 | ### Rust options
61 |
62 | Options for the Rust language, relevant to running tests and playground
63 | integration.
64 |
65 | ```toml
66 | [rust]
67 | edition = "2015" # the default edition for code blocks
68 | ```
69 |
70 | - **edition**: Rust edition to use by default for the code snippets. Default
71 | is "2015". Individual code blocks can be controlled with the `edition2015`,
72 | `edition2018` or `edition2021` annotations, such as:
73 |
74 | ~~~text
75 | ```rust,edition2015
76 | // This only works in 2015.
77 | let try = true;
78 | ```
79 | ~~~
80 |
81 | ### Build options
82 |
83 | This controls the build process of your book.
84 |
85 | ```toml
86 | [build]
87 | build-dir = "book" # the directory where the output is placed
88 | create-missing = true # whether or not to create missing pages
89 | use-default-preprocessors = true # use the default preprocessors
90 | ```
91 |
92 | - **build-dir:** The directory to put the rendered book in. By default this is
93 | `book/` in the book's root directory.
94 | This can overridden with the `--dest-dir` CLI option.
95 | - **create-missing:** By default, any missing files specified in `SUMMARY.md`
96 | will be created when the book is built (i.e. `create-missing = true`). If this
97 | is `false` then the build process will instead exit with an error if any files
98 | do not exist.
99 | - **use-default-preprocessors:** Disable the default preprocessors of (`links` &
100 | `index`) by setting this option to `false`.
101 |
102 | If you have the same, and/or other preprocessors declared via their table
103 | of configuration, they will run instead.
104 |
105 | - For clarity, with no preprocessor configuration, the default `links` and
106 | `index` will run.
107 | - Setting `use-default-preprocessors = false` will disable these
108 | default preprocessors from running.
109 | - Adding `[preprocessor.links]`, for example, will ensure, regardless of
110 | `use-default-preprocessors` that `links` it will run.
111 |
--------------------------------------------------------------------------------
/guide/src/guide/reading.md:
--------------------------------------------------------------------------------
1 | # Reading Books
2 |
3 | This chapter gives an introduction on how to interact with a book produced by mdBook.
4 | This assumes you are reading an HTML book.
5 | The options and formatting will be different for other output formats such as PDF.
6 |
7 | A book is organized into *chapters*.
8 | Each chapter is a separate page.
9 | Chapters can be nested into a hierarchy of sub-chapters.
10 | Typically, each chapter will be organized into a series of *headings* to subdivide a chapter.
11 |
12 | ## Navigation
13 |
14 | There are several methods for navigating through the chapters of a book.
15 |
16 | The **sidebar** on the left provides a list of all chapters.
17 | Clicking on any of the chapter titles will load that page.
18 |
19 | The sidebar may not automatically appear if the window is too narrow, particularly on mobile displays.
20 | In that situation, the menu icon (three horizontal bars) at the top-left of the page can be pressed to open and close the sidebar.
21 |
22 | The **arrow buttons** at the bottom of the page can be used to navigate to the previous or the next chapter.
23 |
24 | The **left and right arrow keys** on the keyboard can be used to navigate to the previous or the next chapter.
25 |
26 | ## Top menu bar
27 |
28 | The menu bar at the top of the page provides some icons for interacting with the book.
29 | The icons displayed will depend on the settings of how the book was generated.
30 |
31 | | Icon | Description |
32 | |------|-------------|
33 | | | Opens and closes the chapter listing sidebar. |
34 | | | Opens a picker to choose a different color theme. |
35 | | | Opens a search bar for searching within the book. |
36 | | | Instructs the web browser to print the entire book. |
37 | | | Opens a link to the website that hosts the source code of the book. |
38 | | | Opens a page to directly edit the source of the page you are currently reading. |
39 |
40 | Tapping the menu bar will scroll the page to the top.
41 |
42 | ## Search
43 |
44 | Each book has a built-in search system.
45 | Pressing the search icon () in the menu bar, or pressing the `S` key on the keyboard will open an input box for entering search terms.
46 | Typing some terms will show matching chapters and sections in real time.
47 |
48 | Clicking any of the results will jump to that section.
49 | The up and down arrow keys can be used to navigate the results, and enter will open the highlighted section.
50 |
51 | After loading a search result, the matching search terms will be highlighted in the text.
52 | Clicking a highlighted word or pressing the `Esc` key will remove the highlighting.
53 |
54 | ## Code blocks
55 |
56 | mdBook books are often used for programming projects, and thus support highlighting code blocks and samples.
57 | Code blocks may contain several different icons for interacting with them:
58 |
59 | | Icon | Description |
60 | |------|-------------|
61 | | | Copies the code block into your local clipboard, to allow pasting into another application. |
62 | | | For Rust code examples, this will execute the sample code and display the compiler output just below the example (see [playground]). |
63 | | | For Rust code examples, this will toggle visibility of "hidden" lines. Sometimes, larger examples will hide lines which are not particularly relevant to what is being illustrated (see [hiding code lines]). |
64 | | | For [editable code examples][editor], this will undo any changes you have made. |
65 |
66 | Here's an example:
67 |
68 | ```rust
69 | println!("Hello, World!");
70 | ```
71 |
72 | [editor]: ../format/theme/editor.md
73 | [playground]: ../format/mdbook.md#rust-playground
74 | [hiding code lines]: ../format/mdbook.md#hiding-code-lines
75 |
--------------------------------------------------------------------------------
/src/utils/toml_ext.rs:
--------------------------------------------------------------------------------
1 | use toml::value::{Table, Value};
2 |
3 | pub(crate) trait TomlExt {
4 | fn read(&self, key: &str) -> Option<&Value>;
5 | fn read_mut(&mut self, key: &str) -> Option<&mut Value>;
6 | fn insert(&mut self, key: &str, value: Value);
7 | fn delete(&mut self, key: &str) -> Option;
8 | }
9 |
10 | impl TomlExt for Value {
11 | fn read(&self, key: &str) -> Option<&Value> {
12 | if let Some((head, tail)) = split(key) {
13 | self.get(head)?.read(tail)
14 | } else {
15 | self.get(key)
16 | }
17 | }
18 |
19 | fn read_mut(&mut self, key: &str) -> Option<&mut Value> {
20 | if let Some((head, tail)) = split(key) {
21 | self.get_mut(head)?.read_mut(tail)
22 | } else {
23 | self.get_mut(key)
24 | }
25 | }
26 |
27 | fn insert(&mut self, key: &str, value: Value) {
28 | if !self.is_table() {
29 | *self = Value::Table(Table::new());
30 | }
31 |
32 | let table = self.as_table_mut().expect("unreachable");
33 |
34 | if let Some((head, tail)) = split(key) {
35 | table
36 | .entry(head)
37 | .or_insert_with(|| Value::Table(Table::new()))
38 | .insert(tail, value);
39 | } else {
40 | table.insert(key.to_string(), value);
41 | }
42 | }
43 |
44 | fn delete(&mut self, key: &str) -> Option {
45 | if let Some((head, tail)) = split(key) {
46 | self.get_mut(head)?.delete(tail)
47 | } else if let Some(table) = self.as_table_mut() {
48 | table.remove(key)
49 | } else {
50 | None
51 | }
52 | }
53 | }
54 |
55 | fn split(key: &str) -> Option<(&str, &str)> {
56 | let ix = key.find('.')?;
57 |
58 | let (head, tail) = key.split_at(ix);
59 | // splitting will leave the "."
60 | let tail = &tail[1..];
61 |
62 | Some((head, tail))
63 | }
64 |
65 | #[cfg(test)]
66 | mod tests {
67 | use super::*;
68 | use std::str::FromStr;
69 |
70 | #[test]
71 | fn read_simple_table() {
72 | let src = "[table]";
73 | let value = Value::from_str(src).unwrap();
74 |
75 | let got = value.read("table").unwrap();
76 |
77 | assert!(got.is_table());
78 | }
79 |
80 | #[test]
81 | fn read_nested_item() {
82 | let src = "[table]\nnested=true";
83 | let value = Value::from_str(src).unwrap();
84 |
85 | let got = value.read("table.nested").unwrap();
86 |
87 | assert_eq!(got, &Value::Boolean(true));
88 | }
89 |
90 | #[test]
91 | fn insert_item_at_top_level() {
92 | let mut value = Value::Table(Table::default());
93 | let item = Value::Boolean(true);
94 |
95 | value.insert("first", item.clone());
96 |
97 | assert_eq!(value.get("first").unwrap(), &item);
98 | }
99 |
100 | #[test]
101 | fn insert_nested_item() {
102 | let mut value = Value::Table(Table::default());
103 | let item = Value::Boolean(true);
104 |
105 | value.insert("first.second", item.clone());
106 |
107 | let inserted = value.read("first.second").unwrap();
108 | assert_eq!(inserted, &item);
109 | }
110 |
111 | #[test]
112 | fn delete_a_top_level_item() {
113 | let src = "top = true";
114 | let mut value = Value::from_str(src).unwrap();
115 |
116 | let got = value.delete("top").unwrap();
117 |
118 | assert_eq!(got, Value::Boolean(true));
119 | }
120 |
121 | #[test]
122 | fn delete_a_nested_item() {
123 | let src = "[table]\n nested = true";
124 | let mut value = Value::from_str(src).unwrap();
125 |
126 | let got = value.delete("table.nested").unwrap();
127 |
128 | assert_eq!(got, Value::Boolean(true));
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/theme/fonts/fonts.css:
--------------------------------------------------------------------------------
1 | /* Open Sans is licensed under the Apache License, Version 2.0. See http://www.apache.org/licenses/LICENSE-2.0 */
2 | /* Source Code Pro is under the Open Font License. See https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL */
3 |
4 | /* open-sans-300 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
5 | @font-face {
6 | font-family: 'Open Sans';
7 | font-style: normal;
8 | font-weight: 300;
9 | src: local('Open Sans Light'), local('OpenSans-Light'),
10 | url('open-sans-v17-all-charsets-300.woff2') format('woff2');
11 | }
12 |
13 | /* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
14 | @font-face {
15 | font-family: 'Open Sans';
16 | font-style: italic;
17 | font-weight: 300;
18 | src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'),
19 | url('open-sans-v17-all-charsets-300italic.woff2') format('woff2');
20 | }
21 |
22 | /* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
23 | @font-face {
24 | font-family: 'Open Sans';
25 | font-style: normal;
26 | font-weight: 400;
27 | src: local('Open Sans Regular'), local('OpenSans-Regular'),
28 | url('open-sans-v17-all-charsets-regular.woff2') format('woff2');
29 | }
30 |
31 | /* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
32 | @font-face {
33 | font-family: 'Open Sans';
34 | font-style: italic;
35 | font-weight: 400;
36 | src: local('Open Sans Italic'), local('OpenSans-Italic'),
37 | url('open-sans-v17-all-charsets-italic.woff2') format('woff2');
38 | }
39 |
40 | /* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
41 | @font-face {
42 | font-family: 'Open Sans';
43 | font-style: normal;
44 | font-weight: 600;
45 | src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
46 | url('open-sans-v17-all-charsets-600.woff2') format('woff2');
47 | }
48 |
49 | /* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
50 | @font-face {
51 | font-family: 'Open Sans';
52 | font-style: italic;
53 | font-weight: 600;
54 | src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'),
55 | url('open-sans-v17-all-charsets-600italic.woff2') format('woff2');
56 | }
57 |
58 | /* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
59 | @font-face {
60 | font-family: 'Open Sans';
61 | font-style: normal;
62 | font-weight: 700;
63 | src: local('Open Sans Bold'), local('OpenSans-Bold'),
64 | url('open-sans-v17-all-charsets-700.woff2') format('woff2');
65 | }
66 |
67 | /* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
68 | @font-face {
69 | font-family: 'Open Sans';
70 | font-style: italic;
71 | font-weight: 700;
72 | src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),
73 | url('open-sans-v17-all-charsets-700italic.woff2') format('woff2');
74 | }
75 |
76 | /* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
77 | @font-face {
78 | font-family: 'Open Sans';
79 | font-style: normal;
80 | font-weight: 800;
81 | src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'),
82 | url('open-sans-v17-all-charsets-800.woff2') format('woff2');
83 | }
84 |
85 | /* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
86 | @font-face {
87 | font-family: 'Open Sans';
88 | font-style: italic;
89 | font-weight: 800;
90 | src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'),
91 | url('open-sans-v17-all-charsets-800italic.woff2') format('woff2');
92 | }
93 |
94 | /* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */
95 | @font-face {
96 | font-family: 'Source Code Pro';
97 | font-style: normal;
98 | font-weight: 500;
99 | src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
100 | }
101 |
--------------------------------------------------------------------------------
/guide/src/format/summary.md:
--------------------------------------------------------------------------------
1 | # SUMMARY.md
2 |
3 | The summary file is used by mdBook to know what chapters to include, in what
4 | order they should appear, what their hierarchy is and where the source files
5 | are. Without this file, there is no book.
6 |
7 | This markdown file must be named `SUMMARY.md`. Its formatting
8 | is very strict and must follow the structure outlined below to allow for easy
9 | parsing. Any element not specified below, be it formatting or textual, is likely
10 | to be ignored at best, or may cause an error when attempting to build the book.
11 |
12 | ### Structure
13 |
14 | 1. ***Title*** - While optional, it's common practice to begin with a title, generally # Summary. This is ignored by the parser however, and
16 | can be omitted.
17 | ```markdown
18 | # Summary
19 | ```
20 |
21 | 1. ***Prefix Chapter*** - Before the main numbered chapters, prefix chapters can be added
22 | that will not be numbered. This is useful for forewords,
23 | introductions, etc. There are, however, some constraints. Prefix chapters cannot be
24 | nested; they should all be on the root level. And you cannot add
25 | prefix chapters once you have added numbered chapters.
26 | ```markdown
27 | [A Prefix Chapter](relative/path/to/markdown.md)
28 |
29 | - [First Chapter](relative/path/to/markdown2.md)
30 | ```
31 |
32 | 1. ***Part Title*** - Headers can be used as a title for the following numbered
33 | chapters. This can be used to logically separate different sections
34 | of the book. The title is rendered as unclickable text.
35 | Titles are optional, and the numbered chapters can be broken into as many
36 | parts as desired.
37 | ```markdown
38 | # My Part Title
39 |
40 | - [First Chapter](relative/path/to/markdown.md)
41 | ```
42 |
43 | 1. ***Numbered Chapter*** - Numbered chapters outline the main content of the book
44 | and can be nested, resulting in a nice hierarchy
45 | (chapters, sub-chapters, etc.).
46 | ```markdown
47 | # Title of Part
48 |
49 | - [First Chapter](relative/path/to/markdown.md)
50 | - [Second Chapter](relative/path/to/markdown2.md)
51 | - [Sub Chapter](relative/path/to/markdown3.md)
52 |
53 | # Title of Another Part
54 |
55 | - [Another Chapter](relative/path/to/markdown4.md)
56 | ```
57 | Numbered chapters can be denoted with either `-` or `*` (do not mix delimiters).
58 |
59 | 1. ***Suffix Chapter*** - Like prefix chapters, suffix chapters are unnumbered, but they come after
60 | numbered chapters.
61 | ```markdown
62 | - [Last Chapter](relative/path/to/markdown.md)
63 |
64 | [Title of Suffix Chapter](relative/path/to/markdown2.md)
65 | ```
66 |
67 | 1. ***Draft chapters*** - Draft chapters are chapters without a file and thus content.
68 | The purpose of a draft chapter is to signal future chapters still to be written.
69 | Or when still laying out the structure of the book to avoid creating the files
70 | while you are still changing the structure of the book a lot.
71 | Draft chapters will be rendered in the HTML renderer as disabled links in the table
72 | of contents, as you can see for the next chapter in the table of contents on the left.
73 | Draft chapters are written like normal chapters but without writing the path to the file.
74 | ```markdown
75 | - [Draft Chapter]()
76 | ```
77 |
78 | 1. ***Separators*** - Separators can be added before, in between, and after any other element. They result
79 | in an HTML rendered line in the built table of contents. A separator is
80 | a line containing exclusively dashes and at least three of them: `---`.
81 | ```markdown
82 | # My Part Title
83 |
84 | [A Prefix Chapter](relative/path/to/markdown.md)
85 |
86 | ---
87 |
88 | - [First Chapter](relative/path/to/markdown2.md)
89 | ```
90 |
91 |
92 | ### Example
93 |
94 | Below is the markdown source for the `SUMMARY.md` for this guide, with the resulting table
95 | of contents as rendered to the left.
96 |
97 | ```markdown
98 | {{#include ../SUMMARY.md}}
99 | ```
100 |
--------------------------------------------------------------------------------
/guide/src/guide/creating.md:
--------------------------------------------------------------------------------
1 | # Creating a Book
2 |
3 | Once you have the `mdbook` CLI tool installed, you can use it to create and render a book.
4 |
5 | ## Initializing a book
6 |
7 | The `mdbook init` command will create a new directory containing an empty book for you to get started.
8 | Give it the name of the directory that you want to create:
9 |
10 | ```sh
11 | mdbook init my-first-book
12 | ```
13 |
14 | It will ask a few questions before generating the book.
15 | After answering the questions, you can change the current directory into the new book:
16 |
17 | ```sh
18 | cd my-first-book
19 | ```
20 |
21 | There are several ways to render a book, but one of the easiest methods is to use the `serve` command, which will build your book and start a local webserver:
22 |
23 | ```sh
24 | mdbook serve --open
25 | ```
26 |
27 | The `--open` option will open your default web browser to view your new book.
28 | You can leave the server running even while you edit the content of the book, and `mdbook` will automatically rebuild the output *and* automatically refresh your web browser.
29 |
30 | Check out the [CLI Guide](../cli/index.html) for more information about other `mdbook` commands and CLI options.
31 |
32 | ## Anatomy of a book
33 |
34 | A book is built from several files which define the settings and layout of the book.
35 |
36 | ### `book.toml`
37 |
38 | In the root of your book, there is a `book.toml` file which contains settings for describing how to build your book.
39 | This is written in the [TOML markup language](https://toml.io/).
40 | The default settings are usually good enough to get you started.
41 | When you are interested in exploring more features and options that mdBook provides, check out the [Configuration chapter](../format/configuration/index.html) for more details.
42 |
43 | A very basic `book.toml` can be as simple as this:
44 |
45 | ```toml
46 | [book]
47 | title = "My First Book"
48 | ```
49 |
50 | ### `SUMMARY.md`
51 |
52 | The next major part of a book is the summary file located at `src/SUMMARY.md`.
53 | This file contains a list of all the chapters in the book.
54 | Before a chapter can be viewed, it must be added to this list.
55 |
56 | Here's a basic summary file with a few chapters:
57 |
58 | ```md
59 | # Summary
60 |
61 | [Introduction](README.md)
62 |
63 | - [My First Chapter](my-first-chapter.md)
64 | - [Nested example](nested/README.md)
65 | - [Sub-chapter](nested/sub-chapter.md)
66 | ```
67 |
68 | Try opening up `src/SUMMARY.md` in your editor and adding a few chapters.
69 | If any of the chapter files do not exist, `mdbook` will automatically create them for you.
70 |
71 | For more details on other formatting options for the summary file, check out the [Summary chapter](../format/summary.md).
72 |
73 | ### Source files
74 |
75 | The content of your book is all contained in the `src` directory.
76 | Each chapter is a separate Markdown file.
77 | Typically, each chapter starts with a level 1 heading with the title of the chapter.
78 |
79 | ```md
80 | # My First Chapter
81 |
82 | Fill out your content here.
83 | ```
84 |
85 | The precise layout of the files is up to you.
86 | The organization of the files will correspond to the HTML files generated, so keep in mind that the file layout is part of the URL of each chapter.
87 |
88 | While the `mdbook serve` command is running, you can open any of the chapter files and start editing them.
89 | Each time you save the file, `mdbook` will rebuild the book and refresh your web browser.
90 |
91 | Check out the [Markdown chapter](../format/markdown.md) for more information on formatting the content of your chapters.
92 |
93 | All other files in the `src` directory will be included in the output.
94 | So if you have images or other static files, just include them somewhere in the `src` directory.
95 |
96 | ## Publishing a book
97 |
98 | Once you've written your book, you may want to host it somewhere for others to view.
99 | The first step is to build the output of the book.
100 | This can be done with the `mdbook build` command in the same directory where the `book.toml` file is located:
101 |
102 | ```sh
103 | mdbook build
104 | ```
105 |
106 | This will generate a directory named `book` which contains the HTML content of your book.
107 | You can then place this directory on any web server to host it.
108 |
109 | For more information about publishing and deploying, check out the [Continuous Integration chapter](../continuous-integration.md) for more.
--------------------------------------------------------------------------------
/src/cmd/init.rs:
--------------------------------------------------------------------------------
1 | use crate::get_book_dir;
2 | use clap::{arg, App, Arg, ArgMatches};
3 | use mdbook::config;
4 | use mdbook::errors::Result;
5 | use mdbook::MDBook;
6 | use std::io;
7 | use std::io::Write;
8 | use std::process::Command;
9 |
10 | // Create clap subcommand arguments
11 | pub fn make_subcommand<'help>() -> App<'help> {
12 | App::new("init")
13 | .about("Creates the boilerplate structure and files for a new book")
14 | // the {n} denotes a newline which will properly aligned in all help messages
15 | .arg(arg!([dir]
16 | "Directory to create the book in{n}\
17 | (Defaults to the Current Directory when omitted)"
18 | ))
19 | .arg(arg!(--theme "Copies the default theme into your source folder"))
20 | .arg(arg!(--force "Skips confirmation prompts"))
21 | .arg(
22 | Arg::new("title")
23 | .long("title")
24 | .takes_value(true)
25 | .help("Sets the book title")
26 | .required(false),
27 | )
28 | .arg(
29 | Arg::new("ignore")
30 | .long("ignore")
31 | .takes_value(true)
32 | .possible_values(&["none", "git"])
33 | .help("Creates a VCS ignore file (i.e. .gitignore)")
34 | .required(false),
35 | )
36 | }
37 |
38 | // Init command implementation
39 | pub fn execute(args: &ArgMatches) -> Result<()> {
40 | let book_dir = get_book_dir(args);
41 | let mut builder = MDBook::init(&book_dir);
42 | let mut config = config::Config::default();
43 | // If flag `--theme` is present, copy theme to src
44 | if args.is_present("theme") {
45 | let theme_dir = book_dir.join("theme");
46 | println!();
47 | println!("Copying the default theme to {}", theme_dir.display());
48 | // Skip this if `--force` is present
49 | if !args.is_present("force") && theme_dir.exists() {
50 | println!("This could potentially overwrite files already present in that directory.");
51 | print!("\nAre you sure you want to continue? (y/n) ");
52 |
53 | // Read answer from user and exit if it's not 'yes'
54 | if confirm() {
55 | builder.copy_theme(true);
56 | }
57 | } else {
58 | builder.copy_theme(true);
59 | }
60 | }
61 |
62 | if let Some(ignore) = args.value_of("ignore") {
63 | match ignore {
64 | "git" => builder.create_gitignore(true),
65 | _ => builder.create_gitignore(false),
66 | };
67 | } else {
68 | println!("\nDo you want a .gitignore to be created? (y/n)");
69 | if confirm() {
70 | builder.create_gitignore(true);
71 | }
72 | }
73 |
74 | config.book.title = if args.is_present("title") {
75 | args.value_of("title").map(String::from)
76 | } else {
77 | request_book_title()
78 | };
79 |
80 | if let Some(author) = get_author_name() {
81 | debug!("Obtained user name from gitconfig: {:?}", author);
82 | config.book.authors.push(author);
83 | builder.with_config(config);
84 | }
85 |
86 | builder.build()?;
87 | println!("\nAll done, no errors...");
88 |
89 | Ok(())
90 | }
91 |
92 | /// Obtains author name from git config file by running the `git config` command.
93 | fn get_author_name() -> Option {
94 | let output = Command::new("git")
95 | .args(&["config", "--get", "user.name"])
96 | .output()
97 | .ok()?;
98 |
99 | if output.status.success() {
100 | Some(String::from_utf8_lossy(&output.stdout).trim().to_owned())
101 | } else {
102 | None
103 | }
104 | }
105 |
106 | /// Request book title from user and return if provided.
107 | fn request_book_title() -> Option {
108 | println!("What title would you like to give the book? ");
109 | io::stdout().flush().unwrap();
110 | let mut resp = String::new();
111 | io::stdin().read_line(&mut resp).unwrap();
112 | let resp = resp.trim();
113 | if resp.is_empty() {
114 | None
115 | } else {
116 | Some(resp.into())
117 | }
118 | }
119 |
120 | // Simple function for user confirmation
121 | fn confirm() -> bool {
122 | io::stdout().flush().unwrap();
123 | let mut s = String::new();
124 | io::stdin().read_line(&mut s).ok();
125 | match &*s.trim() {
126 | "Y" | "y" | "yes" | "Yes" => true,
127 | _ => false,
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! # mdBook
2 | //!
3 | //! **mdBook** is a tool for rendering a collection of markdown documents into
4 | //! a form more suitable for end users like HTML or EPUB. It offers a command
5 | //! line interface, but this crate can be used if more control is required.
6 | //!
7 | //! This is the API doc, the [user guide] is also available if you want
8 | //! information about the command line tool, format, structure etc. It is also
9 | //! rendered with mdBook to showcase the features and default theme.
10 | //!
11 | //! Some reasons why you would want to use the crate (over the cli):
12 | //!
13 | //! - Integrate mdbook in a current project
14 | //! - Extend the capabilities of mdBook
15 | //! - Do some processing or test before building your book
16 | //! - Accessing the public API to help create a new Renderer
17 | //! - ...
18 | //!
19 | //! > **Note:** While we try to ensure `mdbook`'s command-line interface and
20 | //! > behaviour are backwards compatible, the tool's internals are still
21 | //! > evolving and being iterated on. If you wish to prevent accidental
22 | //! > breakages it is recommended to pin any tools building on top of the
23 | //! > `mdbook` crate to a specific release.
24 | //!
25 | //! # Examples
26 | //!
27 | //! If creating a new book from scratch, you'll want to get a `BookBuilder` via
28 | //! the `MDBook::init()` method.
29 | //!
30 | //! ```rust,no_run
31 | //! use mdbook::MDBook;
32 | //! use mdbook::config::Config;
33 | //!
34 | //! let root_dir = "/path/to/book/root";
35 | //!
36 | //! // create a default config and change a couple things
37 | //! let mut cfg = Config::default();
38 | //! cfg.book.title = Some("My Book".to_string());
39 | //! cfg.book.authors.push("Michael-F-Bryan".to_string());
40 | //!
41 | //! MDBook::init(root_dir)
42 | //! .create_gitignore(true)
43 | //! .with_config(cfg)
44 | //! .build()
45 | //! .expect("Book generation failed");
46 | //! ```
47 | //!
48 | //! You can also load an existing book and build it.
49 | //!
50 | //! ```rust,no_run
51 | //! use mdbook::MDBook;
52 | //!
53 | //! let root_dir = "/path/to/book/root";
54 | //!
55 | //! let mut md = MDBook::load(root_dir)
56 | //! .expect("Unable to load the book");
57 | //! md.build().expect("Building failed");
58 | //! ```
59 | //!
60 | //! ## Implementing a new Backend
61 | //!
62 | //! `mdbook` has a fairly flexible mechanism for creating additional backends
63 | //! for your book. The general idea is you'll add an extra table in the book's
64 | //! `book.toml` which specifies an executable to be invoked by `mdbook`. This
65 | //! executable will then be called during a build, with an in-memory
66 | //! representation ([`RenderContext`]) of the book being passed to the
67 | //! subprocess via `stdin`.
68 | //!
69 | //! The [`RenderContext`] gives the backend access to the contents of
70 | //! `book.toml` and lets it know which directory all generated artefacts should
71 | //! be placed in. For a much more in-depth explanation, consult the [relevant
72 | //! chapter] in the *For Developers* section of the user guide.
73 | //!
74 | //! To make creating a backend easier, the `mdbook` crate can be imported
75 | //! directly, making deserializing the `RenderContext` easy and giving you
76 | //! access to the various methods for working with the [`Config`].
77 | //!
78 | //! [user guide]: https://rust-lang.github.io/mdBook/
79 | //! [`RenderContext`]: renderer::RenderContext
80 | //! [relevant chapter]: https://rust-lang.github.io/mdBook/for_developers/backends.html
81 | //! [`Config`]: config::Config
82 |
83 | #![deny(missing_docs)]
84 | #![deny(rust_2018_idioms)]
85 | #![allow(clippy::comparison_chain)]
86 |
87 | #[macro_use]
88 | extern crate lazy_static;
89 | #[macro_use]
90 | extern crate log;
91 | #[macro_use]
92 | extern crate serde_derive;
93 | #[macro_use]
94 | extern crate serde_json;
95 |
96 | #[cfg(test)]
97 | #[macro_use]
98 | extern crate pretty_assertions;
99 |
100 | pub mod book;
101 | pub mod config;
102 | pub mod preprocess;
103 | pub mod renderer;
104 | pub mod theme;
105 | pub mod utils;
106 |
107 | /// The current version of `mdbook`.
108 | ///
109 | /// This is provided as a way for custom preprocessors and renderers to do
110 | /// compatibility checks.
111 | pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
112 |
113 | pub use crate::book::BookItem;
114 | pub use crate::book::MDBook;
115 | pub use crate::config::Config;
116 | pub use crate::renderer::Renderer;
117 |
118 | /// The error types used through out this crate.
119 | pub mod errors {
120 | pub(crate) use anyhow::{bail, ensure, Context};
121 | pub use anyhow::{Error, Result};
122 | }
123 |
--------------------------------------------------------------------------------
/src/theme/fonts/SOURCE-CODE-PRO-LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/src/theme/css/general.css:
--------------------------------------------------------------------------------
1 | /* Base styles and content styles */
2 |
3 | @import 'variables.css';
4 |
5 | :root {
6 | /* Browser default font-size is 16px, this way 1 rem = 10px */
7 | font-size: 62.5%;
8 | }
9 |
10 | html {
11 | font-family: "Open Sans", sans-serif;
12 | color: var(--fg);
13 | background-color: var(--bg);
14 | text-size-adjust: none;
15 | -webkit-text-size-adjust: none;
16 | }
17 |
18 | body {
19 | margin: 0;
20 | font-size: 1.6rem;
21 | overflow-x: hidden;
22 | }
23 |
24 | code {
25 | font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important;
26 | font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
27 | }
28 |
29 | /* Don't change font size in headers. */
30 | h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
31 | font-size: unset;
32 | }
33 |
34 | .left { float: left; }
35 | .right { float: right; }
36 | .boring { opacity: 0.6; }
37 | .hide-boring .boring { display: none; }
38 | .hidden { display: none !important; }
39 |
40 | h2, h3 { margin-top: 2.5em; }
41 | h4, h5 { margin-top: 2em; }
42 |
43 | .header + .header h3,
44 | .header + .header h4,
45 | .header + .header h5 {
46 | margin-top: 1em;
47 | }
48 |
49 | h1:target::before,
50 | h2:target::before,
51 | h3:target::before,
52 | h4:target::before,
53 | h5:target::before,
54 | h6:target::before {
55 | display: inline-block;
56 | content: "»";
57 | margin-left: -30px;
58 | width: 30px;
59 | }
60 |
61 | /* This is broken on Safari as of version 14, but is fixed
62 | in Safari Technology Preview 117 which I think will be Safari 14.2.
63 | https://bugs.webkit.org/show_bug.cgi?id=218076
64 | */
65 | :target {
66 | scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
67 | }
68 |
69 | .page {
70 | outline: 0;
71 | padding: 0 var(--page-padding);
72 | margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
73 | }
74 | .page-wrapper {
75 | box-sizing: border-box;
76 | }
77 | .js:not(.sidebar-resizing) .page-wrapper {
78 | transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
79 | }
80 |
81 | .content {
82 | overflow-y: auto;
83 | padding: 0 15px;
84 | padding-bottom: 50px;
85 | }
86 | .content main {
87 | margin-left: auto;
88 | margin-right: auto;
89 | max-width: var(--content-max-width);
90 | }
91 | .content p { line-height: 1.45em; }
92 | .content ol { line-height: 1.45em; }
93 | .content ul { line-height: 1.45em; }
94 | .content a { text-decoration: none; }
95 | .content a:hover { text-decoration: underline; }
96 | .content img, .content video { max-width: 100%; }
97 | .content .header:link,
98 | .content .header:visited {
99 | color: var(--fg);
100 | }
101 | .content .header:link,
102 | .content .header:visited:hover {
103 | text-decoration: none;
104 | }
105 |
106 | table {
107 | margin: 0 auto;
108 | border-collapse: collapse;
109 | }
110 | table td {
111 | padding: 3px 20px;
112 | border: 1px var(--table-border-color) solid;
113 | }
114 | table thead {
115 | background: var(--table-header-bg);
116 | }
117 | table thead td {
118 | font-weight: 700;
119 | border: none;
120 | }
121 | table thead th {
122 | padding: 3px 20px;
123 | }
124 | table thead tr {
125 | border: 1px var(--table-header-bg) solid;
126 | }
127 | /* Alternate background colors for rows */
128 | table tbody tr:nth-child(2n) {
129 | background: var(--table-alternate-bg);
130 | }
131 |
132 |
133 | blockquote {
134 | margin: 20px 0;
135 | padding: 0 20px;
136 | color: var(--fg);
137 | background-color: var(--quote-bg);
138 | border-top: .1em solid var(--quote-border);
139 | border-bottom: .1em solid var(--quote-border);
140 | }
141 |
142 |
143 | :not(.footnote-definition) + .footnote-definition,
144 | .footnote-definition + :not(.footnote-definition) {
145 | margin-top: 2em;
146 | }
147 | .footnote-definition {
148 | font-size: 0.9em;
149 | margin: 0.5em 0;
150 | }
151 | .footnote-definition p {
152 | display: inline;
153 | }
154 |
155 | .tooltiptext {
156 | position: absolute;
157 | visibility: hidden;
158 | color: #fff;
159 | background-color: #333;
160 | transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
161 | left: -8px; /* Half of the width of the icon */
162 | top: -35px;
163 | font-size: 0.8em;
164 | text-align: center;
165 | border-radius: 6px;
166 | padding: 5px 8px;
167 | margin: 5px;
168 | z-index: 1000;
169 | }
170 | .tooltipped .tooltiptext {
171 | visibility: visible;
172 | }
173 |
174 | .chapter li.part-title {
175 | color: var(--sidebar-fg);
176 | margin: 5px 0px;
177 | font-weight: bold;
178 | }
179 |
180 | .result-no-output {
181 | font-style: italic;
182 | }
183 |
--------------------------------------------------------------------------------
/tests/init.rs:
--------------------------------------------------------------------------------
1 | use mdbook::config::Config;
2 | use mdbook::MDBook;
3 | use std::fs;
4 | use std::fs::File;
5 | use std::io::prelude::*;
6 | use std::path::PathBuf;
7 | use tempfile::Builder as TempFileBuilder;
8 |
9 | /// Run `mdbook init` in an empty directory and make sure the default files
10 | /// are created.
11 | #[test]
12 | fn base_mdbook_init_should_create_default_content() {
13 | let created_files = vec!["book", "src", "src/SUMMARY.md", "src/chapter_1.md"];
14 |
15 | let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
16 | for file in &created_files {
17 | assert!(!temp.path().join(file).exists());
18 | }
19 |
20 | MDBook::init(temp.path()).build().unwrap();
21 |
22 | for file in &created_files {
23 | let target = temp.path().join(file);
24 | println!("{}", target.display());
25 | assert!(target.exists(), "{} doesn't exist", file);
26 | }
27 |
28 | let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
29 | assert_eq!(
30 | contents,
31 | "[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = false\nsrc = \"src\"\n"
32 | );
33 | }
34 |
35 | /// Run `mdbook init` in a directory containing a SUMMARY.md should create the
36 | /// files listed in the summary.
37 | #[test]
38 | fn run_mdbook_init_should_create_content_from_summary() {
39 | let created_files = vec!["intro.md", "first.md", "outro.md"];
40 |
41 | let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
42 | let src_dir = temp.path().join("src");
43 | fs::create_dir_all(src_dir.clone()).unwrap();
44 | static SUMMARY: &str = r#"# Summary
45 |
46 | [intro](intro.md)
47 |
48 | - [First chapter](first.md)
49 |
50 | [outro](outro.md)
51 |
52 | "#;
53 |
54 | let mut summary = File::create(src_dir.join("SUMMARY.md")).unwrap();
55 | summary.write_all(SUMMARY.as_bytes()).unwrap();
56 | MDBook::init(temp.path()).build().unwrap();
57 |
58 | for file in &created_files {
59 | let target = src_dir.join(file);
60 | println!("{}", target.display());
61 | assert!(target.exists(), "{} doesn't exist", file);
62 | }
63 | }
64 |
65 | /// Set some custom arguments for where to place the source and destination
66 | /// files, then call `mdbook init`.
67 | #[test]
68 | fn run_mdbook_init_with_custom_book_and_src_locations() {
69 | let created_files = vec!["out", "in", "in/SUMMARY.md", "in/chapter_1.md"];
70 |
71 | let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
72 | for file in &created_files {
73 | assert!(
74 | !temp.path().join(file).exists(),
75 | "{} shouldn't exist yet!",
76 | file
77 | );
78 | }
79 |
80 | let mut cfg = Config::default();
81 | cfg.book.src = PathBuf::from("in");
82 | cfg.build.build_dir = PathBuf::from("out");
83 |
84 | MDBook::init(temp.path()).with_config(cfg).build().unwrap();
85 |
86 | for file in &created_files {
87 | let target = temp.path().join(file);
88 | assert!(
89 | target.exists(),
90 | "{} should have been created by `mdbook init`",
91 | file
92 | );
93 | }
94 |
95 | let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
96 | assert_eq!(
97 | contents,
98 | "[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = false\nsrc = \"in\"\n\n[build]\nbuild-dir = \"out\"\ncreate-missing = true\nuse-default-preprocessors = true\n"
99 | );
100 | }
101 |
102 | #[test]
103 | fn book_toml_isnt_required() {
104 | let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
105 | let md = MDBook::init(temp.path()).build().unwrap();
106 |
107 | let _ = fs::remove_file(temp.path().join("book.toml"));
108 |
109 | md.build().unwrap();
110 | }
111 |
112 | #[test]
113 | fn copy_theme() {
114 | let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
115 | MDBook::init(temp.path()).copy_theme(true).build().unwrap();
116 | let expected = vec![
117 | "book.js",
118 | "css/chrome.css",
119 | "css/general.css",
120 | "css/print.css",
121 | "css/variables.css",
122 | "favicon.png",
123 | "favicon.svg",
124 | "highlight.css",
125 | "highlight.js",
126 | "index.hbs",
127 | ];
128 | let theme_dir = temp.path().join("theme");
129 | let mut actual: Vec<_> = walkdir::WalkDir::new(&theme_dir)
130 | .into_iter()
131 | .filter_map(|e| e.ok())
132 | .filter(|e| !e.file_type().is_dir())
133 | .map(|e| {
134 | e.path()
135 | .strip_prefix(&theme_dir)
136 | .unwrap()
137 | .to_str()
138 | .unwrap()
139 | .replace('\\', "/")
140 | })
141 | .collect();
142 | actual.sort();
143 | assert_eq!(actual, expected);
144 | }
145 |
--------------------------------------------------------------------------------
/tests/alternative_backends.rs:
--------------------------------------------------------------------------------
1 | //! Integration tests to make sure alternative backends work.
2 |
3 | use mdbook::config::Config;
4 | use mdbook::MDBook;
5 | use std::fs;
6 | use std::path::Path;
7 | use tempfile::{Builder as TempFileBuilder, TempDir};
8 |
9 | #[test]
10 | fn passing_alternate_backend() {
11 | let (md, _temp) = dummy_book_with_backend("passing", success_cmd(), false);
12 |
13 | md.build().unwrap();
14 | }
15 |
16 | #[test]
17 | fn failing_alternate_backend() {
18 | let (md, _temp) = dummy_book_with_backend("failing", fail_cmd(), false);
19 |
20 | md.build().unwrap_err();
21 | }
22 |
23 | #[test]
24 | fn missing_backends_are_fatal() {
25 | let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", false);
26 | assert!(md.build().is_err());
27 | }
28 |
29 | #[test]
30 | fn missing_optional_backends_are_not_fatal() {
31 | let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", true);
32 | assert!(md.build().is_ok());
33 | }
34 |
35 | #[test]
36 | fn alternate_backend_with_arguments() {
37 | let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!", false);
38 |
39 | md.build().unwrap();
40 | }
41 |
42 | /// Get a command which will pipe `stdin` to the provided file.
43 | #[cfg(not(windows))]
44 | fn tee_command>(out_file: P) -> String {
45 | let out_file = out_file.as_ref();
46 |
47 | if cfg!(windows) {
48 | format!("cmd.exe /c \"type > {}\"", out_file.display())
49 | } else {
50 | format!("tee {}", out_file.display())
51 | }
52 | }
53 |
54 | #[test]
55 | #[cfg(not(windows))]
56 | fn backends_receive_render_context_via_stdin() {
57 | use mdbook::renderer::RenderContext;
58 | use std::fs::File;
59 |
60 | let temp = TempFileBuilder::new().prefix("output").tempdir().unwrap();
61 | let out_file = temp.path().join("out.txt");
62 | let cmd = tee_command(&out_file);
63 |
64 | let (md, _temp) = dummy_book_with_backend("cat-to-file", &cmd, false);
65 |
66 | assert!(!out_file.exists());
67 | md.build().unwrap();
68 | assert!(out_file.exists());
69 |
70 | let got = RenderContext::from_json(File::open(&out_file).unwrap());
71 | assert!(got.is_ok());
72 | }
73 |
74 | #[test]
75 | fn relative_command_path() {
76 | // Checks behavior of relative paths for the `command` setting.
77 | let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
78 | let renderers = temp.path().join("renderers");
79 | fs::create_dir(&renderers).unwrap();
80 | rust_exe(
81 | &renderers,
82 | "myrenderer",
83 | r#"fn main() {
84 | std::fs::write("output", "test").unwrap();
85 | }"#,
86 | );
87 | let do_test = |cmd_path| {
88 | let mut config = Config::default();
89 | config
90 | .set("output.html", toml::value::Table::new())
91 | .unwrap();
92 | config.set("output.myrenderer.command", cmd_path).unwrap();
93 | let md = MDBook::init(&temp.path())
94 | .with_config(config)
95 | .build()
96 | .unwrap();
97 | let output = temp.path().join("book/myrenderer/output");
98 | assert!(!output.exists());
99 | md.build().unwrap();
100 | assert!(output.exists());
101 | fs::remove_file(output).unwrap();
102 | };
103 | // Legacy paths work, relative to the output directory.
104 | if cfg!(windows) {
105 | do_test("../../renderers/myrenderer.exe");
106 | } else {
107 | do_test("../../renderers/myrenderer");
108 | }
109 | // Modern path, relative to the book directory.
110 | do_test("renderers/myrenderer");
111 | }
112 |
113 | fn dummy_book_with_backend(
114 | name: &str,
115 | command: &str,
116 | backend_is_optional: bool,
117 | ) -> (MDBook, TempDir) {
118 | let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
119 |
120 | let mut config = Config::default();
121 | config
122 | .set(format!("output.{}.command", name), command)
123 | .unwrap();
124 |
125 | if backend_is_optional {
126 | config
127 | .set(format!("output.{}.optional", name), true)
128 | .unwrap();
129 | }
130 |
131 | let md = MDBook::init(temp.path())
132 | .with_config(config)
133 | .build()
134 | .unwrap();
135 |
136 | (md, temp)
137 | }
138 |
139 | fn fail_cmd() -> &'static str {
140 | if cfg!(windows) {
141 | r#"cmd.exe /c "exit 1""#
142 | } else {
143 | "false"
144 | }
145 | }
146 |
147 | fn success_cmd() -> &'static str {
148 | if cfg!(windows) {
149 | r#"cmd.exe /c "exit 0""#
150 | } else {
151 | "true"
152 | }
153 | }
154 |
155 | fn rust_exe(temp: &Path, name: &str, src: &str) {
156 | let rs = temp.join(name).with_extension("rs");
157 | fs::write(&rs, src).unwrap();
158 | let status = std::process::Command::new("rustc")
159 | .arg(rs)
160 | .current_dir(temp)
161 | .status()
162 | .expect("rustc should run");
163 | assert!(status.success());
164 | }
165 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | #[macro_use]
2 | extern crate clap;
3 | #[macro_use]
4 | extern crate log;
5 |
6 | use anyhow::anyhow;
7 | use chrono::Local;
8 | use clap::{App, AppSettings, Arg, ArgMatches};
9 | use clap_complete::Shell;
10 | use env_logger::Builder;
11 | use log::LevelFilter;
12 | use mdbook::utils;
13 | use std::env;
14 | use std::ffi::OsStr;
15 | use std::io::Write;
16 | use std::path::{Path, PathBuf};
17 |
18 | mod cmd;
19 |
20 | const VERSION: &str = concat!("v", crate_version!());
21 |
22 | fn main() {
23 | init_logger();
24 |
25 | let app = create_clap_app();
26 |
27 | // Check which subcomamnd the user ran...
28 | let res = match app.get_matches().subcommand() {
29 | Some(("init", sub_matches)) => cmd::init::execute(sub_matches),
30 | Some(("build", sub_matches)) => cmd::build::execute(sub_matches),
31 | Some(("clean", sub_matches)) => cmd::clean::execute(sub_matches),
32 | #[cfg(feature = "watch")]
33 | Some(("watch", sub_matches)) => cmd::watch::execute(sub_matches),
34 | #[cfg(feature = "serve")]
35 | Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches),
36 | Some(("test", sub_matches)) => cmd::test::execute(sub_matches),
37 | Some(("completions", sub_matches)) => (|| {
38 | let shell: Shell = sub_matches
39 | .value_of("shell")
40 | .ok_or_else(|| anyhow!("Shell name missing."))?
41 | .parse()
42 | .map_err(|s| anyhow!("Invalid shell: {}", s))?;
43 |
44 | let mut complete_app = create_clap_app();
45 | clap_complete::generate(
46 | shell,
47 | &mut complete_app,
48 | "mdbook",
49 | &mut std::io::stdout().lock(),
50 | );
51 | Ok(())
52 | })(),
53 | _ => unreachable!(),
54 | };
55 |
56 | if let Err(e) = res {
57 | utils::log_backtrace(&e);
58 |
59 | std::process::exit(101);
60 | }
61 | }
62 |
63 | /// Create a list of valid arguments and sub-commands
64 | fn create_clap_app() -> App<'static> {
65 | let app = App::new(crate_name!())
66 | .about(crate_description!())
67 | .author("Mathieu David ")
68 | .version(VERSION)
69 | .setting(AppSettings::PropagateVersion)
70 | .setting(AppSettings::ArgRequiredElseHelp)
71 | .after_help(
72 | "For more information about a specific command, try `mdbook --help`\n\
73 | The source code for mdBook is available at: https://github.com/rust-lang/mdBook",
74 | )
75 | .subcommand(cmd::init::make_subcommand())
76 | .subcommand(cmd::build::make_subcommand())
77 | .subcommand(cmd::test::make_subcommand())
78 | .subcommand(cmd::clean::make_subcommand())
79 | .subcommand(
80 | App::new("completions")
81 | .about("Generate shell completions for your shell to stdout")
82 | .arg(
83 | Arg::new("shell")
84 | .takes_value(true)
85 | .possible_values(Shell::possible_values())
86 | .help("the shell to generate completions for")
87 | .value_name("SHELL")
88 | .required(true),
89 | ),
90 | );
91 |
92 | #[cfg(feature = "watch")]
93 | let app = app.subcommand(cmd::watch::make_subcommand());
94 | #[cfg(feature = "serve")]
95 | let app = app.subcommand(cmd::serve::make_subcommand());
96 |
97 | app
98 | }
99 |
100 | fn init_logger() {
101 | let mut builder = Builder::new();
102 |
103 | builder.format(|formatter, record| {
104 | writeln!(
105 | formatter,
106 | "{} [{}] ({}): {}",
107 | Local::now().format("%Y-%m-%d %H:%M:%S"),
108 | record.level(),
109 | record.target(),
110 | record.args()
111 | )
112 | });
113 |
114 | if let Ok(var) = env::var("RUST_LOG") {
115 | builder.parse_filters(&var);
116 | } else {
117 | // if no RUST_LOG provided, default to logging at the Info level
118 | builder.filter(None, LevelFilter::Info);
119 | // Filter extraneous html5ever not-implemented messages
120 | builder.filter(Some("html5ever"), LevelFilter::Error);
121 | }
122 |
123 | builder.init();
124 | }
125 |
126 | fn get_book_dir(args: &ArgMatches) -> PathBuf {
127 | if let Some(dir) = args.value_of("dir") {
128 | // Check if path is relative from current dir, or absolute...
129 | let p = Path::new(dir);
130 | if p.is_relative() {
131 | env::current_dir().unwrap().join(dir)
132 | } else {
133 | p.to_path_buf()
134 | }
135 | } else {
136 | env::current_dir().expect("Unable to determine the current directory")
137 | }
138 | }
139 |
140 | fn open>(path: P) {
141 | info!("Opening web browser");
142 | if let Err(e) = opener::open(path) {
143 | error!("Error opening web browser: {}", e);
144 | }
145 | }
146 |
147 | #[test]
148 | fn verify_app() {
149 | create_clap_app().debug_assert();
150 | }
151 |
--------------------------------------------------------------------------------
/tests/dummy_book/mod.rs:
--------------------------------------------------------------------------------
1 | //! This will create an entire book in a temporary directory using some
2 | //! dummy contents from the `tests/dummy-book/` directory.
3 |
4 | // Not all features are used in all test crates, so...
5 | #![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
6 |
7 | use anyhow::Context;
8 | use mdbook::errors::*;
9 | use mdbook::MDBook;
10 | use std::fs::{self, File};
11 | use std::io::{Read, Write};
12 | use std::path::Path;
13 | use tempfile::{Builder as TempFileBuilder, TempDir};
14 | use walkdir::WalkDir;
15 |
16 | /// Create a dummy book in a temporary directory, using the contents of
17 | /// `SUMMARY_MD` as a guide.
18 | ///
19 | /// The "Nested Chapter" file contains a code block with a single
20 | /// `assert!($TEST_STATUS)`. If you want to check MDBook's testing
21 | /// functionality, `$TEST_STATUS` can be substitute for either `true` or
22 | /// `false`. This is done using the `passing_test` parameter.
23 | #[derive(Clone, Debug, PartialEq)]
24 | pub struct DummyBook {
25 | passing_test: bool,
26 | }
27 |
28 | impl DummyBook {
29 | /// Create a new `DummyBook` with all the defaults.
30 | pub fn new() -> DummyBook {
31 | DummyBook { passing_test: true }
32 | }
33 |
34 | /// Whether the doc-test included in the "Nested Chapter" should pass or
35 | /// fail (it passes by default).
36 | pub fn with_passing_test(&mut self, test_passes: bool) -> &mut DummyBook {
37 | self.passing_test = test_passes;
38 | self
39 | }
40 |
41 | /// Write a book to a temporary directory using the provided settings.
42 | pub fn build(&self) -> Result {
43 | let temp = TempFileBuilder::new()
44 | .prefix("dummy_book-")
45 | .tempdir()
46 | .with_context(|| "Unable to create temp directory")?;
47 |
48 | let dummy_book_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/dummy_book");
49 | recursive_copy(&dummy_book_root, temp.path()).with_context(|| {
50 | "Couldn't copy files into a \
51 | temporary directory"
52 | })?;
53 |
54 | let sub_pattern = if self.passing_test { "true" } else { "false" };
55 | let files_containing_tests = [
56 | "src/first/nested.md",
57 | "src/first/nested-test.rs",
58 | "src/first/nested-test-with-anchors.rs",
59 | "src/first/partially-included-test.rs",
60 | "src/first/partially-included-test-with-anchors.rs",
61 | ];
62 | for file in &files_containing_tests {
63 | let path_containing_tests = temp.path().join(file);
64 | replace_pattern_in_file(&path_containing_tests, "$TEST_STATUS", sub_pattern)?;
65 | }
66 |
67 | Ok(temp)
68 | }
69 | }
70 |
71 | fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()> {
72 | let contents = fs::read_to_string(filename)?;
73 | File::create(filename)?.write_all(contents.replace(from, to).as_bytes())?;
74 |
75 | Ok(())
76 | }
77 |
78 | /// Read the contents of the provided file into memory and then iterate through
79 | /// the list of strings asserting that the file contains all of them.
80 | pub fn assert_contains_strings>(filename: P, strings: &[&str]) {
81 | let filename = filename.as_ref();
82 | let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
83 |
84 | for s in strings {
85 | assert!(
86 | content.contains(s),
87 | "Searching for {:?} in {}\n\n{}",
88 | s,
89 | filename.display(),
90 | content
91 | );
92 | }
93 | }
94 |
95 | pub fn assert_doesnt_contain_strings>(filename: P, strings: &[&str]) {
96 | let filename = filename.as_ref();
97 | let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
98 |
99 | for s in strings {
100 | assert!(
101 | !content.contains(s),
102 | "Found {:?} in {}\n\n{}",
103 | s,
104 | filename.display(),
105 | content
106 | );
107 | }
108 | }
109 |
110 | /// Recursively copy an entire directory tree to somewhere else (a la `cp -r`).
111 | fn recursive_copy, B: AsRef>(from: A, to: B) -> Result<()> {
112 | let from = from.as_ref();
113 | let to = to.as_ref();
114 |
115 | for entry in WalkDir::new(&from) {
116 | let entry = entry.with_context(|| "Unable to inspect directory entry")?;
117 |
118 | let original_location = entry.path();
119 | let relative = original_location
120 | .strip_prefix(&from)
121 | .expect("`original_location` is inside the `from` directory");
122 | let new_location = to.join(relative);
123 |
124 | if original_location.is_file() {
125 | if let Some(parent) = new_location.parent() {
126 | fs::create_dir_all(parent).with_context(|| "Couldn't create directory")?;
127 | }
128 |
129 | fs::copy(&original_location, &new_location)
130 | .with_context(|| "Unable to copy file contents")?;
131 | }
132 | }
133 |
134 | Ok(())
135 | }
136 |
137 | pub fn new_copy_of_example_book() -> Result {
138 | let temp = TempFileBuilder::new().prefix("guide").tempdir()?;
139 |
140 | let guide = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
141 |
142 | recursive_copy(guide, temp.path())?;
143 |
144 | Ok(temp)
145 | }
146 |
--------------------------------------------------------------------------------
/src/cmd/watch.rs:
--------------------------------------------------------------------------------
1 | use crate::{get_book_dir, open};
2 | use clap::{arg, App, Arg, ArgMatches};
3 | use mdbook::errors::Result;
4 | use mdbook::utils;
5 | use mdbook::MDBook;
6 | use notify::Watcher;
7 | use std::path::{Path, PathBuf};
8 | use std::sync::mpsc::channel;
9 | use std::thread::sleep;
10 | use std::time::Duration;
11 |
12 | // Create clap subcommand arguments
13 | pub fn make_subcommand<'help>() -> App<'help> {
14 | App::new("watch")
15 | .about("Watches a book's files and rebuilds it on changes")
16 | .arg(
17 | Arg::new("dest-dir")
18 | .short('d')
19 | .long("dest-dir")
20 | .value_name("dest-dir")
21 | .help(
22 | "Output directory for the book{n}\
23 | Relative paths are interpreted relative to the book's root directory.{n}\
24 | If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
25 | ),
26 | )
27 | .arg(arg!([dir]
28 | "Root directory for the book{n}\
29 | (Defaults to the Current Directory when omitted)"
30 | ))
31 | .arg(arg!(-o --open "Opens the compiled book in a web browser"))
32 | }
33 |
34 | // Watch command implementation
35 | pub fn execute(args: &ArgMatches) -> Result<()> {
36 | let book_dir = get_book_dir(args);
37 | let mut book = MDBook::load(&book_dir)?;
38 |
39 | let update_config = |book: &mut MDBook| {
40 | if let Some(dest_dir) = args.value_of("dest-dir") {
41 | book.config.build.build_dir = dest_dir.into();
42 | }
43 | };
44 | update_config(&mut book);
45 |
46 | if args.is_present("open") {
47 | book.build()?;
48 | open(book.build_dir_for("html").join("index.html"));
49 | }
50 |
51 | trigger_on_change(&book, |paths, book_dir| {
52 | info!("Files changed: {:?}\nBuilding book...\n", paths);
53 | let result = MDBook::load(&book_dir).and_then(|mut b| {
54 | update_config(&mut b);
55 | b.build()
56 | });
57 |
58 | if let Err(e) = result {
59 | error!("Unable to build the book");
60 | utils::log_backtrace(&e);
61 | }
62 | });
63 |
64 | Ok(())
65 | }
66 |
67 | fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec {
68 | if paths.is_empty() {
69 | return vec![];
70 | }
71 |
72 | match find_gitignore(book_root) {
73 | Some(gitignore_path) => {
74 | match gitignore::File::new(gitignore_path.as_path()) {
75 | Ok(exclusion_checker) => filter_ignored_files(exclusion_checker, paths),
76 | Err(_) => {
77 | // We're unable to read the .gitignore file, so we'll silently allow everything.
78 | // Please see discussion: https://github.com/rust-lang/mdBook/pull/1051
79 | paths.iter().map(|path| path.to_path_buf()).collect()
80 | }
81 | }
82 | }
83 | None => {
84 | // There is no .gitignore file.
85 | paths.iter().map(|path| path.to_path_buf()).collect()
86 | }
87 | }
88 | }
89 |
90 | fn find_gitignore(book_root: &Path) -> Option {
91 | book_root
92 | .ancestors()
93 | .map(|p| p.join(".gitignore"))
94 | .find(|p| p.exists())
95 | }
96 |
97 | fn filter_ignored_files(exclusion_checker: gitignore::File, paths: &[PathBuf]) -> Vec {
98 | paths
99 | .iter()
100 | .filter(|path| match exclusion_checker.is_excluded(path) {
101 | Ok(exclude) => !exclude,
102 | Err(error) => {
103 | warn!(
104 | "Unable to determine if {:?} is excluded: {:?}. Including it.",
105 | &path, error
106 | );
107 | true
108 | }
109 | })
110 | .map(|path| path.to_path_buf())
111 | .collect()
112 | }
113 |
114 | /// Calls the closure when a book source file is changed, blocking indefinitely.
115 | pub fn trigger_on_change(book: &MDBook, closure: F)
116 | where
117 | F: Fn(Vec, &Path),
118 | {
119 | use notify::DebouncedEvent::*;
120 | use notify::RecursiveMode::*;
121 |
122 | // Create a channel to receive the events.
123 | let (tx, rx) = channel();
124 |
125 | let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) {
126 | Ok(w) => w,
127 | Err(e) => {
128 | error!("Error while trying to watch the files:\n\n\t{:?}", e);
129 | std::process::exit(1)
130 | }
131 | };
132 |
133 | // Add the source directory to the watcher
134 | if let Err(e) = watcher.watch(book.source_dir(), Recursive) {
135 | error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
136 | std::process::exit(1);
137 | };
138 |
139 | let _ = watcher.watch(book.theme_dir(), Recursive);
140 |
141 | // Add the book.toml file to the watcher if it exists
142 | let _ = watcher.watch(book.root.join("book.toml"), NonRecursive);
143 |
144 | info!("Listening for changes...");
145 |
146 | loop {
147 | let first_event = rx.recv().unwrap();
148 | sleep(Duration::from_millis(50));
149 | let other_events = rx.try_iter();
150 |
151 | let all_events = std::iter::once(first_event).chain(other_events);
152 |
153 | let paths = all_events
154 | .filter_map(|event| {
155 | debug!("Received filesystem event: {:?}", event);
156 |
157 | match event {
158 | Create(path) | Write(path) | Remove(path) | Rename(_, path) => Some(path),
159 | _ => None,
160 | }
161 | })
162 | .collect::>();
163 |
164 | let paths = remove_ignored_files(&book.root, &paths[..]);
165 |
166 | if !paths.is_empty() {
167 | closure(paths, &book.root);
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/test_book/src/individual/paragraph.md:
--------------------------------------------------------------------------------
1 | Just a simple paragraph.
2 |
3 | Let's stress test this.
4 |
5 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer elit lorem, eleifend eu leo sit amet, suscipit feugiat libero. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Proin congue lectus sit amet lacus venenatis, ac sollicitudin purus condimentum. Suspendisse pretium volutpat sapien at gravida. In tincidunt, sem non accumsan consectetur, leo libero porttitor dolor, at imperdiet erat nibh quis leo. Cras dictum erat augue, quis pharetra justo porttitor posuere. Aenean sed lacinia justo, vel suscipit nisl. Etiam eleifend id mauris at gravida. Aliquam molestie cursus lorem pulvinar sollicitudin. Nam et ex dignissim, posuere sem non, pellentesque lacus. Morbi vulputate sed lorem et convallis. Duis non turpis eget elit posuere volutpat. Donec accumsan euismod enim, id consequat ex rhoncus ac. Pellentesque ac felis nisl. Duis imperdiet vel tellus ac iaculis.
6 |
7 | Vivamus nec tempus enim. Integer in ligula eget elit ornare vulputate id et est. Proin mi elit, sagittis nec urna et, iaculis imperdiet neque. Vestibulum placerat cursus dolor. Donec eu sodales nulla. Praesent ac tellus eros. Donec venenatis ligula id ex porttitor malesuada. Aliquam maximus, nisi in fringilla finibus, ante elit rhoncus dui, placerat semper nisl tellus quis odio. Cras luctus magna ultrices dolor pharetra volutpat. Maecenas non enim vitae ligula efficitur aliquet id quis quam. In sagittis mollis magna eu porta. Morbi at nulla et ante elementum pharetra in sed est. Nam commodo purus enim.
8 |
9 | Ut non elit sit amet urna luctus facilisis vel et sapien. Morbi nec metus at libero imperdiet sollicitudin eget quis lacus. Donec in ipsum at enim accumsan tempor vel sed magna. Aliquam non imperdiet neque. Etiam pharetra neque sed pretium interdum. Suspendisse potenti. Phasellus varius, lectus quis dapibus faucibus, purus mauris accumsan nibh, vel tempor quam metus nec sem. Nunc sagittis suscipit lorem eu finibus. Nullam augue leo, imperdiet vel diam et, vulputate scelerisque turpis. Nullam ut volutpat diam. Praesent cursus accumsan dui a commodo. Vivamus sed libero sed turpis facilisis rutrum id sed ligula. Ut id sollicitudin dui. Nulla pulvinar commodo lectus. Cras ut quam congue, consectetur dolor ac, consequat ante.
10 |
11 | Curabitur scelerisque sed leo eu facilisis. Nam faucibus neque eget dictum hendrerit. Duis efficitur ex sed vulputate volutpat. Praesent condimentum nisl ac sapien efficitur laoreet. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Ut ut nibh elit. Nunc a neque lobortis, tempus diam vitae, interdum magna. Aenean eget nisl sed justo volutpat interdum. Mauris malesuada ex nisl, a dignissim dui elementum eget. Suspendisse potenti.
12 |
13 | Praesent congue fringilla sem sed faucibus. Vivamus malesuada eget mauris at molestie. In sed faucibus nulla. Vivamus elementum accumsan metus quis suscipit. Maecenas interdum est nulla. Cras volutpat cursus nibh quis sollicitudin. Morbi vitae massa laoreet, aliquet tellus quis, consectetur ipsum. Mauris euismod congue purus non condimentum. Etiam laoreet mi vel sem consectetur gravida. Vestibulum volutpat magna nunc, vitae ultrices risus commodo eu.
14 |
15 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer elit lorem, eleifend eu leo sit amet, suscipit feugiat libero. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Proin congue lectus sit amet lacus venenatis, ac sollicitudin purus condimentum. Suspendisse pretium volutpat sapien at gravida. In tincidunt, sem non accumsan consectetur, leo libero porttitor dolor, at imperdiet erat nibh quis leo. Cras dictum erat augue, quis pharetra justo porttitor posuere. Aenean sed lacinia justo, vel suscipit nisl. Etiam eleifend id mauris at gravida. Aliquam molestie cursus lorem pulvinar sollicitudin. Nam et ex dignissim, posuere sem non, pellentesque lacus. Morbi vulputate sed lorem et convallis. Duis non turpis eget elit posuere volutpat. Donec accumsan euismod enim, id consequat ex rhoncus ac. Pellentesque ac felis nisl. Duis imperdiet vel tellus ac iaculis.
16 |
17 | Vivamus nec tempus enim. Integer in ligula eget elit ornare vulputate id et est. Proin mi elit, sagittis nec urna et, iaculis imperdiet neque. Vestibulum placerat cursus dolor. Donec eu sodales nulla. Praesent ac tellus eros. Donec venenatis ligula id ex porttitor malesuada. Aliquam maximus, nisi in fringilla finibus, ante elit rhoncus dui, placerat semper nisl tellus quis odio. Cras luctus magna ultrices dolor pharetra volutpat. Maecenas non enim vitae ligula efficitur aliquet id quis quam. In sagittis mollis magna eu porta. Morbi at nulla et ante elementum pharetra in sed est. Nam commodo purus enim.
18 |
19 | Ut non elit sit amet urna luctus facilisis vel et sapien. Morbi nec metus at libero imperdiet sollicitudin eget quis lacus. Donec in ipsum at enim accumsan tempor vel sed magna. Aliquam non imperdiet neque. Etiam pharetra neque sed pretium interdum. Suspendisse potenti. Phasellus varius, lectus quis dapibus faucibus, purus mauris accumsan nibh, vel tempor quam metus nec sem. Nunc sagittis suscipit lorem eu finibus. Nullam augue leo, imperdiet vel diam et, vulputate scelerisque turpis. Nullam ut volutpat diam. Praesent cursus accumsan dui a commodo. Vivamus sed libero sed turpis facilisis rutrum id sed ligula. Ut id sollicitudin dui. Nulla pulvinar commodo lectus. Cras ut quam congue, consectetur dolor ac, consequat ante.
20 |
21 | Curabitur scelerisque sed leo eu facilisis. Nam faucibus neque eget dictum hendrerit. Duis efficitur ex sed vulputate volutpat. Praesent condimentum nisl ac sapien efficitur laoreet. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Ut ut nibh elit. Nunc a neque lobortis, tempus diam vitae, interdum magna. Aenean eget nisl sed justo volutpat interdum. Mauris malesuada ex nisl, a dignissim dui elementum eget. Suspendisse potenti.
22 |
23 | Praesent congue fringilla sem sed faucibus. Vivamus malesuada eget mauris at molestie. In sed faucibus nulla. Vivamus elementum accumsan metus quis suscipit. Maecenas interdum est nulla. Cras volutpat cursus nibh quis sollicitudin. Morbi vitae massa laoreet, aliquet tellus quis, consectetur ipsum. Mauris euismod congue purus non condimentum. Etiam laoreet mi vel sem consectetur gravida. Vestibulum volutpat magna nunc, vitae ultrices risus commodo eu.
24 |
25 | Hopefully everything above was rendered nicely, on both desktop and mobile.
26 |
--------------------------------------------------------------------------------
/guide/src/continuous-integration.md:
--------------------------------------------------------------------------------
1 | # Running `mdbook` in Continuous Integration
2 |
3 | There are a variety of services such as [GitHub Actions] or [GitLab CI/CD] which can be used to test and deploy your book automatically.
4 |
5 | The following provides some general guidelines on how to configure your service to run mdBook.
6 | Specific recipes can be found at the [Automated Deployment] wiki page.
7 |
8 | [GitHub Actions]: https://docs.github.com/en/actions
9 | [GitLab CI/CD]: https://docs.gitlab.com/ee/ci/
10 | [Automated Deployment]: https://github.com/rust-lang/mdBook/wiki/Automated-Deployment
11 |
12 | ## Installing mdBook
13 |
14 | There are several different strategies for installing mdBook.
15 | The particular method depends on your needs and preferences.
16 |
17 | ### Pre-compiled binaries
18 |
19 | Perhaps the easiest method is to use the pre-compiled binaries found on the [GitHub Releases page][releases].
20 | A simple approach would be to use the popular `curl` CLI tool to download the executable:
21 |
22 | ```sh
23 | mkdir bin
24 | curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.17/mdbook-v0.4.17-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
25 | bin/mdbook build
26 | ```
27 |
28 | Some considerations for this approach:
29 |
30 | * This is relatively fast, and does not necessarily require dealing with caching.
31 | * This does not require installing Rust.
32 | * Specifying a specific URL means you have to manually update your script to get a new version.
33 | This may be a benefit if you want to lock to a specific version.
34 | However, some users prefer to automatically get a newer version when they are published.
35 | * You are reliant on the GitHub CDN being available.
36 |
37 | [releases]: https://github.com/rust-lang/mdBook/releases
38 |
39 | ### Building from source
40 |
41 | Building from source will require having Rust installed.
42 | Some services have Rust pre-installed, but if your service does not, you will need to add a step to install it.
43 |
44 | After Rust is installed, `cargo install` can be used to build and install mdBook.
45 | We recommend using a SemVer version specifier so that you get the latest **non-breaking** version of mdBook.
46 | For example:
47 |
48 | ```sh
49 | cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
50 | ```
51 |
52 | This includes several recommended options:
53 |
54 | * `--no-default-features` — Disables features like the HTTP server used by `mdbook serve` that is likely not needed on CI.
55 | This will speed up the build time significantly.
56 | * `--features search` — Disabling default features means you should then manually enable features that you want, such as the built-in [search] capability.
57 | * `--vers "^0.4"` — This will install the most recent version of the `0.4` series.
58 | However, versions after like `0.5.0` won't be installed, as they may break your build.
59 | Cargo will automatically upgrade mdBook if you have an older version already installed.
60 | * `--locked` — This will use the dependencies that were used when mdBook was released.
61 | Without `--locked`, it will use the latest version of all dependencies, which may include some fixes since the last release, but may also (rarely) cause build problems.
62 |
63 | You will likely want to investigate caching options, as building mdBook can be somewhat slow.
64 |
65 | [search]: guide/reading.md#search
66 |
67 | ## Running tests
68 |
69 | You may want to run tests using [`mdbook test`] every time you push a change or create a pull request.
70 | This can be used to validate Rust code examples in the book.
71 |
72 | This will require having Rust installed.
73 | Some services have Rust pre-installed, but if your service does not, you will need to add a step to install it.
74 |
75 | Other than making sure the appropriate version of Rust is installed, there's not much more than just running `mdbook test` from the book directory.
76 |
77 | You may also want to consider running other kinds of tests, like [mdbook-linkcheck] which will check for broken links.
78 | Or if you have your own style checks, spell checker, or any other tests it might be good to run them in CI.
79 |
80 | [`mdbook test`]: cli/test.md
81 | [mdbook-linkcheck]: https://github.com/Michael-F-Bryan/mdbook-linkcheck#continuous-integration
82 |
83 | ## Deploying
84 |
85 | You may want to automatically deploy your book.
86 | Some may want to do this with every time a change is pushed, and others may want to only deploy when a specific release is tagged.
87 |
88 | You'll also need to understand the specifics on how to push a change to your web service.
89 | For example, [GitHub Pages] just requires committing the output onto a specific git branch.
90 | Other services may require using something like SSH to connect to a remote server.
91 |
92 | The basic outline is that you need to run `mdbook build` to generate the output, and then transfer the files (which are in the `book` directory) to the correct location.
93 |
94 | You may then want to consider if you need to invalidate any caches on your web service.
95 |
96 | See the [Automated Deployment] wiki page for examples of various different services.
97 |
98 | [GitHub Pages]: https://docs.github.com/en/pages
99 |
100 | ### 404 handling
101 |
102 | mdBook automatically generates a 404 page to be used for broken links.
103 | The default output is a file named `404.html` at the root of the book.
104 | Some services like [GitHub Pages] will automatically use this page for broken links.
105 | For other services, you may want to consider configuring the web server to use this page as it will provide the reader navigation to get back to the book.
106 |
107 | If your book is not deployed at the root of the domain, then you should set the [`output.html.site-url`] setting so that the 404 page works correctly.
108 | It needs to know where the book is deployed in order to load the static files (like CSS) correctly.
109 | For example, this guide is deployed at , and the `site-url` setting is configured like this:
110 |
111 | ```toml
112 | # book.toml
113 | [output.html]
114 | site-url = "/mdBook/"
115 | ```
116 |
117 | You can customize the look of the 404 page by creating a file named `src/404.md` in your book.
118 | If you want to use a different filename, you can set [`output.html.input-404`] to a different filename.
119 |
120 | [`output.html.site-url`]: format/configuration/renderers.md#html-renderer-options
121 | [`output.html.input-404`]: format/configuration/renderers.md#html-renderer-options
122 |
--------------------------------------------------------------------------------
/guide/src/for_developers/preprocessors.md:
--------------------------------------------------------------------------------
1 | # Preprocessors
2 |
3 | A *preprocessor* is simply a bit of code which gets run immediately after the
4 | book is loaded and before it gets rendered, allowing you to update and mutate
5 | the book. Possible use cases are:
6 |
7 | - Creating custom helpers like `\{{#include /path/to/file.md}}`
8 | - Substituting in latex-style expressions (`$$ \frac{1}{3} $$`) with their
9 | mathjax equivalents
10 |
11 | See [Configuring Preprocessors](../format/configuration/preprocessors.md) for more information about using preprocessors.
12 |
13 | ## Hooking Into MDBook
14 |
15 | MDBook uses a fairly simple mechanism for discovering third party plugins.
16 | A new table is added to `book.toml` (e.g. `[preprocessor.foo]` for the `foo`
17 | preprocessor) and then `mdbook` will try to invoke the `mdbook-foo` program as
18 | part of the build process.
19 |
20 | Once the preprocessor has been defined and the build process starts, mdBook executes the command defined in the `preprocessor.foo.command` key twice.
21 | The first time it runs the preprocessor to determine if it supports the given renderer.
22 | mdBook passes two arguments to the process: the first argument is the string `supports` and the second argument is the renderer name.
23 | The preprocessor should exit with a status code 0 if it supports the given renderer, or return a non-zero exit code if it does not.
24 |
25 | If the preprocessor supports the renderer, then mdbook runs it a second time, passing JSON data into stdin.
26 | The JSON consists of an array of `[context, book]` where `context` is the serialized object [`PreprocessorContext`] and `book` is a [`Book`] object containing the content of the book.
27 |
28 | The preprocessor should return the JSON format of the [`Book`] object to stdout, with any modifications it wishes to perform.
29 |
30 | The easiest way to get started is by creating your own implementation of the
31 | `Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
32 | translates inputs to the correct `Preprocessor` method. For convenience, there
33 | is [an example no-op preprocessor] in the `examples/` directory which can easily
34 | be adapted for other preprocessors.
35 |
36 |
37 | Example no-op preprocessor
38 |
39 | ```rust
40 | // nop-preprocessors.rs
41 |
42 | {{#include ../../../examples/nop-preprocessor.rs}}
43 | ```
44 |
45 |
46 | ## Hints For Implementing A Preprocessor
47 |
48 | By pulling in `mdbook` as a library, preprocessors can have access to the
49 | existing infrastructure for dealing with books.
50 |
51 | For example, a custom preprocessor could use the
52 | [`CmdPreprocessor::parse_input()`] function to deserialize the JSON written to
53 | `stdin`. Then each chapter of the `Book` can be mutated in-place via
54 | [`Book::for_each_mut()`], and then written to `stdout` with the `serde_json`
55 | crate.
56 |
57 | Chapters can be accessed either directly (by recursively iterating over
58 | chapters) or via the `Book::for_each_mut()` convenience method.
59 |
60 | The `chapter.content` is just a string which happens to be markdown. While it's
61 | entirely possible to use regular expressions or do a manual find & replace,
62 | you'll probably want to process the input into something more computer-friendly.
63 | The [`pulldown-cmark`][pc] crate implements a production-quality event-based
64 | Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] crate allowing you to
65 | translate events back into markdown text.
66 |
67 | The following code block shows how to remove all emphasis from markdown,
68 | without accidentally breaking the document.
69 |
70 | ```rust
71 | fn remove_emphasis(
72 | num_removed_items: &mut usize,
73 | chapter: &mut Chapter,
74 | ) -> Result {
75 | let mut buf = String::with_capacity(chapter.content.len());
76 |
77 | let events = Parser::new(&chapter.content).filter(|e| {
78 | let should_keep = match *e {
79 | Event::Start(Tag::Emphasis)
80 | | Event::Start(Tag::Strong)
81 | | Event::End(Tag::Emphasis)
82 | | Event::End(Tag::Strong) => false,
83 | _ => true,
84 | };
85 | if !should_keep {
86 | *num_removed_items += 1;
87 | }
88 | should_keep
89 | });
90 |
91 | cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
92 | Error::from(format!("Markdown serialization failed: {}", err))
93 | })
94 | }
95 | ```
96 |
97 | For everything else, have a look [at the complete example][example].
98 |
99 | ## Implementing a preprocessor with a different language
100 |
101 | The fact that mdBook utilizes stdin and stdout to communicate with the preprocessors makes it easy to implement them in a language other than Rust.
102 | The following code shows how to implement a simple preprocessor in Python, which will modify the content of the first chapter.
103 | The example below follows the configuration shown above with `preprocessor.foo.command` actually pointing to a Python script.
104 |
105 | ```python
106 | import json
107 | import sys
108 |
109 |
110 | if __name__ == '__main__':
111 | if len(sys.argv) > 1: # we check if we received any argument
112 | if sys.argv[1] == "supports":
113 | # then we are good to return an exit status code of 0, since the other argument will just be the renderer's name
114 | sys.exit(0)
115 |
116 | # load both the context and the book representations from stdin
117 | context, book = json.load(sys.stdin)
118 | # and now, we can just modify the content of the first chapter
119 | book['sections'][0]['Chapter']['content'] = '# Hello'
120 | # we are done with the book's modification, we can just print it to stdout,
121 | print(json.dumps(book))
122 | ```
123 |
124 |
125 |
126 | [preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
127 | [pc]: https://crates.io/crates/pulldown-cmark
128 | [pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
129 | [example]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
130 | [an example no-op preprocessor]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
131 | [`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input
132 | [`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut
133 | [`PreprocessorContext`]: https://docs.rs/mdbook/latest/mdbook/preprocess/struct.PreprocessorContext.html
134 | [`Book`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html
135 |
--------------------------------------------------------------------------------