Compare commits

...

79 Commits

Author SHA1 Message Date
Steve Klabnik
6cbc41d413 version 0.2.3 2019-01-18 10:39:51 -05:00
Steve Klabnik
25c1ca1275 Merge pull request #866 from rust-lang-nursery/gh828
Fixing links in print.html
2019-01-18 09:54:17 -05:00
Steve Klabnik
acbb951240 Merge pull request #865 from sderosiaux/patch-1
Fix websocket hostname usage
2019-01-17 15:08:18 -05:00
Steve Klabnik
9e96165d8f Merge pull request #845 from phansch/update_contributing
Update CONTRIBUTING.md (Clippy is on stable now)
2019-01-17 15:07:21 -05:00
Steve Klabnik
5c5ef2f86b Merge pull request #853 from sshplendid/broken-link
Update broken link at the user guide.
2019-01-17 15:06:45 -05:00
Steve Klabnik
23ac06e2eb Fix a bug in link re-writing
In a regex, `.` means `[0-9a-zA-Z]`, not a period character. This means
that this regex worked, unless a link anchor contained `md` inside of
it. This fixes the regex to match a literal period.

Reported by @ehuss in
https://github.com/rust-lang-nursery/mdBook/pull/866#issuecomment-454595806
2019-01-16 15:44:51 -05:00
Steve Klabnik
2ddbb37f49 Fix the bug 2019-01-15 14:10:02 -05:00
Steve Klabnik
a481735fa2 failing test 2019-01-15 14:08:53 -05:00
Stéphane Derosiaux
954cfa86e5 Fix websocket hostname usage
The livereload url was using an unknown property "websocket-address" instead of "websocket-hostname", hence it was always fallback onto the hostname (which can be different).
2019-01-09 17:59:45 +01:00
Shawn
b675b91980 Update broken link
https://www.rust-lang.org/downloads.html is the outdated link.
2018-12-15 10:09:23 +09:00
Philipp Hansch
eb19d2d654 Update CONTRIBUTING.md (Clippy is on stable now) 2018-12-08 13:48:59 +01:00
Michael Bryan
1052ee92e1 Merge pull request #837 from basbossink/master
Random acts of kindness
2018-12-08 15:44:40 +08:00
Bas Bossink
3598e905aa Make failing_alternate_backend test more platform specific
Use the suggestion from @Michael-F-Bryan to make the passing_ and
failing_alternate_backend test more reliable across platforms.
2018-12-05 22:26:53 +01:00
Bas Bossink
3f002979c4 Suppress dead_code warning in test 2018-12-04 00:57:15 +01:00
Bas Bossink
742dbbc917 Run rustfmt. 2018-12-04 00:11:41 +01:00
Bas Bossink
991a725c26 Solve the simplest clippy warnings and run rustfmt 2018-12-04 00:10:09 +01:00
Michael Bryan
005dfc55bf Merge pull request #829 from kingdido999/patch-1
Fix invalid url in renderer documentation
2018-11-22 20:12:19 +08:00
Desmond
8c86031384 Fix invalid url in renderer documentation 2018-11-21 08:59:53 +08:00
Michael Bryan
42b87e0fbc Merge pull request #804 from Bassetts/default-theme-option
Default theme option
2018-10-30 21:18:48 +08:00
Matt Ickstadt
33add4b532 Merge pull request #782 from mattico/document-dest-dir-rel
Document dest-dir relative path behavior
2018-10-23 12:16:37 -05:00
Michael Bryan
b0513ee771 Merge pull request #788 from weihanglo/feat/non-ascii-heading-anchor
Allow non alphabetic initial in heading anchor
2018-10-23 19:21:44 +08:00
Michael Bryan
b4538da9c3 Merge pull request #802 from Bassetts/git-button
Implement a git repository button
2018-10-23 19:16:45 +08:00
Michael Bryan
7ac3e50b37 Merge pull request #809 from hikarinotomadoi/fix-documentation-comments-for-HtmlConfig
Improve documentation comments
2018-10-23 19:15:51 +08:00
yoshimura masataka
13a9aab2b2 Improve documentation comments 2018-10-23 10:34:14 +09:00
Jason Liquorish
eccec9bb52 Update documentation for 2018-10-21 13:16:59 +01:00
Michael Bryan
e63f53fe47 (cargo-release) start next development iteration 0.2.3-alpha.0 2018-10-20 14:22:47 +08:00
Michael Bryan
2c20c99d4a (cargo-release) version 0.2.2 2018-10-20 14:21:21 +08:00
Michael Bryan
c6125b184f Merge pull request #807 from rust-lang-nursery/update-readme
Update the readme to mention plugins
2018-10-20 14:17:57 +08:00
Michael Bryan
dfb6e3cb10 Merge pull request #806 from rust-lang-nursery/serialize-checks
Add some round-trip checks for preprocessor input serialization
2018-10-20 14:17:03 +08:00
Michael Bryan
cffc385b0c Updated the user guide's config section to mention specifying plugin commands 2018-10-20 14:16:07 +08:00
Michael Bryan
e73928f933 Mentioned plugins in the README 2018-10-20 14:01:51 +08:00
Michael Bryan
41071a5dd9 Bumped dependencies 2018-10-20 12:51:45 +08:00
Michael Bryan
f6a7432569 Added a round-trip test to make sure parse_input() is always correct 2018-10-20 12:50:35 +08:00
Michael Bryan
89ea60e7a5 Made __non_exhaustive fields #[serde(skip)] 2018-10-20 11:21:24 +08:00
Jason Liquorish
10b69e60c8 Add documentation and tests 2018-10-15 21:40:59 +01:00
Jason Liquorish
336e08fe50 Update FontAwesome version to 4.7.0 2018-10-15 20:01:36 +01:00
Jason Liquorish
5bfdf9fcc8 Added git-repository-icon option
Updated documentation and added tests.
2018-10-15 19:48:54 +01:00
Michael Bryan
29f8b791f1 Merge pull request #792 from rust-lang-nursery/custom-preprocessor
WIP: Custom Preprocessors
2018-10-16 00:02:12 +08:00
xyh
877bf37d18 avoid using cd in example travis-ci script (#803) 2018-10-15 18:52:37 +08:00
Jason Liquorish
d2565af000 Add helper to format theme name for theme changer 2018-10-13 14:44:10 +01:00
Jason Liquorish
599e47f1f1 Initial implementation of a git repository button 2018-10-13 12:17:33 +01:00
Jason Liquorish
0c31ab2953 Initial implementation of default theme option 2018-10-12 19:57:59 +01:00
Michael Bryan
b1c7c54108 Rewrote a large proportion of the Preprocessor docs to be up-to-date 2018-09-25 19:48:20 +08:00
Jan-Erik Rediger
0c926b3e88 Ensure section numbers are correctly incremented after a horizontal separator (#790)
Fixes #779
2018-09-19 23:33:28 +08:00
mwilbur
e4eddb3f26 Fix broken link to API pages (#795) 2018-09-19 23:32:37 +08:00
Michael Bryan
adec78e7f5 Forgot to implement 3rd party preprocessor discovery 2018-09-19 23:16:11 +08:00
Michael Bryan
5cd5e4764c Fleshed out the api docs 2018-09-16 23:44:52 +08:00
Michael Bryan
132f4fd358 Fixed a bug where the tests use the wrong dummy book 2018-09-16 23:33:58 +08:00
Michael Bryan
1d72cea972 The example preprocessor works 2018-09-16 23:28:01 +08:00
Michael Bryan
1aa1194d79 We can shell out to the preprocessor 2018-09-16 23:23:03 +08:00
Michael Bryan
304234c122 The example can now tell mdbook if renderers are supported 2018-09-16 23:00:19 +08:00
Michael Bryan
729c94a7e4 Started working on a custom preprocessor 2018-09-16 22:49:52 +08:00
Ramon Buckland
df874cdbdb Resolves #270. details for Clippy and Rustmft usage (#776)
* Resolves #270. details for Clippy and Rustmft usage

* Fix Typo
2018-09-16 14:39:18 +08:00
Michael Bryan
5dce539928 Notify preprocessors of the mdbook version and add __non_exhaustive elements 2018-09-16 14:27:37 +08:00
Michael Bryan
206a00915b Export the mdbook version from the crate root 2018-09-16 14:22:50 +08:00
Ramon Buckland
ced74ca4dd Updated the documentation for new preprocessor format (#787)
* Updated the documentation for new preprocessor format

* adjusted the descriptions for preprocessors
2018-09-10 19:51:18 +08:00
Michael Bryan
09667c9956 Configurable preprocessor (#658)
* The preprocessor trait now returns a modified book instead of editing in place

* A preprocessor is told which render it's running for

* Made sure preprocessors get their renderer's name

* Users can now manually specify whether a preprocessor should run for a renderer

* You can normally use default preprocessors by default

* Got my logic around the wrong way

* Fixed the `build.use-default-preprocessors` flag
2018-09-10 18:55:58 +08:00
Weihang Lo
d729a762fe Remove insertion on non alphabetic initial headings 2018-09-09 12:00:28 +08:00
Weihang Lo
43b3d157d9 (test) validate id from non ascii headings 2018-09-09 12:00:25 +08:00
Matt Ickstadt
a9f3be6f44 Make serve command note more prominent 2018-09-06 10:24:56 -05:00
Matt Ickstadt
34356b87a0 Document dest-dir relative path behavior 2018-09-06 10:24:42 -05:00
Matt Ickstadt
48c97dadd0 Merge pull request #777 from wirelyre/fix-additional-css-and-js
Fix paths to additional CSS and JavaScript files
2018-09-04 14:16:42 -04:00
wirelyre
65198a7632 Fix paths to additional CSS and JavaScript files
Expressions in an `#each` block need to begin with "../" to reference
values in the main context.
2018-08-31 20:03:34 -05:00
Matt Ickstadt
a0e7b19784 (cargo-release) start next development iteration 0.2.2-alpha.0 2018-08-22 08:51:07 -05:00
Matt Ickstadt
7e2e095c26 (cargo-release) version 0.2.1 2018-08-22 08:48:25 -05:00
Matt Ickstadt
5baaf55abc Merge pull request #770 from mattico/improve-travis
Improve CI
2018-08-22 08:55:06 -05:00
Matt Ickstadt
9157f6e32d Improve CI
- Try to get OSX releases working
- Use LTO for appveyor releases
- Remove unused install.sh
2018-08-21 11:18:20 -05:00
Matt Ickstadt
3688f73052 rustfmt
using rustfmt 0.99.2-nightly
2018-08-21 10:58:44 -05:00
Matt Ickstadt
3e89e8b1bd Update dependencies 2018-08-21 10:51:34 -05:00
Matt Ickstadt
e08fc148b1 Merge pull request #769 from dguo/patch-1
Fix a typo
2018-08-21 09:44:35 -05:00
Danny Guo
d9c1c77aae Fix a typo 2018-08-20 23:48:18 -04:00
Matt Ickstadt
bb2ca4f938 Merge pull request #765 from weihanglo/fix/quote-table-colors
Fix table and quote related color
2018-08-13 11:08:29 -05:00
Weihang Lo
42aded9577 Fix table and quote related color 2018-08-12 02:47:06 +08:00
Matt Ickstadt
7fb2d5437a Merge pull request #761 from sunng87/feature/handlebars-1.0
Update to handlebars 1.0
2018-08-07 15:39:09 -05:00
Ning Sun
4cc3a1333b (feat) update to handlebars 1.0 2018-08-05 15:08:47 +08:00
Matt Ickstadt
322e8fcf77 Rewrap guide markdown at 80 columns
Much of the book was wrapped at around 80 columns, but let's make this consistent.
2018-08-02 21:34:26 -05:00
Matt Ickstadt
a8a460545f Update installation instructions in guide 2018-08-02 21:19:47 -05:00
Matt Ickstadt
1d69ccae48 Run rustfmt
using 0.8.2-stable included in rustc 1.28.0
2018-08-02 20:22:49 -05:00
Matt Ickstadt
762d89ebbf (cargo-release) start next development iteration 0.2.1-alpha.0 2018-08-02 19:20:05 -05:00
74 changed files with 5294 additions and 1799 deletions

View File

@@ -1,5 +1,14 @@
language: rust
rust:
- stable
- beta
- nightly
os:
- linux
- osx
cache:
timeout: 360
cargo: true
@@ -10,16 +19,10 @@ before_cache:
env:
global:
- CRATE_NAME=mdbook
- TARGET=x86_64-unknown-linux-gnu
install:
- sh ci/install.sh
- export PATH=$PATH:$HOME/.cargo/bin
script:
- cargo build --all --no-default-features
- cargo build --verbose
- cargo test --verbose
- cargo test --all
- cargo test --all --no-default-features
before_deploy:
- sh ci/before_deploy.sh

View File

@@ -48,6 +48,54 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
### Code Quality
We love code quality and Rust has some excellent tools to assist you with contributions.
#### Formatting Code with rustfmt
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
This will ensure we have good quality source code that is better for us all to maintain.
[rustfmt](https://github.com/rust-lang-nursery/rustfmt) has a lot more information on the project.
The quick guide is
1. Install it
```
rustup component add rustfmt
```
1. You can now run `rustfmt` on a single file simply by...
```
rustfmt src/path/to/your/file.rs
```
... or you can format the entire project with
```
cargo fmt
```
When run through `cargo` it will format all bin and lib files in the current crate.
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang-nursery/rustfmt)
#### Finding Issues with Clippy
Clippy is a code analyser/linter detecting mistakes, and therfore helps to improve your code.
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will
help us maintain awesome code.
The best documentation can be found over at [rust-clippy](https://github.com/rust-lang-nursery/rust-clippy)
1. To install
```
rustup component add clippy
```
2. Running clippy
```
cargo clippy
```
Clippy has an ever growing list of checks, that are managed in [lint files](https://rust-lang-nursery.github.io/rust-clippy/master/index.html).
### Making a pull-request
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.

763
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "mdbook"
version = "0.2.0"
version = "0.2.3"
authors = [
"Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>",
@@ -17,7 +17,7 @@ exclude = ["book-example/*"]
[dependencies]
clap = "2.24"
chrono = "0.4"
handlebars = "0.32"
handlebars = "1.0"
serde = "1.0"
serde_derive = "1.0"
error-chain = "0.12"
@@ -26,7 +26,7 @@ pulldown-cmark = "0.1.2"
lazy_static = "1.0"
log = "0.4"
env_logger = "0.5"
toml = "0.4"
toml = "0.4.8"
memchr = "2.0"
open = "1.1"
regex = "1.0.0"

View File

@@ -145,6 +145,57 @@ explanation, check out the [User Guide].
Delete directory in which generated book is located.
### 3rd Party Plugins
The way a book is loaded and rendered can be configured by the user via third
party plugins. These plugins are just programs which will be invoked during the
build process and are split into roughly two categories, *preprocessors* and
*renderers*.
Preprocessors are used to transform a book before it is sent to a renderer.
One example would be to replace all occurrences of
`{{#include some_file.ext}}` with the contents of that file. Some existing
preprocessors are:
- `index` - a built-in preprocessor (enabled by default) which will transform
all `README.md` chapters to `index.md` so `foo/README.md` can be accessed via
the url `foo/` when published to a browser
- `links` - a built-in preprocessor (enabled by default) for expanding the
`{{# playpen}}` and `{{# include}}` helpers in a chapter.
Renderers are given the final book so they can do something with it. This is
typically used for, as the name suggests, rendering the document in a particular
format, however there's nothing stopping a renderer from doing static analysis
of a book in order to validate links or run tests. Some existing renderers are:
- `html` - the built-in renderer which will generate a HTML version of the book
- [`linkcheck`] - a backend which will check that all links are valid
- [`epub`] - an experimental EPUB generator
> **Note for Developers:** Feel free to send us a PR if you've developed your
> own plugin and want it mentioned here.
A preprocessor or renderer is enabled by installing the appropriate program and
then mentioning it in the book's `book.toml` file.
```console
$ cargo install mdbook-linkcheck
$ edit book.toml && cat book.toml
[book]
title = "My Awesome Book"
authors = ["Michael-F-Bryan"]
[output.html]
[output.linkcheck] # enable the "mdbook-linkcheck" renderer
$ mdbook build
2018-10-20 13:57:51 [INFO] (mdbook::book): Book building has started
2018-10-20 13:57:51 [INFO] (mdbook::book): Running the html backend
2018-10-20 13:57:53 [INFO] (mdbook::book): Running the linkcheck backend
```
For more information on the plugin system, consult the [User Guide].
### As a library
@@ -189,3 +240,5 @@ All the code in this repository is released under the ***Mozilla Public License
[Rust]: https://www.rust-lang.org/
[CLI docs]: http://rust-lang-nursery.github.io/mdBook/cli/init.html
[master-docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
[`linkcheck`]: https://crates.io/crates/mdbook-linkcheck
[`epub`]: https://crates.io/crates/mdbook-epub

View File

@@ -36,12 +36,12 @@ build: false
# Equivalent to Travis' `script` phase
test_script:
- cargo build --verbose
- cargo test --verbose
- cargo test --all
- cargo test --all --no-default-features
before_deploy:
# Generate artifacts for release
- cargo build --release
- cargo rustc --bin mdbook --release -- -C lto
- mkdir staging
- copy target\release\mdbook.exe staging
- cd staging

View File

@@ -1,15 +1,25 @@
# mdBook
**mdBook** is a command line tool and Rust crate to create books using Markdown files. It's very similar to Gitbook but written in [Rust](http://www.rust-lang.org).
**mdBook** is a command line tool and Rust crate to create books using Markdown
files. It's very similar to Gitbook but written in
[Rust](http://www.rust-lang.org).
What you are reading serves as an example of the output of mdBook and at the same time as a high-level documentation.
What you are reading serves as an example of the output of mdBook and at the
same time as a high-level documentation.
mdBook is free and open source, you can find the source code on [Github](https://github.com/rust-lang-nursery/mdBook). Issues and feature requests can be posted on the [Github Issue tracker](https://github.com/rust-lang-nursery/mdBook/issues).
mdBook is free and open source, you can find the source code on
[GitHub](https://github.com/rust-lang-nursery/mdBook). Issues and feature
requests can be posted on the [GitHub issue
tracker](https://github.com/rust-lang-nursery/mdBook/issues).
## API docs
Alongside this book you can also read the [API docs](https://docs.rs/mdbook/*/mdbook/) generated by Rustdoc if you would like to use mdBook as a crate or write a new renderer and need a more low-level overview.
Alongside this book you can also read the [API
docs](https://docs.rs/mdbook/*/mdbook/) generated by Rustdoc if you would like
to use mdBook as a crate or write a new renderer and need a more low-level
overview.
## License
mdBook, all the source code, is released under the [Mozilla Public License v2.0](https://www.mozilla.org/MPL/2.0/)
mdBook, all the source code, is released under the [Mozilla Public License
v2.0](https://www.mozilla.org/MPL/2.0/).

View File

@@ -1,30 +1,49 @@
# Command Line Tool
mdBook can be used either as a command line tool or a [Rust crate](https://crates.io/crates/mdbook).
Let's focus on the command line tool capabilities first.
mdBook can be used either as a command line tool or a [Rust
crate](https://crates.io/crates/mdbook). Let's focus on the command line tool
capabilities first.
## Install
## Install From Binaries
Precompiled binaries are provided for major platforms on a best-effort basis.
Visit [the releases page](https://github.com/rust-lang-nursery/mdBook/releases)
to download the appropriate version for your platform.
## Install From Source
mdBook can also be installed from source
### Pre-requisite
mdBook is written in **[Rust](https://www.rust-lang.org/)** and therefore needs to be compiled with **Cargo**, because we don't yet offer ready-to-go binaries. If you haven't already installed Rust, please go ahead and [install it](https://www.rust-lang.org/downloads.html) now.
mdBook is written in **[Rust](https://www.rust-lang.org/)** and therefore needs
to be compiled with **Cargo**. If you haven't already installed Rust, please go
ahead and [install it](https://www.rust-lang.org/tools/install) now.
### Install Crates.io version
Installing mdBook is relatively easy if you already have Rust and Cargo installed. You just have to type this snippet in your terminal:
Installing mdBook is relatively easy if you already have Rust and Cargo
installed. You just have to type this snippet in your terminal:
```bash
cargo install mdbook
```
This will fetch the source code from [Crates.io](https://crates.io/) and compile it. You will have to add Cargo's `bin` directory to your `PATH`.
This will fetch the source code for the latest release from
[Crates.io](https://crates.io/) and compile it. You will have to add Cargo's
`bin` directory to your `PATH`.
Run `mdbook help` in your terminal to verify if it works. Congratulations, you have installed mdBook!
Run `mdbook help` in your terminal to verify if it works. Congratulations, you
have installed mdBook!
### Install Git version
The **[git version](https://github.com/rust-lang-nursery/mdBook)** contains all the latest bug-fixes and features, that will be released in the next version on **Crates.io**, if you can't wait until the next release. You can build the git version yourself. Open your terminal and navigate to the directory of you choice. We need to clone the git repository and then build it with Cargo.
The **[git version](https://github.com/rust-lang-nursery/mdBook)** contains all
the latest bug-fixes and features, that will be released in the next version on
**Crates.io**, if you can't wait until the next release. You can build the git
version yourself. Open your terminal and navigate to the directory of you
choice. We need to clone the git repository and then build it with Cargo.
```bash
git clone --depth=1 https://github.com/rust-lang-nursery/mdBook.git
@@ -32,4 +51,5 @@ cd mdBook
cargo build --release
```
The executable `mdbook` will be in the `./target/release` folder, this should be added to the path.
The executable `mdbook` will be in the `./target/release` folder, this should be
added to the path.

View File

@@ -6,8 +6,8 @@ The build command is used to render your book:
mdbook build
```
It will try to parse your `SUMMARY.md` file to understand the structure of your book
and fetch the corresponding files.
It will try to parse your `SUMMARY.md` file to understand the structure of your
book and fetch the corresponding files.
The rendered output will maintain the same directory structure as the source for
convenience. Large books will therefore remain structured when rendered.
@@ -28,11 +28,12 @@ your default web browser after building it.
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for
the book. If not specified it will default to the value of the
`build.build-dir` key in `book.toml`, or to `./book` relative to the book's
root directory.
The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. Relative paths are interpreted relative to the book's root directory. If
not specified it will default to the value of the `build.build-dir` key in
`book.toml`, or to `./book`.
-------------------
***Note:*** *Make sure to run the build command in the root directory and not in the source directory*
***Note:*** *Make sure to run the build command in the root directory and not in
the source directory*

View File

@@ -19,9 +19,9 @@ mdbook clean path/to/book
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to override the book's output
directory, which will be deleted by this command. If not specified it
will default to the value of the `build.build-dir` key in `book.toml`, or to
`./book` relative to the book's root directory.
directory, which will be deleted by this command. Relative paths are interpreted
relative to the book's root directory. If not specified it will default to the
value of the `build.build-dir` key in `book.toml`, or to `./book`.
```bash
mdbook clean --dest-dir=path/to/book

View File

@@ -1,7 +1,7 @@
# The init command
There is some minimal boilerplate that is the same for every new book. It's for this purpose
that mdBook includes an `init` command.
There is some minimal boilerplate that is the same for every new book. It's for
this purpose that mdBook includes an `init` command.
The `init` command is used like this:
@@ -9,7 +9,8 @@ The `init` command is used like this:
mdbook init
```
When using the `init` command for the first time, a couple of files will be set up for you:
When using the `init` command for the first time, a couple of files will be set
up for you:
```bash
book-test/
├── book
@@ -18,25 +19,27 @@ book-test/
└── SUMMARY.md
```
- The `src` directory is were you write your book in markdown. It contains all the source files,
configuration files, etc.
- The `src` directory is were you write your book in markdown. It contains all
the source files, configuration files, etc.
- The `book` directory is where your book is rendered. All the output is ready to be uploaded
to a server to be seen by your audience.
- The `book` directory is where your book is rendered. All the output is ready
to be uploaded to a server to be seen by your audience.
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is
discussed in more detail [in another chapter](../format/summary.md)
- The `SUMMARY.md` file is the most important file, it's the skeleton of your
book and is discussed in more detail [in another
chapter](../format/summary.md)
#### Tip: Generate chapters from SUMMARY.md
When a `SUMMARY.md` file already exists, the `init` command will first parse it and generate the
missing files according to the paths used in the `SUMMARY.md`. This allows you to think and create
the whole structure of your book and then let mdBook generate it for you.
When a `SUMMARY.md` file already exists, the `init` command will first parse it
and generate the missing files according to the paths used in the `SUMMARY.md`.
This allows you to think and create the whole structure of your book and then
let mdBook generate it for you.
#### Specify a directory
The `init` command can take a directory as an argument to use as the book's
root instead of the current working directory.
The `init` command can take a directory as an argument to use as the book's root
instead of the current working directory.
```bash
mdbook init path/to/book
@@ -44,8 +47,8 @@ mdbook init path/to/book
#### --theme
When you use the `--theme` flag, the default theme will be copied into a directory
called `theme` in your source directory so that you can modify it.
When you use the `--theme` flag, the default theme will be copied into a
directory called `theme` in your source directory so that you can modify it.
The theme is selectively overwritten, this means that if you don't want to overwrite a
specific file, just delete it and the default file will be used.
The theme is selectively overwritten, this means that if you don't want to
overwrite a specific file, just delete it and the default file will be used.

View File

@@ -1,9 +1,12 @@
# The serve command
The serve command is used to preview a book by serving it over HTTP at
`localhost:3000` by default. Additionally it watches the book's directory
for changes, rebuilding the book and refreshing clients for each change.
A websocket connection is used to trigger the client-side refresh.
`localhost:3000` by default. Additionally it watches the book's directory for
changes, rebuilding the book and refreshing clients for each change. A websocket
connection is used to trigger the client-side refresh.
***Note:*** *The `serve` command is for testing a book's HTML output, and is not
intended to be a complete HTTP server for a website.*
#### Specify a directory
@@ -20,8 +23,8 @@ mdbook serve path/to/book
to listen on, and the hostname for the browser to connect to for WebSockets.
For example: suppose you have an nginx server for SSL termination which has a
public address of 192.168.1.100 on port 80 and proxied that to 127.0.0.1 on
port 8000. To run use the nginx proxy do:
public address of 192.168.1.100 on port 80 and proxied that to 127.0.0.1 on port
8000\. To run use the nginx proxy do:
```bash
mdbook serve path/to/book -p 8000 -n 127.0.0.1 --websocket-hostname 192.168.1.100
@@ -34,17 +37,12 @@ configured.
#### --open
When you use the `--open` (`-o`) flag, mdbook will open the book in your
your default web browser after starting the server.
When you use the `--open` (`-o`) flag, mdbook will open the book in your your
default web browser after starting the server.
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for
the book. If not specified it will default to the value of the
`build.build-dir` key in `book.toml`, or to `./book` relative to the book's
root directory.
-----
***Note:*** *The `serve` command is for testing, and is not intended
to be a complete HTTP server for a website.*
The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. Relative paths are interpreted relative to the book's root directory. If
not specified it will default to the value of the `build.build-dir` key in
`book.toml`, or to `./book`.

View File

@@ -5,9 +5,9 @@ When writing a book, you sometimes need to automate some tests. For example,
of code examples that could get outdated. Therefore it is very important for
them to be able to automatically test these code examples.
mdBook supports a `test` command that will run all available tests in a book.
At the moment, only rustdoc tests are supported, but this may be expanded upon
in the future.
mdBook supports a `test` command that will run all available tests in a book. At
the moment, only rustdoc tests are supported, but this may be expanded upon in
the future.
#### Disable tests on a code block
@@ -31,8 +31,8 @@ rustdoc *does* test code blocks which have no language specified:
#### Specify a directory
The `test` command can take a directory as an argument to use as the book's
root instead of the current working directory.
The `test` command can take a directory as an argument to use as the book's root
instead of the current working directory.
```bash
mdbook test path/to/book
@@ -47,7 +47,7 @@ comma-delimited list (`-L foo,bar`).
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for
the book. If not specified it will default to the value of the
`build.build-dir` key in `book.toml`, or to `./book` relative to the book's
root directory.
The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. Relative paths are interpreted relative to the book's root directory. If
not specified it will default to the value of the `build.build-dir` key in
`book.toml`, or to `./book`.

View File

@@ -2,8 +2,8 @@
The `watch` command is useful when you want your book to be rendered on every
file change. You could repeatedly issue `mdbook build` every time a file is
changed. But using `mdbook watch` once will watch your files and will trigger
a build automatically whenever you modify a file.
changed. But using `mdbook watch` once will watch your files and will trigger a
build automatically whenever you modify a file.
#### Specify a directory
@@ -21,7 +21,7 @@ your default web browser.
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for
the book. If not specified it will default to the value of the
`build.build-dir` key in `book.toml`, or to `./book` relative to the book's
root directory.
The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. Relative paths are interpreted relative to the book's root directory. If
not specified it will default to the value of the `build.build-dir` key in
`book.toml`, or to `./book`.

View File

@@ -26,7 +26,7 @@ before_script:
- cargo install-update -a
script:
- cd path/to/mybook && mdbook build && mdbook test
- mdbook build path/to/mybook && mdbook test path/to/mybook
```
## Deploying Your Book to GitHub Pages
@@ -34,9 +34,13 @@ script:
Following these instructions will result in your book being published to GitHub
pages after a successful CI run on your repository's `master` branch.
First, create a new GitHub "Personal Access Token" with the "public_repo" permissions (or "repo" for private repositories). Go to your repository's Travis CI settings page and add an environment variable named `GITHUB_TOKEN` that is marked secure and *not* shown in the logs.
First, create a new GitHub "Personal Access Token" with the "public_repo"
permissions (or "repo" for private repositories). Go to your repository's Travis
CI settings page and add an environment variable named `GITHUB_TOKEN` that is
marked secure and *not* shown in the logs.
Then, append this snippet to your `.travis.yml` and update the path to the `book` directory:
Then, append this snippet to your `.travis.yml` and update the path to the
`book` directory:
```yaml
deploy:

View File

@@ -1,12 +1,12 @@
# For Developers
While `mdbook` is mainly used as a command line tool, you can also import the
While `mdbook` is mainly used as a command line tool, you can also import the
underlying library directly and use that to manage a book. It also has a fairly
flexible plugin mechanism, allowing you to create your own custom tooling and
flexible plugin mechanism, allowing you to create your own custom tooling and
consumers (often referred to as *backends*) if you need to do some analysis of
the book or render it in a different format.
The *For Developers* chapters are here to show you the more advanced usage of
The *For Developers* chapters are here to show you the more advanced usage of
`mdbook`.
The two main ways a developer can hook into the book's build process is via,
@@ -32,15 +32,15 @@ The process of rendering a book project goes through several steps.
The `mdbook` binary is just a wrapper around the `mdbook` crate, exposing its
functionality as a command-line program. As such it is quite easy to create your
own programs which use `mdbook` internally, adding your own functionality (e.g.
own programs which use `mdbook` internally, adding your own functionality (e.g.
a custom preprocessor) or tweaking the build process.
The easiest way to find out how to use the `mdbook` crate is by looking at the
[API Docs]. The top level documentation explains how one would use the
[API Docs]. The top level documentation explains how one would use the
[`MDBook`] type to load and build a book, while the [config] module gives a good
explanation on the configuration system.
[`MDBook`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.MDBook.html
[API Docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
[API Docs]: https://docs.rs/mdbook/*/mdbook/
[config]: file:///home/michael/Documents/forks/mdBook/target/doc/mdbook/config/index.html

View File

@@ -1,11 +1,11 @@
# Alternate Backends
A "backend" is simply a program which `mdbook` will invoke during the book
A "backend" is simply a program which `mdbook` will invoke during the book
rendering process. This program is passed a JSON representation of the book and
configuration information via `stdin`. Once the backend receives this
configuration information via `stdin`. Once the backend receives this
information it is free to do whatever it wants.
There are already several alternate backends on GitHub which can be used as a
There are already several alternate backends on GitHub which can be used as a
rough example of how this is accomplished in practice.
- [mdbook-linkcheck] - a simple program for verifying the book doesn't contain
@@ -21,7 +21,7 @@ no reason why it couldn't be accomplished using something like Python or Ruby.
## Setting Up
First you'll want to create a new binary program and add `mdbook` as a
First you'll want to create a new binary program and add `mdbook` as a
dependency.
```
@@ -30,8 +30,8 @@ $ cd mdbook-wordcount
$ cargo add mdbook
```
When our `mdbook-wordcount` plugin is invoked, `mdbook` will send it a JSON
version of [`RenderContext`] via our plugin's `stdin`. For convenience, there's
When our `mdbook-wordcount` plugin is invoked, `mdbook` will send it a JSON
version of [`RenderContext`] via our plugin's `stdin`. For convenience, there's
a [`RenderContext::from_json()`] constructor which will load a `RenderContext`.
This is all the boilerplate necessary for our backend to load the book.
@@ -49,9 +49,9 @@ fn main() {
}
```
> **Note:** The `RenderContext` contains a `version` field. This lets backends
> **Note:** The `RenderContext` contains a `version` field. This lets backends
figure out whether they are compatible with the version of `mdbook` it's being
called by. This `version` comes directly from the corresponding field in
called by. This `version` comes directly from the corresponding field in
`mdbook`'s `Cargo.toml`.
It is recommended that backends use the [`semver`] crate to inspect this field
@@ -60,7 +60,7 @@ fn main() {
## Inspecting the Book
Now our backend has a copy of the book, lets count how many words are in each
Now our backend has a copy of the book, lets count how many words are in each
chapter!
Because the `RenderContext` contains a [`Book`] field (`book`), and a `Book` has
@@ -89,8 +89,8 @@ fn count_words(ch: &Chapter) -> usize {
## Enabling the Backend
Now we've got the basics running, we want to actually use it. First, install
the program.
Now we've got the basics running, we want to actually use it. First, install the
program.
```
$ cargo install
@@ -110,15 +110,14 @@ Then `cd` to the particular book you'd like to count the words of and update its
+ [output.wordcount]
```
When it loads a book into memory, `mdbook` will inspect your `book.toml` file
to try and figure out which backends to use by looking for all `output.*`
tables. If none are provided it'll fall back to using the default HTML
renderer.
When it loads a book into memory, `mdbook` will inspect your `book.toml` file to
try and figure out which backends to use by looking for all `output.*` tables.
If none are provided it'll fall back to using the default HTML renderer.
Notably, this means if you want to add your own custom backend you'll also
need to make sure to add the HTML backend, even if its table just stays empty.
Notably, this means if you want to add your own custom backend you'll also need
to make sure to add the HTML backend, even if its table just stays empty.
Now you just need to build your book like normal, and everything should *Just
Now you just need to build your book like normal, and everything should *Just
Work*.
```
@@ -145,11 +144,11 @@ Alternate Backends: 710
Contributors: 85
```
The reason we didn't need to specify the full name/path of our `wordcount`
backend is because `mdbook` will try to *infer* the program's name via
convention. The executable for the `foo` backend is typically called
The reason we didn't need to specify the full name/path of our `wordcount`
backend is because `mdbook` will try to *infer* the program's name via
convention. The executable for the `foo` backend is typically called
`mdbook-foo`, with an associated `[output.foo]` entry in the `book.toml`. To
explicitly tell `mdbook` what command to invoke (it may require command-line
explicitly tell `mdbook` what command to invoke (it may require command-line
arguments or be an interpreted script), you can use the `command` field.
```diff
@@ -168,16 +167,16 @@ arguments or be an interpreted script), you can use the `command` field.
## Configuration
Now imagine you don't want to count the number of words on a particular chapter
(it might be generated text/code, etc). The canonical way to do this is via
the usual `book.toml` configuration file by adding items to your `[output.foo]`
(it might be generated text/code, etc). The canonical way to do this is via the
usual `book.toml` configuration file by adding items to your `[output.foo]`
table.
The `Config` can be treated roughly as a nested hashmap which lets you call
methods like `get()` to access the config's contents, with a
`get_deserialized()` convenience method for retrieving a value and
automatically deserializing to some arbitrary type `T`.
`get_deserialized()` convenience method for retrieving a value and automatically
deserializing to some arbitrary type `T`.
To implement this, we'll create our own serializable `WordcountConfig` struct
To implement this, we'll create our own serializable `WordcountConfig` struct
which will encapsulate all configuration for this backend.
First add `serde` and `serde_derive` to your `Cargo.toml`,
@@ -229,7 +228,7 @@ and then add a check to make sure we skip ignored chapters.
## Output and Signalling Failure
While it's nice to print word counts to the terminal when a book is built, it
While it's nice to print word counts to the terminal when a book is built, it
might also be a good idea to output them to a file somewhere. `mdbook` tells a
backend where it should place any generated output via the `destination` field
in [`RenderContext`].
@@ -264,7 +263,7 @@ in [`RenderContext`].
> so it's always a good idea to create it with `fs::create_dir_all()`.
There's always the possibility that an error will occur while processing a book
(just look at all the `unwrap()`'s we've written already), so `mdbook` will
(just look at all the `unwrap()`'s we've written already), so `mdbook` will
interpret a non-zero exit code as a rendering failure.
For example, if we wanted to make sure all chapters have an *even* number of
@@ -315,27 +314,27 @@ init: 283
init has an odd number of words!
2018-01-16 21:21:39 [ERROR] (mdbook::renderer): Renderer exited with non-zero return code.
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Error: Rendering failed
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Caused By: The "mdbook-wordcount" renderer failed
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Caused By: The "mdbook-wordcount" renderer failed
```
As you've probably already noticed, output from the plugin's subprocess is
immediately passed through to the user. It is encouraged for plugins to
follow the "rule of silence" and only generate output when necessary (e.g. an
error in generation or a warning).
immediately passed through to the user. It is encouraged for plugins to follow
the "rule of silence" and only generate output when necessary (e.g. an error in
generation or a warning).
All environment variables are passed through to the backend, allowing you to
use the usual `RUST_LOG` to control logging verbosity.
All environment variables are passed through to the backend, allowing you to use
the usual `RUST_LOG` to control logging verbosity.
## Wrapping Up
Although contrived, hopefully this example was enough to show how you'd create
an alternate backend for `mdbook`. If you feel it's missing something, don't
an alternate backend for `mdbook`. If you feel it's missing something, don't
hesitate to create an issue in the [issue tracker] so we can improve the user
guide.
The existing backends mentioned towards the start of this chapter should serve
as a good example of how it's done in real life, so feel free to skim through
as a good example of how it's done in real life, so feel free to skim through
the source code or ask questions.
@@ -348,5 +347,5 @@ the source code or ask questions.
[`semver`]: https://crates.io/crates/semver
[`Book`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html
[`Book::iter()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html#method.iter
[`Config`]: http://rust-lang-nursery.github.io/mdBook/mdbook/config/struct.Config.html
[`Config`]: http://rust-lang-nursery.github.io/mdBook/mdbook/config/struct.Config.html
[issue tracker]: https://github.com/rust-lang-nursery/mdBook/issues

View File

@@ -11,64 +11,79 @@ the book. Possible use cases are:
mathjax equivalents
## Implementing a Preprocessor
## Hooking Into MDBook
A preprocessor is represented by the `Preprocessor` trait.
MDBook uses a fairly simple mechanism for discovering third party plugins.
A new table is added to `book.toml` (e.g. `preprocessor.foo` for the `foo`
preprocessor) and then `mdbook` will try to invoke the `mdbook-foo` program as
part of the build process.
```rust
pub trait Preprocessor {
fn name(&self) -> &str;
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
}
While preprocessors can be hard-coded to specify which backend it should be run
for (e.g. it doesn't make sense for MathJax to be used for non-HTML renderers)
with the `preprocessor.foo.renderer` key.
```toml
[book]
title = "My Book"
authors = ["Michael-F-Bryan"]
[preprocessor.foo]
# The command can also be specified manually
command = "python3 /path/to/foo.py"
# Only run the `foo` preprocessor for the HTML and EPUB renderer
renderer = ["html", "epub"]
```
Where the `PreprocessorContext` is defined as
In typical unix style, all inputs to the plugin will be written to `stdin` as
JSON and `mdbook` will read from `stdout` if it is expecting output.
The easiest way to get started is by creating your own implementation of the
`Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
translates inputs to the correct `Preprocessor` method. For convenience, there
is [an example no-op preprocessor] in the `examples/` directory which can easily
be adapted for other preprocessors.
<details>
<summary>Example no-op preprocessor</summary>
```rust
pub struct PreprocessorContext {
pub root: PathBuf,
pub config: Config,
}
// nop-preprocessors.rs
{{#include ../../../examples/nop-preprocessor.rs}}
```
</details>
## A complete Example
## Hints For Implementing A Preprocessor
The magic happens within the `run(...)` method of the [`Preprocessor`][preprocessor-docs] trait implementation.
By pulling in `mdbook` as a library, preprocessors can have access to the
existing infrastructure for dealing with books.
As direct access to the chapters is not possible, you will probably end up iterating
them using `for_each_mut(...)`:
For example, a custom preprocessor could use the
[`CmdPreprocessor::parse_input()`] function to deserialize the JSON written to
`stdin`. Then each chapter of the `Book` can be mutated in-place via
[`Book::for_each_mut()`], and then written to `stdout` with the `serde_json`
crate.
Chapters can be accessed either directly (by recursively iterating over
chapters) or via the `Book::for_each_mut()` convenience method.
The `chapter.content` is just a string which happens to be markdown. While it's
entirely possible to use regular expressions or do a manual find & replace,
you'll probably want to process the input into something more computer-friendly.
The [`pulldown-cmark`][pc] crate implements a production-quality event-based
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] allowing you to
translate events back into markdown text.
The following code block shows how to remove all emphasis from markdown,
without accidentally breaking the document.
```rust
book.for_each_mut(|item: &mut BookItem| {
if let BookItem::Chapter(ref mut chapter) = *item {
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
res = Some(
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
Ok(md) => {
chapter.content = md;
Ok(())
}
Err(err) => Err(err),
},
);
}
});
```
The `chapter.content` is just a markdown formatted string, and you will have to
process it in some way. Even though it's entirely possible to implement some sort of
manual find & replace operation, if that feels too unsafe you can use [`pulldown-cmark`][pc]
to parse the string into events and work on them instead.
Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events back to
a string.
The following code block shows how to remove all emphasis from markdown, and do so
safely.
```rust
fn remove_emphasis(num_removed_items: &mut i32, chapter: &mut Chapter) -> Result<String> {
fn remove_emphasis(
num_removed_items: &mut usize,
chapter: &mut Chapter,
) -> Result<String> {
let mut buf = String::with_capacity(chapter.content.len());
let events = Parser::new(&chapter.content).filter(|e| {
let should_keep = match *e {
Event::Start(Tag::Emphasis)
@@ -82,15 +97,19 @@ fn remove_emphasis(num_removed_items: &mut i32, chapter: &mut Chapter) -> Result
}
should_keep
});
cmark(events, &mut buf, None)
.map(|_| buf)
.map_err(|err| Error::from(format!("Markdown serialization failed: {}", err)))
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
Error::from(format!("Markdown serialization failed: {}", err))
})
}
```
For everything else, have a look [at the complete example][example].
[preprocessor-docs]: https://docs.rs/mdbook/0.1.3/mdbook/preprocess/trait.Preprocessor.html
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
[pc]: https://crates.io/crates/pulldown-cmark
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs
[an example no-op preprocessor]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/nop-preprocessor.rs
[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input
[`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut

View File

@@ -14,6 +14,10 @@ description = "The example book covers examples."
build-dir = "my-example-book"
create-missing = false
[preprocess.index]
[preprocess.links]
[output.html]
additional-css = ["custom.css"]
@@ -23,9 +27,9 @@ limit-results = 15
## Supported configuration options
It is important to note that **any** relative path specified in the in the configuration will
always be taken relative from the root of the book where the configuration file is located.
It is important to note that **any** relative path specified in the in the
configuration will always be taken relative from the root of the book where the
configuration file is located.
### General metadata
@@ -58,13 +62,28 @@ This controls the build process of your book.
will be created when the book is built (i.e. `create-missing = true`). If this
is `false` then the build process will instead exit with an error if any files
do not exist.
- **preprocess:** Specify which preprocessors to be applied. Default is `["links", "index"]`. To disable default preprocessors, pass an empty array `[]` in.
- **use-default-preprocessors:** Disable the default preprocessors of (`links` &
`index`) by setting this option to `false`.
If you have the same, and/or other preprocessors declared via their table
of configuration, they will run instead.
- For clarity, with no preprocessor configuration, the default `links` and
`index` will run.
- Setting `use-default-preprocessors = false` will disable these
default preprocessors from running.
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
`use-default-preprocessors` that `links` it will run.
## Configuring Preprocessors
The following preprocessors are available and included by default:
- `links`: Expand the `{{# playpen}}` and `{{# include}}` handlebars helpers in a chapter.
- `index`: Convert all chapter files named `README.md` into `index.md`. That is to say, all `README.md` would be rendered to an index file `index.html` in the rendered book.
- `links`: Expand the `{{ #playpen }}` and `{{ #include }}` handlebars
helpers in a chapter to include the contents of a file.
- `index`: Convert all chapter files named `README.md` into `index.md`. That is
to say, all `README.md` would be rendered to an index file `index.html` in the
rendered book.
**book.toml**
@@ -72,36 +91,84 @@ The following preprocessors are available and included by default:
[build]
build-dir = "build"
create-missing = false
preprocess = ["links", "index"]
[preprocess.links]
[preprocess.index]
```
### Custom Preprocessor Configuration
Like renderers, preprocessor will need to be given its own table (e.g.
`[preprocessor.mathjax]`). In the section, you may then pass extra
configuration to the preprocessor by adding key-value pairs to the table.
For example
```
[preprocess.links]
# set the renderers this preprocessor will run for
renderers = ["html"]
some_extra_feature = true
```
#### Locking a Preprocessor dependency to a renderer
You can explicitly specify that a preprocessor should run for a renderer by
binding the two together.
```
[preprocessor.mathjax]
renderers = ["html"] # mathjax only makes sense with the HTML renderer
```
### Provide Your Own Command
By default when you add a `[preprocessor.foo]` table to your `book.toml` file,
`mdbook` will try to invoke the `mdbook-foo` executa`ble. If you want to use a
different program name or pass in command-line arguments, this behaviour can
be overridden by adding a `command` field.
```toml
[preprocessor.random]
command = "python random.py"
```
## Configuring Renderers
### HTML renderer options
The HTML renderer has a couple of options as well. All the options for the
renderer need to be specified under the TOML table `[output.html]`.
The following configuration options are available:
- **theme:** mdBook comes with a default theme and all the resource files
needed for it. But if this option is set, mdBook will selectively overwrite
the theme files with the ones found in the specified folder.
- **curly-quotes:** Convert straight quotes to curly quotes, except for
those that occur in code blocks and code spans. Defaults to `false`.
- **google-analytics:** If you use Google Analytics, this option lets you
enable it by simply specifying your ID in the configuration file.
- **additional-css:** If you need to slightly change the appearance of your
book without overwriting the whole style, you can specify a set of
stylesheets that will be loaded after the default ones where you can
surgically change the style.
- **theme:** mdBook comes with a default theme and all the resource files needed
for it. But if this option is set, mdBook will selectively overwrite the theme
files with the ones found in the specified folder.
- **default-theme:** The theme color scheme to select by default in the
'Change Theme' dropdown. Defaults to `light`.
- **curly-quotes:** Convert straight quotes to curly quotes, except for those
that occur in code blocks and code spans. Defaults to `false`.
- **google-analytics:** If you use Google Analytics, this option lets you enable
it by simply specifying your ID in the configuration file.
- **additional-css:** If you need to slightly change the appearance of your book
without overwriting the whole style, you can specify a set of stylesheets that
will be loaded after the default ones where you can surgically change the
style.
- **additional-js:** If you need to add some behaviour to your book without
removing the current behaviour, you can specify a set of JavaScript files
that will be loaded alongside the default one.
removing the current behaviour, you can specify a set of JavaScript files that
will be loaded alongside the default one.
- **no-section-label:** mdBook by defaults adds section label in table of
contents column. For example, "1.", "2.1". Set this option to true to
disable those labels. Defaults to `false`.
contents column. For example, "1.", "2.1". Set this option to true to disable
those labels. Defaults to `false`.
- **playpen:** A subtable for configuring various playpen settings.
- **search:** A subtable for configuring the in-browser search
functionality. mdBook must be compiled with the `search` feature enabled
(on by default).
- **search:** A subtable for configuring the in-browser search functionality.
mdBook must be compiled with the `search` feature enabled (on by default).
- **git_repository_url:** A url to the git repository for the book. If provided
an icon link will be output in the menu bar of the book.
- **git_repository_icon:** The FontAwesome icon class to use for the git
repository link. Defaults to `fa-github`.
Available configuration options for the `[output.html.playpen]` table:
@@ -115,27 +182,27 @@ Available configuration options for the `[output.html.search]` table:
- **enable:** Enables the search feature. Defaults to `true`.
- **limit-results:** The maximum number of search results. Defaults to `30`.
- **teaser-word-count:** The number of words used for a search result teaser.
- **teaser-word-count:** The number of words used for a search result teaser.
Defaults to `30`.
- **use-boolean-and:** Define the logical link between multiple search words.
If true, all search words must appear in each result. Defaults to `true`.
- **use-boolean-and:** Define the logical link between multiple search words. If
true, all search words must appear in each result. Defaults to `true`.
- **boost-title:** Boost factor for the search result score if a search word
appears in the header. Defaults to `2`.
- **boost-hierarchy:** Boost factor for the search result score if a search
word appears in the hierarchy. The hierarchy contains all titles of the
parent documents and all parent headings. Defaults to `1`.
- **boost-paragraph:** Boost factor for the search result score if a search
word appears in the text. Defaults to `1`.
- **expand:** True if search should match longer results e.g. search `micro`
- **boost-hierarchy:** Boost factor for the search result score if a search word
appears in the hierarchy. The hierarchy contains all titles of the parent
documents and all parent headings. Defaults to `1`.
- **boost-paragraph:** Boost factor for the search result score if a search word
appears in the text. Defaults to `1`.
- **expand:** True if search should match longer results e.g. search `micro`
should match `microwave`. Defaults to `true`.
- **heading-split-level:** Search results will link to a section of the document
which contains the result. Documents are split into sections by headings
this level or less.
Defaults to `3`. (`### This is a level 3 heading`)
- **copy-js:** Copy JavaScript files for the search implementation to the
output directory. Defaults to `true`.
which contains the result. Documents are split into sections by headings this
level or less. Defaults to `3`. (`### This is a level 3 heading`)
- **copy-js:** Copy JavaScript files for the search implementation to the output
directory. Defaults to `true`.
This shows all available HTML output options in the **book.toml**:
This shows all available options in the **book.toml**:
```toml
[book]
title = "Example book"
@@ -172,6 +239,16 @@ heading-split-level = 3
copy-js = true
```
### Custom Renderers
A custom renderer can be enabled by adding a `[output.foo]` table to your
`book.toml`. Similar to [preprocessors](#configuring-preprocessors) this will
instruct `mdbook` to pass a representation of the book to `mdbook-foo` for
rendering.
Custom renderers will have access to all configuration within their table
(i.e. anything under `[output.foo]`), and the command to be invoked can be
manually specified with the `command` field.
## Environment Variables
@@ -180,10 +257,10 @@ corresponding environment variable. Because many operating systems restrict
environment variables to be alphanumeric characters or `_`, the configuration
key needs to be formatted slightly differently to the normal `foo.bar.baz` form.
Variables starting with `MDBOOK_` are used for configuration. The key is
created by removing the `MDBOOK_` prefix and turning the resulting
string into `kebab-case`. Double underscores (`__`) separate nested
keys, while a single underscore (`_`) is replaced with a dash (`-`).
Variables starting with `MDBOOK_` are used for configuration. The key is created
by removing the `MDBOOK_` prefix and turning the resulting string into
`kebab-case`. Double underscores (`__`) separate nested keys, while a single
underscore (`_`) is replaced with a dash (`-`).
For example:
@@ -193,21 +270,21 @@ For example:
- `MDBOOK_FOO_BAR` -> `foo-bar`
- `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
So by setting the `MDBOOK_BOOK__TITLE` environment variable you can
override the book's title without needing to touch your `book.toml`.
So by setting the `MDBOOK_BOOK__TITLE` environment variable you can override the
book's title without needing to touch your `book.toml`.
> **Note:** To facilitate setting more complex config items, the value
> of an environment variable is first parsed as JSON, falling back to a
> string if the parse fails.
> **Note:** To facilitate setting more complex config items, the value of an
> environment variable is first parsed as JSON, falling back to a string if the
> parse fails.
>
> This means, if you so desired, you could override all book metadata
> when building the book with something like
> This means, if you so desired, you could override all book metadata when
> building the book with something like
>
> ```text
> $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
> $ mdbook build
> ```
The latter case may be useful in situations where `mdbook` is invoked
from a script or CI, where it sometimes isn't possible to update the
`book.toml` before building.
The latter case may be useful in situations where `mdbook` is invoked from a
script or CI, where it sometimes isn't possible to update the `book.toml` before
building.

View File

@@ -1,29 +1,37 @@
# MathJax Support
mdBook has optional support for math equations through [MathJax](https://www.mathjax.org/).
mdBook has optional support for math equations through
[MathJax](https://www.mathjax.org/).
To enable MathJax, you need to add the `mathjax-support` key to your `book.toml` under the `output.html` section.
To enable MathJax, you need to add the `mathjax-support` key to your `book.toml`
under the `output.html` section.
```toml
[output.html]
mathjax-support = true
```
>**Note:**
The usual delimiters MathJax uses are not yet supported. You can't currently use `$$ ... $$` as delimiters and the `\[ ... \]` delimiters need an extra backslash to work. Hopefully this limitation will be lifted soon.
>**Note:** The usual delimiters MathJax uses are not yet supported. You can't
currently use `$$ ... $$` as delimiters and the `\[ ... \]` delimiters need an
extra backslash to work. Hopefully this limitation will be lifted soon.
>**Note:**
> When you use double backslashes in MathJax blocks (for example in commands such as `\begin{cases} \frac 1 2 \\ \frac 3 4 \end{cases}`) you need to add _two extra_ backslashes (e.g., `\begin{cases} \frac 1 2 \\\\ \frac 3 4 \end{cases}`).
>**Note:** When you use double backslashes in MathJax blocks (for example in
> commands such as `\begin{cases} \frac 1 2 \\ \frac 3 4 \end{cases}`) you need
> to add _two extra_ backslashes (e.g., `\begin{cases} \frac 1 2 \\\\ \frac 3 4
> \end{cases}`).
### Inline equations
Inline equations are delimited by `\\(` and `\\)`. So for example, to render the following inline equation \\( \int x dx = \frac{x^2}{2} + C \\) you would write the following:
Inline equations are delimited by `\\(` and `\\)`. So for example, to render the
following inline equation \\( \int x dx = \frac{x^2}{2} + C \\) you would write
the following:
```
\\( \int x dx = \frac{x^2}{2} + C \\)
```
### Block equations
Block equations are delimited by `\\[` and `\\]`. To render the following equation
Block equations are delimited by `\\[` and `\\]`. To render the following
equation
\\[ \mu = \frac{1}{N} \sum_{i=0} x_i \\]

View File

@@ -2,7 +2,8 @@
## Hiding code lines
There is a feature in mdBook that lets you hide code lines by prepending them with a `#`.
There is a feature in mdBook that lets you hide code lines by prepending them
with a `#`.
```bash
# fn main() {
@@ -34,7 +35,10 @@ With the following syntax, you can include files into your book:
The path to the file has to be relative from the current source file.
Usually, this command is used for including code snippets and examples. In this case, oftens one would include a specific part of the file e.g. which only contains the relevant lines for the example. We support four different modes of partial includes:
Usually, this command is used for including code snippets and examples. In this
case, oftens one would include a specific part of the file e.g. which only
contains the relevant lines for the example. We support four different modes of
partial includes:
```hbs
\{{#include file.rs:2}}
@@ -43,7 +47,11 @@ Usually, this command is used for including code snippets and examples. In this
\{{#include file.rs:2:10}}
```
The first command only includes the second line from file `file.rs`. The second command includes all lines up to line 10, i.e. the lines from 11 till the end of the file are omitted. The third command includes all lines from line 2, i.e. the first line is omitted. The last command includes the excerpt of `file.rs` consisting of lines 2 to 10.
The first command only includes the second line from file `file.rs`. The second
command includes all lines up to line 10, i.e. the lines from 11 till the end of
the file are omitted. The third command includes all lines from line 2, i.e. the
first line is omitted. The last command includes the excerpt of `file.rs`
consisting of lines 2 to 10.
## Inserting runnable Rust files
@@ -55,7 +63,9 @@ With the following syntax, you can insert runnable Rust files into your book:
The path to the Rust file has to be relative from the current source file.
When play is clicked, the code snippet will be sent to the [Rust Playpen] to be compiled and run. The result is sent back and displayed directly underneath the code.
When play is clicked, the code snippet will be sent to the [Rust Playpen] to be
compiled and run. The result is sent back and displayed directly underneath the
code.
Here is what a rendered code snippet looks like:

View File

@@ -1,32 +1,38 @@
# SUMMARY.md
The summary file is used by mdBook to know what chapters to include,
in what order they should appear, what their hierarchy is and where the source files are.
Without this file, there is no book.
The summary file is used by mdBook to know what chapters to include, in what
order they should appear, what their hierarchy is and where the source files
are. Without this file, there is no book.
Even though `SUMMARY.md` is a markdown file, the formatting is very strict to
allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
#### Allowed elements
1. ***Title*** It's common practice to begin with a title, generally
<code class="language-markdown"># Summary</code>.
But it is not mandatory, the parser just ignores it. So you can too
if you feel like it.
1. ***Title*** It's common practice to begin with a title, generally <code
class="language-markdown"># Summary</code>. But it is not mandatory, the
parser just ignores it. So you can too if you feel like it.
2. ***Prefix Chapter*** Before the main numbered chapters you can add a couple of elements that will not be numbered. This is useful for
forewords, introductions, etc. There are however some constraints. You can not nest prefix chapters, they should all be on the root level. And you can not add prefix chapters once you have added numbered chapters.
2. ***Prefix Chapter*** Before the main numbered chapters you can add a couple
of elements that will not be numbered. This is useful for forewords,
introductions, etc. There are however some constraints. You can not nest
prefix chapters, they should all be on the root level. And you can not add
prefix chapters once you have added numbered chapters.
```markdown
[Title of prefix element](relative/path/to/markdown.md)
```
3. ***Numbered Chapter*** Numbered chapters are the main content of the book, they will be numbered and can be nested,
resulting in a nice hierarchy (chapters, sub-chapters, etc.)
3. ***Numbered Chapter*** Numbered chapters are the main content of the book,
they will be numbered and can be nested, resulting in a nice hierarchy
(chapters, sub-chapters, etc.)
```markdown
- [Title of the Chapter](relative/path/to/markdown.md)
```
You can either use `-` or `*` to indicate a numbered chapter.
4. ***Suffix Chapter*** After the numbered chapters you can add a couple of non-numbered chapters. They are the same as prefix chapters but come after the numbered chapters instead of before.
4. ***Suffix Chapter*** After the numbered chapters you can add a couple of
non-numbered chapters. They are the same as prefix chapters but come after
the numbered chapters instead of before.
All other elements are unsupported and will be ignored at best or result in an error.
All other elements are unsupported and will be ignored at best or result in an
error.

View File

@@ -1,22 +1,34 @@
# Theme
The default renderer uses a [handlebars](http://handlebarsjs.com/) template to render your markdown files and comes with a default theme
included in the mdBook binary.
The default renderer uses a [handlebars](http://handlebarsjs.com/) template to
render your markdown files and comes with a default theme included in the mdBook
binary.
The theme is totally customizable, you can selectively replace every file from the theme by your own by adding a
`theme` directory next to `src` folder in your project root. Create a new file with the name of the file you want to override
The theme is totally customizable, you can selectively replace every file from
the theme by your own by adding a `theme` directory next to `src` folder in your
project root. Create a new file with the name of the file you want to override
and now that file will be used instead of the default file.
Here are the files you can override:
- ***index.hbs*** is the handlebars template.
- ***book.css*** is the style used in the output. If you want to change the design of your book, this is probably the file you want to modify. Sometimes in conjunction with `index.hbs` when you want to radically change the layout.
- ***book.js*** is mostly used to add client side functionality, like hiding / un-hiding the sidebar, changing the theme, ...
- ***highlight.js*** is the JavaScript that is used to highlight code snippets, you should not need to modify this.
- ***book.css*** is the style used in the output. If you want to change the
design of your book, this is probably the file you want to modify. Sometimes
in conjunction with `index.hbs` when you want to radically change the layout.
- ***book.js*** is mostly used to add client side functionality, like hiding /
un-hiding the sidebar, changing the theme, ...
- ***highlight.js*** is the JavaScript that is used to highlight code snippets,
you should not need to modify this.
- ***highlight.css*** is the theme used for the code highlighting
- ***favicon.png*** the favicon that will be used
Generally, when you want to tweak the theme, you don't need to override all the files. If you only need changes in the stylesheet,
there is no point in overriding all the other files. Because custom files take precedence over built-in ones, they will not get updated with new fixes / features.
Generally, when you want to tweak the theme, you don't need to override all the
files. If you only need changes in the stylesheet, there is no point in
overriding all the other files. Because custom files take precedence over
built-in ones, they will not get updated with new fixes / features.
**Note:** When you override a file, it is possible that you break some functionality. Therefore I recommend to use the file from the default theme as template and only add / modify what you need. You can copy the default theme into your source directory automatically by using `mdbook init --theme` just remove the files you don't want to override.
**Note:** When you override a file, it is possible that you break some
functionality. Therefore I recommend to use the file from the default theme as
template and only add / modify what you need. You can copy the default theme
into your source directory automatically by using `mdbook init --theme` just
remove the files you don't want to override.

View File

@@ -1,13 +1,16 @@
# Editor
In addition to providing runnable code playpens, mdBook optionally allows them to be editable. In order to enable editable code blocks, the following needs to be added to the ***book.toml***:
In addition to providing runnable code playpens, mdBook optionally allows them
to be editable. In order to enable editable code blocks, the following needs to
be added to the ***book.toml***:
```toml
[output.html.playpen]
editable = true
```
To make a specific block available for editing, the attribute `editable` needs to be added to it:
To make a specific block available for editing, the attribute `editable` needs
to be added to it:
<pre><code class="language-markdown">```rust,editable
fn main() {
@@ -29,7 +32,8 @@ Note the new `Undo Changes` button in the editable playpens.
## Customizing the Editor
By default, the editor is the [Ace](https://ace.c9.io/) editor, but, if desired, the functionality may be overriden by providing a different folder:
By default, the editor is the [Ace](https://ace.c9.io/) editor, but, if desired,
the functionality may be overriden by providing a different folder:
```toml
[output.html.playpen]
@@ -37,4 +41,6 @@ editable = true
editor = "/path/to/editor"
```
Note that for the editor changes to function correctly, the `book.js` inside of the `theme` folder will need to be overriden as it has some couplings with the default Ace editor.
Note that for the editor changes to function correctly, the `book.js` inside of
the `theme` folder will need to be overriden as it has some couplings with the
default Ace editor.

View File

@@ -1,15 +1,15 @@
# index.hbs
`index.hbs` is the handlebars template that is used to render the book.
The markdown files are processed to html and then injected in that template.
`index.hbs` is the handlebars template that is used to render the book. The
markdown files are processed to html and then injected in that template.
If you want to change the layout or style of your book, chances are that you will
have to modify this template a little bit. Here is what you need to know.
If you want to change the layout or style of your book, chances are that you
will have to modify this template a little bit. Here is what you need to know.
## Data
A lot of data is exposed to the handlebars template with the "context".
In the handlebars template you can access this information by using
A lot of data is exposed to the handlebars template with the "context". In the
handlebars template you can access this information by using
```handlebars
{{name_of_property}}
@@ -17,25 +17,31 @@ In the handlebars template you can access this information by using
Here is a list of the properties that are exposed:
- ***language*** Language of the book in the form `en`. To use in <code class="language-html">\<html lang="{{ language }}"></code> for example.
At the moment it is hardcoded.
- ***language*** Language of the book in the form `en`. To use in <code
class="language-html">\<html lang="{{ language }}"></code> for example. At the
moment it is hardcoded.
- ***title*** Title of the book, as specified in `book.toml`
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
- ***path*** Relative path to the original markdown file from the source directory
- ***path*** Relative path to the original markdown file from the source
directory
- ***content*** This is the rendered markdown.
- ***path_to_root*** This is a path containing exclusively `../`'s that points to the root of the book from the current file.
Since the original directory structure is maintained, it is useful to prepend relative links with this `path_to_root`.
- ***path_to_root*** This is a path containing exclusively `../`'s that points
to the root of the book from the current file. Since the original directory
structure is maintained, it is useful to prepend relative links with this
`path_to_root`.
- ***chapters*** Is an array of dictionaries of the form
```json
{"section": "1.2.1", "name": "name of this chapter", "path": "dir/markdown.md"}
```
containing all the chapters of the book. It is used for example to construct the table of contents (sidebar).
containing all the chapters of the book. It is used for example to construct
the table of contents (sidebar).
## Handlebars Helpers
In addition to the properties you can access, there are some handlebars helpers at your disposal.
In addition to the properties you can access, there are some handlebars helpers
at your disposal.
### 1. toc
@@ -87,4 +93,5 @@ In addition to the properties you can access, there are some handlebars helpers
------
*If you would like other properties or helpers exposed, please [create a new issue](https://github.com/rust-lang-nursery/mdBook/issues)*
*If you would like other properties or helpers exposed, please [create a new
issue](https://github.com/rust-lang-nursery/mdBook/issues)*

View File

@@ -1,6 +1,7 @@
# Syntax Highlighting
For syntax highlighting I use [Highlight.js](https://highlightjs.org) with a custom theme.
For syntax highlighting I use [Highlight.js](https://highlightjs.org) with a
custom theme.
Automatic language detection has been turned off, so you will probably want to
specify the programming language you use like this
@@ -12,19 +13,23 @@ fn main() {
```</code></pre>
## Custom theme
Like the rest of the theme, the files used for syntax highlighting can be overridden with your own.
Like the rest of the theme, the files used for syntax highlighting can be
overridden with your own.
- ***highlight.js*** normally you shouldn't have to overwrite this file, unless you want to use a more recent version.
- ***highlight.js*** normally you shouldn't have to overwrite this file, unless
you want to use a more recent version.
- ***highlight.css*** theme used by highlight.js for syntax highlighting.
If you want to use another theme for `highlight.js` download it from their website, or make it yourself,
rename it to `highlight.css` and put it in `src/theme` (or the equivalent if you changed your source folder)
If you want to use another theme for `highlight.js` download it from their
website, or make it yourself, rename it to `highlight.css` and put it in
`src/theme` (or the equivalent if you changed your source folder)
Now your theme will be used instead of the default theme.
## Hiding code lines
There is a feature in mdBook that let's you hide code lines by prepending them with a `#`.
There is a feature in mdBook that lets you hide code lines by prepending them
with a `#`.
```bash
@@ -47,13 +52,18 @@ Will render as
# }
```
**At the moment, this only works for code examples that are annotated with `rust`. Because it would collide with semantics of some programming languages. In the future, we want to make this configurable through the `book.toml` so that everyone can benefit from it.**
**At the moment, this only works for code examples that are annotated with
`rust`. Because it would collide with semantics of some programming languages.
In the future, we want to make this configurable through the `book.toml` so that
everyone can benefit from it.**
## Improve default theme
If you think the default theme doesn't look quite right for a specific language, or could be improved.
Feel free to [submit a new issue](https://github.com/rust-lang-nursery/mdBook/issues) explaining what you have in mind and I will take a look at it.
If you think the default theme doesn't look quite right for a specific language,
or could be improved. Feel free to [submit a new
issue](https://github.com/rust-lang-nursery/mdBook/issues) explaining what you
have in mind and I will take a look at it.
You could also create a pull-request with the proposed improvements.

View File

@@ -1,8 +1,7 @@
# Contributors
Here is a list of the contributors who have helped improving mdBook. Big shout-out to them!
If you have contributed to mdBook and I forgot to add you, don't hesitate to add yourself to the list. If you are in the list, feel free to add your real name & contact information if you wish.
Here is a list of the contributors who have helped improving mdBook. Big
shout-out to them!
- [mdinger](https://github.com/mdinger)
- Kevin ([kbknapp](https://github.com/kbknapp))
@@ -12,8 +11,10 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add
- [funnkill](https://github.com/funkill)
- Fu Gangqiang ([FuGangqiang](https://github.com/FuGangqiang))
- [Michael-F-Bryan](https://github.com/Michael-F-Bryan)
- [Chris Spiegel](https://github.com/cspiegel)
- Chris Spiegel ([cspiegel](https://github.com/cspiegel))
- [projektir](https://github.com/projektir)
- [Phaiax](https://github.com/Phaiax)
- [Matt Ickstadt](https://github.com/mattico)
- Matt Ickstadt ([mattico](https://github.com/mattico))
- Weihang Lo ([@weihanglo](https://github.com/weihanglo))
If you feel you're missing from this list, feel free to add yourself in a PR.

View File

@@ -1,47 +0,0 @@
set -ex
main() {
local target=
if [ $TRAVIS_OS_NAME = linux ]; then
target=x86_64-unknown-linux-musl
sort=sort
else
target=x86_64-apple-darwin
sort=gsort # for `sort --sort-version`, from brew's coreutils.
fi
# Builds for iOS are done on OSX, but require the specific target to be
# installed.
case $TARGET in
aarch64-apple-ios)
rustup target install aarch64-apple-ios
;;
armv7-apple-ios)
rustup target install armv7-apple-ios
;;
armv7s-apple-ios)
rustup target install armv7s-apple-ios
;;
i386-apple-ios)
rustup target install i386-apple-ios
;;
x86_64-apple-ios)
rustup target install x86_64-apple-ios
;;
esac
# This fetches latest stable release
local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \
| cut -d/ -f3 \
| grep -E '^v[0.1.0-9.]+$' \
| $sort --version-sort \
| tail -n1)
curl -LSfs https://japaric.github.io/trust/install.sh | \
sh -s -- \
--force \
--git japaric/cross \
--tag $tag \
--target $target
}
main

View File

@@ -1,4 +1,6 @@
//! This program removes all forms of emphasis from the markdown of the book.
//! An example preprocessor for removing all forms of emphasis from a markdown
//! book.
extern crate mdbook;
extern crate pulldown_cmark;
extern crate pulldown_cmark_to_cmark;
@@ -6,89 +8,73 @@ extern crate pulldown_cmark_to_cmark;
use mdbook::book::{Book, BookItem, Chapter};
use mdbook::errors::{Error, Result};
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::MDBook;
use pulldown_cmark::{Event, Parser, Tag};
use pulldown_cmark_to_cmark::fmt::cmark;
use std::env::{args, args_os};
use std::ffi::OsString;
use std::process;
const NAME: &str = "md-links-to-html-links";
fn main() {
panic!("This example is intended to be part of a library");
}
#[allow(dead_code)]
struct Deemphasize;
impl Preprocessor for Deemphasize {
fn name(&self) -> &str {
"md-links-to-html-links"
NAME
}
fn run(&self, _ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
eprintln!("Running '{}' preprocessor", self.name());
let mut res: Option<_> = None;
let mut num_removed_items = 0;
book.for_each_mut(|item: &mut BookItem| {
if let Some(Err(_)) = res {
return;
}
if let BookItem::Chapter(ref mut chapter) = *item {
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
res = Some(
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
Ok(md) => {
chapter.content = md;
Ok(())
}
Err(err) => Err(err),
},
);
}
});
process(&mut book.sections, &mut num_removed_items)?;
eprintln!(
"{}: removed {} events from markdown stream.",
self.name(),
num_removed_items
);
match res {
Some(res) => res,
None => Ok(()),
Ok(book)
}
}
fn process<'a, I>(items: I, num_removed_items: &mut usize) -> Result<()>
where
I: IntoIterator<Item = &'a mut BookItem> + 'a,
{
for item in items {
if let BookItem::Chapter(ref mut chapter) = *item {
eprintln!("{}: processing chapter '{}'", NAME, chapter.name);
let md = remove_emphasis(num_removed_items, chapter)?;
chapter.content = md;
}
}
Ok(())
}
fn do_it(book: OsString) -> Result<()> {
let mut book = MDBook::load(book)?;
book.with_preprecessor(Deemphasize);
book.build()
}
fn remove_emphasis(num_removed_items: &mut usize, chapter: &mut Chapter) -> Result<String> {
let mut buf = String::with_capacity(chapter.content.len());
fn main() {
if args_os().count() != 2 {
eprintln!("USAGE: {} <book>", args().next().expect("executable"));
return;
}
if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) {
eprintln!("{}", e);
process::exit(1);
}
}
let events = Parser::new(&chapter.content).filter(|e| {
let should_keep = match *e {
Event::Start(Tag::Emphasis)
| Event::Start(Tag::Strong)
| Event::End(Tag::Emphasis)
| Event::End(Tag::Strong) => false,
_ => true,
};
if !should_keep {
*num_removed_items += 1;
}
should_keep
});
impl Deemphasize {
fn remove_emphasis(num_removed_items: &mut i32, chapter: &mut Chapter) -> Result<String> {
let mut buf = String::with_capacity(chapter.content.len());
let events = Parser::new(&chapter.content).filter(|e| {
let should_keep = match *e {
Event::Start(Tag::Emphasis)
| Event::Start(Tag::Strong)
| Event::End(Tag::Emphasis)
| Event::End(Tag::Strong) => false,
_ => true,
};
if !should_keep {
*num_removed_items += 1;
}
should_keep
});
cmark(events, &mut buf, None)
.map(|_| buf)
.map_err(|err| Error::from(format!("Markdown serialization failed: {}", err)))
}
cmark(events, &mut buf, None)
.map(|_| buf)
.map_err(|err| Error::from(format!("Markdown serialization failed: {}", err)))
}

View File

@@ -0,0 +1,108 @@
extern crate clap;
extern crate mdbook;
extern crate serde_json;
use clap::{App, Arg, ArgMatches, SubCommand};
use mdbook::book::Book;
use mdbook::errors::Error;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
use nop_lib::Nop;
use std::io;
use std::process;
pub fn make_app() -> App<'static, 'static> {
App::new("nop-preprocessor")
.about("A mdbook preprocessor which does precisely nothing")
.subcommand(
SubCommand::with_name("supports")
.arg(Arg::with_name("renderer").required(true))
.about("Check whether a renderer is supported by this preprocessor"),
)
}
fn main() {
let matches = make_app().get_matches();
// Users will want to construct their own preprocessor here
let preprocessor = Nop::new();
if let Some(sub_args) = matches.subcommand_matches("supports") {
handle_supports(&preprocessor, sub_args);
} else {
if let Err(e) = handle_preprocessing(&preprocessor) {
eprintln!("{}", e);
process::exit(1);
}
}
}
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
// We should probably use the `semver` crate to check compatibility
// here...
eprintln!(
"Warning: The {} plugin was built against version {} of mdbook, \
but we're being called from version {}",
pre.name(),
mdbook::MDBOOK_VERSION,
ctx.mdbook_version
);
}
let processed_book = pre.run(&ctx, book)?;
serde_json::to_writer(io::stdout(), &processed_book)?;
Ok(())
}
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
let renderer = sub_args.value_of("renderer").expect("Required argument");
let supported = pre.supports_renderer(&renderer);
// Signal whether the renderer is supported by exiting with 1 or 0.
if supported {
process::exit(0);
} else {
process::exit(1);
}
}
/// The actual implementation of the `Nop` preprocessor. This would usually go
/// in your main `lib.rs` file.
mod nop_lib {
use super::*;
/// A no-op preprocessor.
pub struct Nop;
impl Nop {
pub fn new() -> Nop {
Nop
}
}
impl Preprocessor for Nop {
fn name(&self) -> &str {
"nop-preprocessor"
}
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book, Error> {
// In testing we want to tell the preprocessor to blow up by setting a
// particular config value
if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
if nop_cfg.contains_key("blow-up") {
return Err("Boom!!1!".into());
}
}
// we *are* a no-op preprocessor after all
Ok(book)
}
fn supports_renderer(&self, renderer: &str) -> bool {
renderer != "not-supported"
}
}
}

View File

@@ -167,9 +167,9 @@ impl Chapter {
) -> Chapter {
Chapter {
name: name.to_string(),
content: content,
content,
path: path.into(),
parent_names: parent_names,
parent_names,
..Default::default()
}
}
@@ -210,7 +210,7 @@ fn load_summary_item<P: AsRef<Path>>(
match *item {
SummaryItem::Separator => Ok(BookItem::Separator),
SummaryItem::Link(ref link) => {
load_chapter(link, src_dir, parent_names).map(|c| BookItem::Chapter(c))
load_chapter(link, src_dir, parent_names).map(BookItem::Chapter)
}
}
}
@@ -245,7 +245,8 @@ fn load_chapter<P: AsRef<Path>>(
ch.number = link.number.clone();
sub_item_parents.push(link.name.clone());
let sub_items = link.nested_items
let sub_items = link
.nested_items
.iter()
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
.collect::<Result<Vec<_>>>()?;
@@ -475,12 +476,12 @@ And here is some \
assert_eq!(got.len(), 5);
// checking the chapter names are in the order should be sufficient here...
let chapter_names: Vec<String> = got.into_iter()
let chapter_names: Vec<String> = got
.into_iter()
.filter_map(|i| match *i {
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
_ => None,
})
.collect();
}).collect();
let should_be: Vec<_> = vec![
String::from("Chapter 1"),
String::from("Hello World"),

View File

@@ -110,7 +110,8 @@ impl BookBuilder {
fn copy_across_theme(&self) -> Result<()> {
debug!("Copying theme");
let themedir = self.config
let themedir = self
.config
.html_config()
.and_then(|html| html.theme)
.unwrap_or_else(|| self.config.book.src.join("theme"));
@@ -174,7 +175,7 @@ impl BookBuilder {
let summary = src_dir.join("SUMMARY.md");
let mut f = File::create(&summary).chain_err(|| "Unable to create SUMMARY.md")?;
writeln!(f, "# Summary")?;
writeln!(f, "")?;
writeln!(f)?;
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
let chapter_1 = src_dir.join("chapter_1.md");

View File

@@ -20,7 +20,9 @@ use tempfile::Builder as TempFileBuilder;
use toml::Value;
use errors::*;
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext};
use preprocess::{
CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
};
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
use utils;
@@ -149,23 +151,36 @@ impl MDBook {
pub fn build(&self) -> Result<()> {
info!("Book building has started");
let mut preprocessed_book = self.book.clone();
let preprocess_ctx = PreprocessorContext::new(self.root.clone(), self.config.clone());
for preprocessor in &self.preprocessors {
debug!("Running the {} preprocessor.", preprocessor.name());
preprocessor.run(&preprocess_ctx, &mut preprocessed_book)?;
}
for renderer in &self.renderers {
info!("Running the {} backend", renderer.name());
self.run_renderer(&preprocessed_book, renderer.as_ref())?;
self.execute_build_process(&**renderer)?;
}
Ok(())
}
fn run_renderer(&self, preprocessed_book: &Book, renderer: &Renderer) -> Result<()> {
/// Run the entire build process for a particular `Renderer`.
fn execute_build_process(&self, renderer: &Renderer) -> Result<()> {
let mut preprocessed_book = self.book.clone();
let preprocess_ctx = PreprocessorContext::new(
self.root.clone(),
self.config.clone(),
renderer.name().to_string(),
);
for preprocessor in &self.preprocessors {
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
debug!("Running the {} preprocessor.", preprocessor.name());
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
}
}
info!("Running the {} backend", renderer.name());
self.render(&preprocessed_book, renderer)?;
Ok(())
}
fn render(&self, preprocessed_book: &Book, renderer: &Renderer) -> Result<()> {
let name = renderer.name();
let build_dir = self.build_dir_for(name);
if build_dir.exists() {
@@ -215,13 +230,15 @@ impl MDBook {
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
let preprocess_context = PreprocessorContext::new(self.root.clone(), self.config.clone());
// FIXME: Is "test" the proper renderer name to use here?
let preprocess_context =
PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
LinkPreprocessor::new().run(&preprocess_context, &mut self.book)?;
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
// Index Preprocessor is disabled so that chapter paths continue to point to the
// actual markdown files.
for item in self.iter() {
for item in book.iter() {
if let BookItem::Chapter(ref ch) = *item {
if !ch.path.as_os_str().is_empty() {
let path = self.source_dir().join(&ch.path);
@@ -330,30 +347,46 @@ fn default_preprocessors() -> Vec<Box<Preprocessor>> {
]
}
fn is_default_preprocessor(pre: &Preprocessor) -> bool {
let name = pre.name();
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
}
/// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
let preprocess_list = match config.build.preprocess {
Some(ref p) => p,
// If no preprocessor field is set, default to the LinkPreprocessor and
// IndexPreprocessor. This allows you to disable default preprocessors
// by setting "preprocess" to an empty list.
None => return Ok(default_preprocessors()),
};
let mut preprocessors = Vec::new();
let mut preprocessors: Vec<Box<Preprocessor>> = Vec::new();
if config.build.use_default_preprocessors {
preprocessors.extend(default_preprocessors());
}
for key in preprocess_list {
match key.as_ref() {
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
_ => bail!("{:?} is not a recognised preprocessor", key),
if let Some(preprocessor_table) = config.get("preprocessor").and_then(|v| v.as_table()) {
for key in preprocessor_table.keys() {
match key.as_ref() {
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
name => preprocessors.push(interpret_custom_preprocessor(
name,
&preprocessor_table[name],
)),
}
}
}
Ok(preprocessors)
}
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocessor> {
let command = table
.get("command")
.and_then(|c| c.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| format!("mdbook-{}", key));
Box::new(CmdPreprocessor::new(key.to_string(), command.to_string()))
}
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
// look for the `command` field, falling back to using the key
// prepended by "mdbook-"
let table_dot_command = table
@@ -366,6 +399,31 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
Box::new(CmdRenderer::new(key.to_string(), command.to_string()))
}
/// Check whether we should run a particular `Preprocessor` in combination
/// with the renderer, falling back to `Preprocessor::supports_renderer()`
/// method if the user doesn't say anything.
///
/// The `build.use-default-preprocessors` config option can be used to ensure
/// default preprocessors always run if they support the renderer.
fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg: &Config) -> bool {
// default preprocessors should be run by default (if supported)
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
return preprocessor.supports_renderer(renderer.name());
}
let key = format!("preprocessor.{}.renderers", preprocessor.name());
let renderer_name = renderer.name();
if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
return explicit_renderers
.iter()
.filter_map(|val| val.as_str())
.any(|name| name == renderer_name);
}
preprocessor.supports_renderer(renderer_name)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -413,8 +471,8 @@ mod tests {
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
let cfg = Config::default();
// make sure we haven't got anything in the `output` table
assert!(cfg.build.preprocess.is_none());
// make sure we haven't got anything in the `preprocessor` table
assert!(cfg.get("preprocessor").is_none());
let got = determine_preprocessors(&cfg);
@@ -425,47 +483,105 @@ mod tests {
}
#[test]
fn config_doesnt_default_if_empty() {
let cfg_str: &'static str = r#"
[book]
title = "Some Book"
fn use_default_preprocessors_works() {
let mut cfg = Config::default();
cfg.build.use_default_preprocessors = false;
[build]
build-dir = "outputs"
create-missing = false
preprocess = []
"#;
let got = determine_preprocessors(&cfg).unwrap();
let cfg = Config::from_str(cfg_str).unwrap();
// make sure we have something in the `output` table
assert!(cfg.build.preprocess.is_some());
let got = determine_preprocessors(&cfg);
assert!(got.is_ok());
assert!(got.unwrap().is_empty());
assert_eq!(got.len(), 0);
}
#[test]
fn config_complains_if_unimplemented_preprocessor() {
fn can_determine_third_party_preprocessors() {
let cfg_str: &'static str = r#"
[book]
title = "Some Book"
[preprocessor.random]
[build]
build-dir = "outputs"
create-missing = false
preprocess = ["random"]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure we have something in the `output` table
assert!(cfg.build.preprocess.is_some());
// make sure the `preprocessor.random` table exists
assert!(cfg.get_preprocessor("random").is_some());
let got = determine_preprocessors(&cfg);
let got = determine_preprocessors(&cfg).unwrap();
assert!(got.is_err());
assert!(got.into_iter().any(|p| p.name() == "random"));
}
#[test]
fn preprocessors_can_provide_their_own_commands() {
let cfg_str = r#"
[preprocessor.random]
command = "python random.py"
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure the `preprocessor.random` table exists
let random = cfg.get_preprocessor("random").unwrap();
let random = interpret_custom_preprocessor("random", &Value::Table(random.clone()));
assert_eq!(random.cmd(), "python random.py");
}
#[test]
fn config_respects_preprocessor_selection() {
let cfg_str: &'static str = r#"
[preprocessor.links]
renderers = ["html"]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// double-check that we can access preprocessor.links.renderers[0]
let html = cfg
.get_preprocessor("links")
.and_then(|links| links.get("renderers"))
.and_then(|renderers| renderers.as_array())
.and_then(|renderers| renderers.get(0))
.and_then(|renderer| renderer.as_str())
.unwrap();
assert_eq!(html, "html");
let html_renderer = HtmlHandlebars::default();
let pre = LinkPreprocessor::new();
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
assert!(should_run);
}
struct BoolPreprocessor(bool);
impl Preprocessor for BoolPreprocessor {
fn name(&self) -> &str {
"bool-preprocessor"
}
fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
unimplemented!()
}
fn supports_renderer(&self, _renderer: &str) -> bool {
self.0
}
}
#[test]
fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
let cfg = Config::default();
let html = HtmlHandlebars::new();
let should_be = true;
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
assert_eq!(got, should_be);
let should_be = false;
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
assert_eq!(got, should_be);
}
}

View File

@@ -221,11 +221,14 @@ impl<'a> SummaryParser<'a> {
fn parse(mut self) -> Result<Summary> {
let title = self.parse_title();
let prefix_chapters = self.parse_affix(true)
let prefix_chapters = self
.parse_affix(true)
.chain_err(|| "There was an error parsing the prefix chapters")?;
let numbered_chapters = self.parse_numbered()
let numbered_chapters = self
.parse_numbered()
.chain_err(|| "There was an error parsing the numbered chapters")?;
let suffix_chapters = self.parse_affix(false)
let suffix_chapters = self
.parse_affix(false)
.chain_err(|| "There was an error parsing the suffix chapters")?;
Ok(Summary {
@@ -277,7 +280,7 @@ impl<'a> SummaryParser<'a> {
Err(self.parse_error("You can't have an empty link."))
} else {
Ok(Link {
name: name,
name,
location: PathBuf::from(href.to_string()),
number: None,
nested_items: Vec::new(),
@@ -289,6 +292,7 @@ impl<'a> SummaryParser<'a> {
/// already been consumed by a previous parser.
fn parse_numbered(&mut self) -> Result<Vec<SummaryItem>> {
let mut items = Vec::new();
let mut root_items = 0;
let root_number = SectionNumber::default();
// we need to do this funny loop-match-if-let dance because a rule will
@@ -305,7 +309,8 @@ impl<'a> SummaryParser<'a> {
// if we've resumed after something like a rule the root sections
// will be numbered from 1. We need to manually go back and update
// them
update_section_numbers(&mut bunch_of_items, 0, items.len() as u32);
update_section_numbers(&mut bunch_of_items, 0, root_items);
root_items += bunch_of_items.len() as u32;
items.extend(bunch_of_items);
match self.next_event() {
@@ -472,8 +477,7 @@ fn stringify_events(events: Vec<Event>) -> String {
.filter_map(|t| match t {
Event::Text(text) => Some(text.into_owned()),
_ => None,
})
.collect()
}).collect()
}
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
@@ -722,4 +726,41 @@ mod tests {
let got = parser.parse_numbered();
assert!(got.is_err());
}
/// Regression test for https://github.com/rust-lang-nursery/mdBook/issues/779
/// Ensure section numbers are correctly incremented after a horizontal separator.
#[test]
fn keep_numbering_after_separator() {
let src =
"- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(),
}),
SummaryItem::Separator,
SummaryItem::Link(Link {
name: String::from("Second"),
location: PathBuf::from("./second.md"),
number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(),
}),
SummaryItem::Separator,
SummaryItem::Link(Link {
name: String::from("Third"),
location: PathBuf::from("./third.md"),
number: Some(SectionNumber(vec![3])),
nested_items: Vec::new(),
}),
];
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next();
let got = parser.parse_numbered().unwrap();
assert_eq!(got, should_be);
}
}

View File

@@ -9,13 +9,12 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.about("Builds a book from its markdown files")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
)
.arg_from_usage(
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
).arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
)
.arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
).arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
}
// Build command implementation

View File

@@ -10,9 +10,10 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.about("Deletes a built book")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
)
.arg_from_usage(
Relative paths are interpreted relative to the book's root directory.{n}\
Running this command deletes this directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
).arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
)

View File

@@ -12,9 +12,10 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("init")
.about("Creates the boilerplate structure and files for a new book")
// the {n} denotes a newline which will properly aligned in all help messages
.arg_from_usage("[dir] 'Directory to create the book in{n}\
(Defaults to the Current Directory when omitted)'")
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
.arg_from_usage(
"[dir] 'Directory to create the book in{n}\
(Defaults to the Current Directory when omitted)'",
).arg_from_usage("--theme 'Copies the default theme into your source folder'")
.arg_from_usage("--force 'Skips confirmation prompts'")
}

View File

@@ -22,7 +22,8 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.about("Serves a book at http://localhost:3000, and rebuilds it on changes")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
@@ -75,7 +76,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let port = args.value_of("port").unwrap();
let ws_port = args.value_of("websocket-port").unwrap();
let hostname = args.value_of("hostname").unwrap();
let public_address = args.value_of("websocket-address").unwrap_or(hostname);
let public_address = args.value_of("websocket-hostname").unwrap_or(hostname);
let open_browser = args.is_present("open");
let address = format!("{}:{}", hostname, port);
@@ -114,7 +115,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
}
#[cfg(feature = "watch")]
watch::trigger_on_change(&mut book, move |path, book_dir| {
watch::trigger_on_change(&book, move |path, book_dir| {
info!("File changed: {:?}", path);
info!("Building book...");
@@ -122,10 +123,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let result = MDBook::load(&book_dir)
.and_then(|mut b| {
b.config.set("output.html.livereload-url", &livereload_url)?;
b.config
.set("output.html.livereload-url", &livereload_url)?;
Ok(b)
})
.and_then(|b| b.build());
}).and_then(|b| b.build());
if let Err(e) = result {
error!("Unable to load the book");

View File

@@ -9,7 +9,8 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.about("Tests that a book's Rust code samples compile")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
@@ -28,7 +29,8 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
// test command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
let library_paths: Vec<&str> = args.values_of("library-path")
let library_paths: Vec<&str> = args
.values_of("library-path")
.map(|v| v.collect())
.unwrap_or_default();
let book_dir = get_book_dir(args);

View File

@@ -16,13 +16,12 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.about("Watches a book's files and rebuilds it on changes")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
)
.arg_from_usage(
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
).arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
)
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
).arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
}
// Watch command implementation

View File

@@ -209,6 +209,18 @@ impl Config {
Ok(())
}
/// Get the table associated with a particular renderer.
pub fn get_renderer<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
let key = format!("output.{}", index.as_ref());
self.get(&key).and_then(|v| v.as_table())
}
/// Get the table associated with a particular preprocessor.
pub fn get_preprocessor<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
let key = format!("preprocessor.{}", index.as_ref());
self.get(&key).and_then(|v| v.as_table())
}
fn from_legacy(mut table: Value) -> Config {
let mut cfg = Config::default();
@@ -289,8 +301,8 @@ impl<'de> Deserialize<'de> for Config {
.unwrap_or_default();
Ok(Config {
book: book,
build: build,
book,
build,
rest: Value::Table(table),
})
}
@@ -382,8 +394,9 @@ pub struct BuildConfig {
/// Should non-existent markdown files specified in `SETTINGS.md` be created
/// if they don't exist?
pub create_missing: bool,
/// Which preprocessors should be applied
pub preprocess: Option<Vec<String>>,
/// Should the default preprocessors always be used when they are
/// compatible with the renderer?
pub use_default_preprocessors: bool,
}
impl Default for BuildConfig {
@@ -391,7 +404,7 @@ impl Default for BuildConfig {
BuildConfig {
build_dir: PathBuf::from("book"),
create_missing: true,
preprocess: None,
use_default_preprocessors: true,
}
}
}
@@ -402,6 +415,8 @@ impl Default for BuildConfig {
pub struct HtmlConfig {
/// The theme directory, if specified.
pub theme: Option<PathBuf>,
/// The default theme to use, defaults to 'light'
pub default_theme: Option<String>,
/// Use "smart quotes" instead of the usual `"` character.
pub curly_quotes: bool,
/// Should mathjax be enabled?
@@ -423,10 +438,15 @@ pub struct HtmlConfig {
/// This config item *should not be edited* by the end user.
#[doc(hidden)]
pub livereload_url: Option<String>,
/// Should section labels be rendered?
/// Don't render section labels.
pub no_section_label: bool,
/// Search settings. If `None`, the default will be used.
pub search: Option<Search>,
/// Git repository url. If `None`, the git button will not be shown.
pub git_repository_url: Option<String>,
/// FontAwesome icon class to use for the Git repository link.
/// Defaults to `fa-github` if `None`.
pub git_repository_icon: Option<String>,
}
impl HtmlConfig {
@@ -534,11 +554,7 @@ trait Updateable<'de>: Serialize + Deserialize<'de> {
}
}
impl<'de, T> Updateable<'de> for T
where
T: Serialize + Deserialize<'de>,
{
}
impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
#[cfg(test)]
mod tests {
@@ -555,17 +571,24 @@ mod tests {
[build]
build-dir = "outputs"
create-missing = false
preprocess = ["first_preprocessor", "second_preprocessor"]
use-default-preprocessors = true
[output.html]
theme = "./themedir"
default-theme = "rust"
curly-quotes = true
google-analytics = "123456"
additional-css = ["./foo/bar/baz.css"]
git-repository-url = "https://foo.com/"
git-repository-icon = "fa-code-fork"
[output.html.playpen]
editable = true
editor = "ace"
[preprocess.first]
[preprocess.second]
"#;
#[test]
@@ -583,10 +606,7 @@ mod tests {
let build_should_be = BuildConfig {
build_dir: PathBuf::from("outputs"),
create_missing: false,
preprocess: Some(vec![
"first_preprocessor".to_string(),
"second_preprocessor".to_string(),
]),
use_default_preprocessors: true,
};
let playpen_should_be = Playpen {
editable: true,
@@ -597,7 +617,10 @@ mod tests {
google_analytics: Some(String::from("123456")),
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
theme: Some(PathBuf::from("./themedir")),
default_theme: Some(String::from("rust")),
playpen: playpen_should_be,
git_repository_url: Some(String::from("https://foo.com/")),
git_repository_icon: Some(String::from("fa-code-fork")),
..Default::default()
};
@@ -688,7 +711,7 @@ mod tests {
let build_should_be = BuildConfig {
build_dir: PathBuf::from("my-book"),
create_missing: true,
preprocess: None,
use_default_preprocessors: true,
};
let html_should_be = HtmlConfig {

View File

@@ -114,6 +114,12 @@ pub mod renderer;
pub mod theme;
pub mod utils;
/// The current version of `mdbook`.
///
/// This is provided as a way for custom preprocessors and renderers to do
/// compatibility checks.
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
pub use book::BookItem;
pub use book::MDBook;
pub use config::Config;

View File

@@ -20,8 +20,8 @@ use std::path::{Path, PathBuf};
mod cmd;
const NAME: &'static str = "mdBook";
const VERSION: &'static str = concat!("v", crate_version!());
const NAME: &str = "mdBook";
const VERSION: &str = concat!("v", crate_version!());
fn main() {
init_logger();

197
src/preprocess/cmd.rs Normal file
View File

@@ -0,0 +1,197 @@
use super::{Preprocessor, PreprocessorContext};
use book::Book;
use errors::*;
use serde_json;
use shlex::Shlex;
use std::io::{self, Read, Write};
use std::process::{Child, Command, Stdio};
/// A custom preprocessor which will shell out to a 3rd-party program.
///
/// # Preprocessing Protocol
///
/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will
/// execute the shell command `$cmd supports $renderer`. If the renderer is
/// supported, custom preprocessors should exit with a exit code of `0`,
/// any other exit code be considered as unsupported.
///
/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)`
/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors
/// should then "return" a processed book by printing it to `stdout` as JSON.
/// For convenience, the `CmdPreprocessor::parse_input()` function can be used
/// to parse the input provided by `mdbook`.
///
/// Exiting with a non-zero exit code while preprocessing is considered an
/// error. `stderr` is passed directly through to the user, so it can be used
/// for logging or emitting warnings if desired.
///
/// # Examples
///
/// An example preprocessor is available in this project's `examples/`
/// directory.
#[derive(Debug, Clone, PartialEq)]
pub struct CmdPreprocessor {
name: String,
cmd: String,
}
impl CmdPreprocessor {
/// Create a new `CmdPreprocessor`.
pub fn new(name: String, cmd: String) -> CmdPreprocessor {
CmdPreprocessor { name, cmd }
}
/// A convenience function custom preprocessors can use to parse the input
/// written to `stdin` by a `CmdRenderer`.
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
serde_json::from_reader(reader).chain_err(|| "Unable to parse the input")
}
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
let stdin = child.stdin.take().expect("Child has stdin");
if let Err(e) = self.write_input(stdin, &book, &ctx) {
// Looks like the backend hung up before we could finish
// sending it the render context. Log the error and keep going
warn!("Error writing the RenderContext to the backend, {}", e);
}
}
fn write_input<W: Write>(
&self,
writer: W,
book: &Book,
ctx: &PreprocessorContext,
) -> Result<()> {
serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into)
}
/// The command this `Preprocessor` will invoke.
pub fn cmd(&self) -> &str {
&self.cmd
}
fn command(&self) -> Result<Command> {
let mut words = Shlex::new(&self.cmd);
let executable = match words.next() {
Some(e) => e,
None => bail!("Command string was empty"),
};
let mut cmd = Command::new(executable);
for arg in words {
cmd.arg(arg);
}
Ok(cmd)
}
}
impl Preprocessor for CmdPreprocessor {
fn name(&self) -> &str {
&self.name
}
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
let mut cmd = self.command()?;
let mut child = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.chain_err(|| {
format!(
"Unable to start the \"{}\" preprocessor. Is it installed?",
self.name()
)
})?;
self.write_input_to_child(&mut child, &book, ctx);
let output = child
.wait_with_output()
.chain_err(|| "Error waiting for the preprocessor to complete")?;
trace!("{} exited with output: {:?}", self.cmd, output);
ensure!(
output.status.success(),
"The preprocessor exited unsuccessfully"
);
serde_json::from_slice(&output.stdout).chain_err(|| "Unable to parse the preprocessed book")
}
fn supports_renderer(&self, renderer: &str) -> bool {
debug!(
"Checking if the \"{}\" preprocessor supports \"{}\"",
self.name(),
renderer
);
let mut cmd = match self.command() {
Ok(c) => c,
Err(e) => {
warn!(
"Unable to create the command for the \"{}\" preprocessor, {}",
self.name(),
e
);
return false;
}
};
let outcome = cmd
.arg("supports")
.arg(renderer)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map(|status| status.code() == Some(0));
if let Err(ref e) = outcome {
if e.kind() == io::ErrorKind::NotFound {
warn!(
"The command wasn't found, is the \"{}\" preprocessor installed?",
self.name
);
warn!("\tCommand: {}", self.cmd);
}
}
outcome.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
use MDBook;
fn book_example() -> MDBook {
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
MDBook::load(example).unwrap()
}
#[test]
fn round_trip_write_and_parse_input() {
let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string());
let md = book_example();
let ctx = PreprocessorContext::new(
md.root.clone(),
md.config.clone(),
"some-renderer".to_string(),
);
let mut buffer = Vec::new();
cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
assert_eq!(got_book, md.book);
assert_eq!(got_ctx, ctx);
}
}

View File

@@ -7,10 +7,12 @@ use super::{Preprocessor, PreprocessorContext};
use book::{Book, BookItem};
/// A preprocessor for converting file name `README.md` to `index.md` since
/// `README.md` is the de facto index file in a markdown-based documentation.
/// `README.md` is the de facto index file in markdown-based documentation.
pub struct IndexPreprocessor;
impl IndexPreprocessor {
pub(crate) const NAME: &'static str = "index";
/// Create a new `IndexPreprocessor`.
pub fn new() -> Self {
IndexPreprocessor
@@ -19,10 +21,10 @@ impl IndexPreprocessor {
impl Preprocessor for IndexPreprocessor {
fn name(&self) -> &str {
"index"
Self::NAME
}
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
let source_dir = ctx.root.join(&ctx.config.book.src);
book.for_each_mut(|section: &mut BookItem| {
if let BookItem::Chapter(ref mut ch) = *section {
@@ -37,7 +39,7 @@ impl Preprocessor for IndexPreprocessor {
}
});
Ok(())
Ok(book)
}
}

View File

@@ -16,6 +16,8 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
pub struct LinkPreprocessor;
impl LinkPreprocessor {
pub(crate) const NAME: &'static str = "links";
/// Create a new `LinkPreprocessor`.
pub fn new() -> Self {
LinkPreprocessor
@@ -24,10 +26,10 @@ impl LinkPreprocessor {
impl Preprocessor for LinkPreprocessor {
fn name(&self) -> &str {
"links"
Self::NAME
}
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
let src_dir = ctx.root.join(&ctx.config.book.src);
book.for_each_mut(|section: &mut BookItem| {
@@ -43,7 +45,7 @@ impl Preprocessor for LinkPreprocessor {
}
});
Ok(())
Ok(book)
}
}
@@ -81,6 +83,10 @@ where
}
Err(e) => {
error!("Error updating \"{}\", {}", playpen.link_text, e);
for cause in e.iter().skip(1) {
warn!("Caused By: {}", cause);
}
// This should make sure we include the raw `{{# ... }}` snippet
// in the page content if there are any errors.
previous_end_index = playpen.start_index;
@@ -136,27 +142,21 @@ fn parse_include_path(path: &str) -> LinkType<'static> {
let end = end.and_then(|s| s.parse::<usize>().ok());
match start {
Some(start) => match end {
Some(end) => LinkType::IncludeRange(
path,
Range {
start: start,
end: end,
},
),
Some(end) => LinkType::IncludeRange(path, Range { start, end }),
None => if has_end {
LinkType::IncludeRangeFrom(path, RangeFrom { start: start })
LinkType::IncludeRangeFrom(path, RangeFrom { start })
} else {
LinkType::IncludeRange(
path,
Range {
start: start,
start,
end: start + 1,
},
)
},
},
None => match end {
Some(end) => LinkType::IncludeRangeTo(path, RangeTo { end: end }),
Some(end) => LinkType::IncludeRangeTo(path, RangeTo { end }),
None => LinkType::IncludeRangeFull(path, RangeFull),
},
}
@@ -205,20 +205,66 @@ impl<'a> Link<'a> {
match self.link {
// omit the escape char
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
LinkType::IncludeRange(ref pat, ref range) => file_to_string(base.join(pat))
.map(|s| take_lines(&s, range.clone()))
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
LinkType::IncludeRangeFrom(ref pat, ref range) => file_to_string(base.join(pat))
.map(|s| take_lines(&s, range.clone()))
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
LinkType::IncludeRangeTo(ref pat, ref range) => file_to_string(base.join(pat))
.map(|s| take_lines(&s, range.clone()))
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
LinkType::IncludeRangeFull(ref pat, _) => file_to_string(base.join(pat))
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
LinkType::IncludeRange(ref pat, ref range) => {
let target = base.join(pat);
file_to_string(&target)
.map(|s| take_lines(&s, range.clone()))
.chain_err(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
target.display(),
)
})
}
LinkType::IncludeRangeFrom(ref pat, ref range) => {
let target = base.join(pat);
file_to_string(&target)
.map(|s| take_lines(&s, range.clone()))
.chain_err(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
target.display(),
)
})
}
LinkType::IncludeRangeTo(ref pat, ref range) => {
let target = base.join(pat);
file_to_string(&target)
.map(|s| take_lines(&s, *range))
.chain_err(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
target.display(),
)
})
}
LinkType::IncludeRangeFull(ref pat, _) => {
let target = base.join(pat);
file_to_string(&target).chain_err(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
target.display()
)
})
}
LinkType::Playpen(ref pat, ref attrs) => {
let contents = file_to_string(base.join(pat))
.chain_err(|| format!("Could not read file for link {}", self.link_text))?;
let target = base.join(pat);
let contents = file_to_string(&target).chain_err(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
target.display()
)
})?;
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
Ok(format!(
"```{}{}\n{}\n```\n",

View File

@@ -1,8 +1,10 @@
//! Book preprocessing.
pub use self::cmd::CmdPreprocessor;
pub use self::index::IndexPreprocessor;
pub use self::links::LinkPreprocessor;
mod cmd;
mod index;
mod links;
@@ -14,17 +16,30 @@ use std::path::PathBuf;
/// Extra information for a `Preprocessor` to give them more context when
/// processing a book.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PreprocessorContext {
/// The location of the book directory on disk.
pub root: PathBuf,
/// The book configuration (`book.toml`).
pub config: Config,
/// The `Renderer` this preprocessor is being used with.
pub renderer: String,
/// The calling `mdbook` version.
pub mdbook_version: String,
#[serde(skip)]
__non_exhaustive: (),
}
impl PreprocessorContext {
/// Create a new `PreprocessorContext`.
pub(crate) fn new(root: PathBuf, config: Config) -> Self {
PreprocessorContext { root, config }
pub(crate) fn new(root: PathBuf, config: Config, renderer: String) -> Self {
PreprocessorContext {
root,
config,
renderer,
mdbook_version: ::MDBOOK_VERSION.to_string(),
__non_exhaustive: (),
}
}
}
@@ -36,5 +51,13 @@ pub trait Preprocessor {
/// Run this `Preprocessor`, allowing it to update the book before it is
/// given to a renderer.
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>;
/// A hint to `MDBook` whether this preprocessor is compatible with a
/// particular renderer.
///
/// By default, always returns `true`.
fn supports_renderer(&self, _renderer: &str) -> bool {
true
}
}

View File

@@ -30,73 +30,71 @@ impl HtmlHandlebars {
print_content: &mut String,
) -> Result<()> {
// FIXME: This should be made DRY-er and rely less on mutable state
match *item {
BookItem::Chapter(ref ch) => {
let content = ch.content.clone();
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
print_content.push_str(&content);
if let BookItem::Chapter(ref ch) = *item {
let content = ch.content.clone();
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
// Update the context with data for this file
let path = ch.path
.to_str()
.chain_err(|| "Could not convert path to str")?;
let filepath = Path::new(&ch.path).with_extension("html");
let string_path = ch.path.parent().unwrap().display().to_string();
// "print.html" is used for the print page.
if ch.path == Path::new("print.md") {
bail!(ErrorKind::ReservedFilenameError(ch.path.clone()));
};
let fixed_content = utils::render_markdown_with_base(&ch.content, ctx.html_config.curly_quotes, &string_path);
print_content.push_str(&fixed_content);
// Non-lexical lifetimes needed :'(
let title: String;
{
let book_title = ctx.data
.get("book_title")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
title = ch.name.clone() + " - " + book_title;
}
// Update the context with data for this file
let path = ch
.path
.to_str()
.chain_err(|| "Could not convert path to str")?;
let filepath = Path::new(&ch.path).with_extension("html");
ctx.data.insert("path".to_owned(), json!(path));
ctx.data.insert("content".to_owned(), json!(content));
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
ctx.data.insert("title".to_owned(), json!(title));
ctx.data.insert(
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(&ch.path)),
);
// "print.html" is used for the print page.
if ch.path == Path::new("print.md") {
bail!(ErrorKind::ReservedFilenameError(ch.path.clone()));
};
// Render the handlebars template with the data
debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?;
let rendered = self.post_process(rendered, &ctx.html_config.playpen);
// Write to file
debug!("Creating {}", filepath.display());
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
if ctx.is_index {
ctx.data.insert("path".to_owned(), json!("index.html"));
ctx.data.insert("path_to_root".to_owned(), json!(""));
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
let rendered_index =
self.post_process(rendered_index, &ctx.html_config.playpen);
debug!("Creating index.html from {}", path);
utils::fs::write_file(
&ctx.destination,
"index.html",
rendered_index.as_bytes(),
)?;
}
// Non-lexical lifetimes needed :'(
let title: String;
{
let book_title = ctx
.data
.get("book_title")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
title = ch.name.clone() + " - " + book_title;
}
ctx.data.insert("path".to_owned(), json!(path));
ctx.data.insert("content".to_owned(), json!(content));
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
ctx.data.insert("title".to_owned(), json!(title));
ctx.data.insert(
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(&ch.path)),
);
// Render the handlebars template with the data
debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?;
let rendered = self.post_process(rendered, &ctx.html_config.playpen);
// Write to file
debug!("Creating {}", filepath.display());
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
if ctx.is_index {
ctx.data.insert("path".to_owned(), json!("index.html"));
ctx.data.insert("path_to_root".to_owned(), json!(""));
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
let rendered_index = self.post_process(rendered_index, &ctx.html_config.playpen);
debug!("Creating index.html from {}", path);
utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
}
_ => {}
}
Ok(())
}
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String {
let rendered = build_header_links(&rendered);
let rendered = fix_code_blocks(&rendered);
@@ -212,6 +210,7 @@ impl HtmlHandlebars {
);
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
handlebars.register_helper("next", Box::new(helpers::navigation::next));
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
}
/// Copy across any additional CSS and JavaScript files which the book
@@ -325,7 +324,7 @@ impl Renderer for HtmlHandlebars {
handlebars: &handlebars,
destination: destination.to_path_buf(),
data: data.clone(),
is_index: is_index,
is_index,
html_config: html_config.clone(),
};
self.render_item(item, ctx, &mut print_content)?;
@@ -393,6 +392,12 @@ fn make_data(
data.insert("livereload".to_owned(), json!(livereload));
}
let default_theme = match html_config.default_theme {
Some(ref theme) => theme,
None => "light",
};
data.insert("default_theme".to_owned(), json!(default_theme));
// Add google analytics tag
if let Some(ref ga) = config.html_config().and_then(|html| html.google_analytics) {
data.insert("google_analytics".to_owned(), json!(ga));
@@ -452,6 +457,15 @@ fn make_data(
)
}
if let Some(ref git_repository_url) = html_config.git_repository_url {
data.insert("git_repository_url".to_owned(), json!(git_repository_url));
}
let git_repository_icon = match html_config.git_repository_icon {
Some(ref git_repository_icon) => git_repository_icon,
None => "fa-github",
};
data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
let mut chapters = vec![];
for item in book.iter() {
@@ -465,7 +479,8 @@ fn make_data(
}
chapter.insert("name".to_owned(), json!(ch.name));
let path = ch.path
let path = ch
.path
.to_str()
.chain_err(|| "Could not convert path to str")?;
chapter.insert("path".to_owned(), json!(path));
@@ -497,8 +512,7 @@ fn build_header_links(html: &str) -> String {
.expect("Regex should ensure we only ever get numbers here");
wrap_header_with_link(level, &caps[2], &mut id_counter)
})
.into_owned()
}).into_owned()
}
/// Wraps a single header tag with a link, making sure each tag gets its own
@@ -549,8 +563,7 @@ fn fix_code_blocks(html: &str) -> String {
classes = classes,
after = after
)
})
.into_owned()
}).into_owned()
}
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
@@ -586,8 +599,7 @@ fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
// not language-rust, so no-op
text.to_owned()
}
})
.into_owned()
}).into_owned()
}
fn partition_source(s: &str) -> (String, String) {

View File

@@ -1,2 +1,3 @@
pub mod navigation;
pub mod theme;
pub mod toc;

View File

@@ -1,7 +1,7 @@
use std::collections::BTreeMap;
use std::path::Path;
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
use serde_json;
use utils;
@@ -18,13 +18,13 @@ impl Target {
/// Returns target if found.
fn find(
&self,
base_path: &String,
current_path: &String,
base_path: &str,
current_path: &str,
current_item: &StringMap,
previous_item: &StringMap,
) -> Result<Option<StringMap>, RenderError> {
match self {
&Target::Next => {
match *self {
Target::Next => {
let previous_path = previous_item
.get("path")
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
@@ -34,7 +34,7 @@ impl Target {
}
}
&Target::Previous => {
Target::Previous => {
if current_path == base_path {
return Ok(Some(previous_item.clone()));
}
@@ -45,15 +45,20 @@ impl Target {
}
}
fn find_chapter(rc: &mut RenderContext, target: Target) -> Result<Option<StringMap>, RenderError> {
fn find_chapter(
ctx: &Context,
rc: &mut RenderContext,
target: Target,
) -> Result<Option<StringMap>, RenderError> {
debug!("Get data from context");
let chapters = rc.evaluate_absolute("chapters", true).and_then(|c| {
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
let base_path = rc.evaluate_absolute("path", true)?
let base_path = rc
.evaluate_absolute(ctx, "path", true)?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
@@ -83,13 +88,16 @@ fn find_chapter(rc: &mut RenderContext, target: Target) -> Result<Option<StringM
fn render(
_h: &Helper,
r: &Handlebars,
ctx: &Context,
rc: &mut RenderContext,
out: &mut Output,
chapter: &StringMap,
) -> Result<(), RenderError> {
trace!("Creating BTreeMap to inject in context");
let mut context = BTreeMap::new();
let base_path = rc.evaluate_absolute("path", false)?
let base_path = rc
.evaluate_absolute(ctx, "path", false)?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
@@ -120,28 +128,41 @@ fn render(
_h.template()
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
.and_then(|t| {
let mut local_rc = rc.with_context(Context::wraps(&context)?);
t.render(r, &mut local_rc)
let mut local_rc = rc.new_for_block();
let local_ctx = Context::wraps(&context)?;
t.render(r, &local_ctx, &mut local_rc, out)
})?;
Ok(())
}
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
pub fn previous(
_h: &Helper,
r: &Handlebars,
ctx: &Context,
rc: &mut RenderContext,
out: &mut Output,
) -> Result<(), RenderError> {
trace!("previous (handlebars helper)");
if let Some(previous) = find_chapter(rc, Target::Previous)? {
render(_h, r, rc, &previous)?;
if let Some(previous) = find_chapter(ctx, rc, Target::Previous)? {
render(_h, r, ctx, rc, out, &previous)?;
}
Ok(())
}
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
pub fn next(
_h: &Helper,
r: &Handlebars,
ctx: &Context,
rc: &mut RenderContext,
out: &mut Output,
) -> Result<(), RenderError> {
trace!("next (handlebars helper)");
if let Some(next) = find_chapter(rc, Target::Next)? {
render(_h, r, rc, &next)?;
if let Some(next) = find_chapter(ctx, rc, Target::Next)? {
render(_h, r, ctx, rc, out, &next)?;
}
Ok(())

View File

@@ -0,0 +1,30 @@
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
pub fn theme_option(
h: &Helper,
_r: &Handlebars,
ctx: &Context,
rc: &mut RenderContext,
out: &mut Output,
) -> Result<(), RenderError> {
trace!("theme_option (handlebars helper)");
let param = h
.param(0)
.and_then(|v| v.value().as_str())
.ok_or(RenderError::new(
"Param 0 with String type is required for theme_option helper.",
))?;
let theme_name = rc
.evaluate_absolute(ctx, "default_theme", true)?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `default_theme`, string expected"))?;
out.write(param)?;
if param.to_lowercase() == theme_name.to_lowercase() {
out.write(" (default)")?;
}
Ok(())
}

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use utils;
use handlebars::{Handlebars, Helper, HelperDef, RenderContext, RenderError};
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
use pulldown_cmark::{html, Event, Parser, Tag};
use serde_json;
@@ -14,27 +14,35 @@ pub struct RenderToc {
}
impl HelperDef for RenderToc {
fn call(&self, _h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
fn call<'reg: 'rc, 'rc>(
&self,
_h: &Helper,
_: &Handlebars,
ctx: &Context,
rc: &mut RenderContext,
out: &mut Output,
) -> Result<(), RenderError> {
// get value from context data
// rc.get_path() is current json parent path, you should always use it like this
// param is the key of value you want to display
let chapters = rc.evaluate_absolute("chapters", true).and_then(|c| {
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
let current = rc.evaluate_absolute("path", true)?
let current = rc
.evaluate_absolute(ctx, "path", true)?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
rc.writer.write_all(b"<ol class=\"chapter\">")?;
out.write("<ol class=\"chapter\">")?;
let mut current_level = 1;
for item in chapters {
// Spacer
if item.get("spacer").is_some() {
rc.writer.write_all(b"<li class=\"spacer\"></li>")?;
out.write("<li class=\"spacer\"></li>")?;
continue;
}
@@ -46,30 +54,30 @@ impl HelperDef for RenderToc {
if level > current_level {
while level > current_level {
rc.writer.write_all(b"<li>")?;
rc.writer.write_all(b"<ol class=\"section\">")?;
out.write("<li>")?;
out.write("<ol class=\"section\">")?;
current_level += 1;
}
rc.writer.write_all(b"<li>")?;
out.write("<li>")?;
} else if level < current_level {
while level < current_level {
rc.writer.write_all(b"</ol>")?;
rc.writer.write_all(b"</li>")?;
out.write("</ol>")?;
out.write("</li>")?;
current_level -= 1;
}
rc.writer.write_all(b"<li>")?;
out.write("<li>")?;
} else {
rc.writer.write_all(b"<li")?;
out.write("<li")?;
if item.get("section").is_none() {
rc.writer.write_all(b" class=\"affix\"")?;
out.write(" class=\"affix\"")?;
}
rc.writer.write_all(b">")?;
out.write(">")?;
}
// Link
let path_exists = if let Some(path) = item.get("path") {
if !path.is_empty() {
rc.writer.write_all(b"<a href=\"")?;
out.write("<a href=\"")?;
let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)"))
.with_extension("html")
@@ -79,16 +87,15 @@ impl HelperDef for RenderToc {
.replace("\\", "/");
// Add link
rc.writer
.write_all(&utils::fs::path_to_root(&current).as_bytes())?;
rc.writer.write_all(tmp.as_bytes())?;
rc.writer.write_all(b"\"")?;
out.write(&utils::fs::path_to_root(&current))?;
out.write(&tmp)?;
out.write("\"")?;
if path == &current {
rc.writer.write_all(b" class=\"active\"")?;
out.write(" class=\"active\"")?;
}
rc.writer.write_all(b">")?;
out.write(">")?;
true
} else {
false
@@ -100,9 +107,9 @@ impl HelperDef for RenderToc {
if !self.no_section_label {
// Section does not necessarily exist
if let Some(section) = item.get("section") {
rc.writer.write_all(b"<strong aria-hidden=\"true\">")?;
rc.writer.write_all(section.as_bytes())?;
rc.writer.write_all(b"</strong> ")?;
out.write("<strong aria-hidden=\"true\">")?;
out.write(&section)?;
out.write("</strong> ")?;
}
}
@@ -123,22 +130,22 @@ impl HelperDef for RenderToc {
html::push_html(&mut markdown_parsed_name, parser);
// write to the handlebars template
rc.writer.write_all(markdown_parsed_name.as_bytes())?;
out.write(&markdown_parsed_name)?;
}
if path_exists {
rc.writer.write_all(b"</a>")?;
out.write("</a>")?;
}
rc.writer.write_all(b"</li>")?;
out.write("</li>")?;
}
while current_level > 1 {
rc.writer.write_all(b"</ol>")?;
rc.writer.write_all(b"</li>")?;
out.write("</ol>")?;
out.write("</li>")?;
current_level -= 1;
}
rc.writer.write_all(b"</ol>")?;
out.write("</ol>")?;
Ok(())
}
}

View File

@@ -54,7 +54,7 @@ fn add_doc(
section_id: &Option<String>,
items: &[&str],
) {
let url = if let &Some(ref id) = section_id {
let url = if let Some(ref id) = *section_id {
Cow::Owned(format!("{}#{}", anchor_base, id))
} else {
Cow::Borrowed(anchor_base)
@@ -74,8 +74,8 @@ fn render_item(
doc_urls: &mut Vec<String>,
item: &BookItem,
) -> Result<()> {
let chapter = match item {
&BookItem::Chapter(ref ch) => ch,
let chapter = match *item {
BookItem::Chapter(ref ch) => ch,
_ => return Ok(()),
};
@@ -101,7 +101,7 @@ fn render_item(
for event in p {
match event {
Event::Start(Tag::Header(i)) if i <= max_section_depth => {
if heading.len() > 0 {
if !heading.is_empty() {
// Section finished, the next header is following now
// Write the data to the index, and clear it for the next section
add_doc(
@@ -155,7 +155,7 @@ fn render_item(
}
}
if heading.len() > 0 {
if !heading.is_empty() {
// Make sure the last section is added to the index
add_doc(
index,

View File

@@ -8,7 +8,7 @@
//!
//! The definition for [RenderContext] may be useful though.
//!
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/lib/index.html
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/for_developers/index.html
//! [RenderContext]: struct.RenderContext.html
pub use self::html_handlebars::HtmlHandlebars;
@@ -26,8 +26,6 @@ use book::Book;
use config::Config;
use errors::*;
const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
/// An arbitrary `mdbook` backend.
///
/// Although it's quite possible for you to import `mdbook` as a library and
@@ -66,6 +64,8 @@ pub struct RenderContext {
/// renderers to cache intermediate results, this directory is not
/// guaranteed to be empty or even exist.
pub destination: PathBuf,
#[serde(skip)]
__non_exhaustive: (),
}
impl RenderContext {
@@ -76,11 +76,12 @@ impl RenderContext {
Q: Into<PathBuf>,
{
RenderContext {
book: book,
config: config,
version: MDBOOK_VERSION.to_string(),
book,
config,
version: ::MDBOOK_VERSION.to_string(),
root: root.into(),
destination: destination.into(),
__non_exhaustive: (),
}
}
@@ -157,7 +158,8 @@ impl Renderer for CmdRenderer {
let _ = fs::create_dir_all(&ctx.destination);
let mut child = match self.compose_command()?
let mut child = match self
.compose_command()?
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 434 KiB

View File

@@ -352,7 +352,7 @@ function playpen_text(playpen) {
var previousTheme;
try { previousTheme = localStorage.getItem('mdbook-theme'); } catch (e) { }
if (previousTheme === null || previousTheme === undefined) { previousTheme = 'light'; }
if (previousTheme === null || previousTheme === undefined) { previousTheme = default_theme; }
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
@@ -364,7 +364,7 @@ function playpen_text(playpen) {
// Set theme
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = 'light'; }
if (theme === null || theme === undefined) { theme = default_theme; }
set_theme(theme);

View File

@@ -63,9 +63,12 @@ a > .hljs {
margin: 0;
}
#print-button {
.right-buttons {
margin: 0 15px;
}
.right-buttons a {
text-decoration: none;
}
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
transform: translateY(-60px);

View File

@@ -10,7 +10,7 @@
/* Themes */
.ayu {
--bg: #0f1419;
--bg: hsl(210, 25%, 8%);
--fg: #c5c5c5;
--sidebar-bg: #14191f;
@@ -32,12 +32,12 @@
--theme-popup-border: #5c6773;
--theme-hover: #191f26;
--quote-bg: #262933;
--quote-border: lighten(var(--quote-bg), 5%);
--quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%);
--table-border-color: lighten(var(--bg), 5%);
--table-header-bg: lighten(var(--bg), 20%);
--table-alternate-bg: lighten(var(--bg), 3%);
--table-border-color: hsl(210, 25%, 13%);
--table-header-bg: hsl(210, 25%, 28%);
--table-alternate-bg: hsl(210, 25%, 11%);
--searchbar-border-color: #848484;
--searchbar-bg: #424242;
@@ -50,7 +50,7 @@
}
.coal {
--bg: #141617;
--bg: hsl(200, 7%, 8%);
--fg: #98a3ad;
--sidebar-bg: #292c2f;
@@ -72,12 +72,12 @@
--theme-popup-border: #43484d;
--theme-hover: #1f2124;
--quote-bg: #242637;
--quote-border: lighten(var(--quote-bg), 5%);
--quote-bg: hsl(234, 21%, 18%);
--quote-border: hsl(234, 21%, 23%);
--table-border-color: lighten(var(--bg), 5%);
--table-header-bg: lighten(var(--bg), 20%);
--table-alternate-bg: lighten(var(--bg), 3%);
--table-border-color: hsl(200, 7%, 13%);
--table-header-bg: hsl(200, 7%, 28%);
--table-alternate-bg: hsl(200, 7%, 11%);
--searchbar-border-color: #aaa;
--searchbar-bg: #b7b7b7;
@@ -90,7 +90,7 @@
}
.light {
--bg: #ffffff;
--bg: hsl(0, 0%, 100%);
--fg: #333333;
--sidebar-bg: #fafafa;
@@ -112,12 +112,12 @@
--theme-popup-border: #cccccc;
--theme-hover: #e6e6e6;
--quote-bg: #f2f7f9;
--quote-border: darken(var(--quote-bg), 5%);
--quote-bg: hsl(197, 37%, 96%);
--quote-border: hsl(197, 37%, 91%);
--table-border-color: darken(var(--bg), 5%);
--table-header-bg: darken(var(--bg), 20%);
--table-alternate-bg: darken(var(--bg), 3%);
--table-border-color: hsl(0, 0%, 95%);
--table-header-bg: hsl(0, 0%, 80%);
--table-alternate-bg: hsl(0, 0%, 97%);
--searchbar-border-color: #aaa;
--searchbar-bg: #fafafa;
@@ -130,7 +130,7 @@
}
.navy {
--bg: #161923;
--bg: hsl(226, 23%, 11%);
--fg: #bcbdd0;
--sidebar-bg: #282d3f;
@@ -152,12 +152,12 @@
--theme-popup-border: #737480;
--theme-hover: #282e40;
--quote-bg: #262933;
--quote-border: lighten(var(--quote-bg), 5%);
--quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%);
--table-border-color: lighten(var(--bg), 5%);
--table-header-bg: lighten(var(--bg), 20%);
--table-alternate-bg: lighten(var(--bg), 3%);
--table-border-color: hsl(226, 23%, 16%);
--table-header-bg: hsl(226, 23%, 31%);
--table-alternate-bg: hsl(226, 23%, 14%);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
@@ -170,7 +170,7 @@
}
.rust {
--bg: #e1e1db;
--bg: hsl(60, 9%, 87%);
--fg: #262625;
--sidebar-bg: #3b2e2a;
@@ -192,12 +192,12 @@
--theme-popup-border: #b38f6b;
--theme-hover: #99908a;
--quote-bg: #c1c1bb;
--quote-border: darken(var(--quote-bg), 5%);
--quote-bg: hsl(60, 5%, 75%);
--quote-border: hsl(60, 5%, 70%);
--table-border-color: darken(var(--bg), 5%);
--table-border-color: hsl(60, 9%, 82%);
--table-header-bg: #b3a497;
--table-alternate-bg: darken(var(--bg), 3%);
--table-alternate-bg: hsl(60, 9%, 84%);
--searchbar-border-color: #aaa;
--searchbar-bg: #fafafa;

View File

@@ -27,7 +27,7 @@
<!-- Custom theme stylesheets -->
{{#each additional_css}}
<link rel="stylesheet" href="{{ this }}">
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
{{/each}}
{{#if mathjax_support}}
@@ -35,9 +35,12 @@
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}}
</head>
<body class="light">
<body class="{{ default_theme }}">
<!-- Provide site root to javascript -->
<script type="text/javascript">var path_to_root = "{{ path_to_root }}";</script>
<script type="text/javascript">
var path_to_root = "{{ path_to_root }}";
var default_theme = "{{ default_theme }}";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
@@ -59,7 +62,7 @@
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = 'light'; }
if (theme === null || theme === undefined) { theme = default_theme; }
document.body.className = theme;
document.querySelector('html').className = theme + ' js';
</script>
@@ -94,11 +97,11 @@
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light <span class="default">(default)</span></button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
<li role="none"><button role="menuitem" class="theme" id="light">{{ theme_option "Light" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">{{ theme_option "Rust" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">{{ theme_option "Coal" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">{{ theme_option "Navy" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">{{ theme_option "Ayu" }}</button></li>
</ul>
{{#if search_enabled}}
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
@@ -113,6 +116,11 @@
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
{{#if git_repository_url}}
<a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
</a>
{{/if}}
</div>
</div>
</div>
@@ -235,7 +243,7 @@
<!-- Custom JS scripts -->
{{#each additional_js}}
<script type="text/javascript" src="{{ path_to_root }}{{this}}"></script>
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script>
{{/each}}
{{#if is_print}}

View File

@@ -21,9 +21,10 @@ pub fn collapse_whitespace<'a>(text: &'a str) -> Cow<'a, str> {
RE.replace_all(text, " ")
}
/// Convert the given string to a valid HTML element ID
/// Convert the given string to a valid HTML element ID.
/// The only restriction is that the ID must not contain any ASCII whitespace.
pub fn normalize_id(content: &str) -> String {
let mut ret = content
content
.chars()
.filter_map(|ch| {
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
@@ -33,16 +34,7 @@ pub fn normalize_id(content: &str) -> String {
} else {
None
}
})
.collect::<String>();
// Ensure that the first character is [A-Za-z]
if ret.chars()
.next()
.map_or(false, |c| !c.is_ascii_alphabetic())
{
ret.insert(0, 'a');
}
ret
}).collect::<String>()
}
/// Generate an ID for use with anchors which is derived from a "normalised"
@@ -74,15 +66,21 @@ pub fn id_from_content(content: &str) -> String {
normalize_id(trimmed)
}
fn adjust_links(event: Event) -> Event {
fn adjust_links<'a>(event: Event<'a>, with_base: &str) -> Event<'a> {
lazy_static! {
static ref HTTP_LINK: Regex = Regex::new("^https?://").unwrap();
static ref MD_LINK: Regex = Regex::new("(?P<link>.*).md(?P<anchor>#.*)?").unwrap();
static ref MD_LINK: Regex = Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap();
}
match event {
Event::Start(Tag::Link(dest, title)) => {
if !HTTP_LINK.is_match(&dest) {
let dest = if !with_base.is_empty() {
format!("{}/{}", with_base, dest)
} else {
dest.clone().into_owned()
};
if let Some(caps) = MD_LINK.captures(&dest) {
let mut html_link = [&caps["link"], ".html"].concat();
@@ -102,6 +100,10 @@ fn adjust_links(event: Event) -> Event {
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
render_markdown_with_base(text, curly_quotes, "")
}
pub fn render_markdown_with_base(text: &str, curly_quotes: bool, base: &str) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2);
let mut opts = Options::empty();
@@ -110,8 +112,9 @@ pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
let p = Parser::new_ext(text, opts);
let mut converter = EventQuoteConverter::new(curly_quotes);
let events = p.map(clean_codeblock_headers)
.map(adjust_links)
let events = p
.map(clean_codeblock_headers)
.map(|event| adjust_links(event, base))
.map(|event| converter.convert(event));
html::push_html(&mut s, events);
@@ -126,7 +129,7 @@ struct EventQuoteConverter {
impl EventQuoteConverter {
fn new(enabled: bool) -> Self {
EventQuoteConverter {
enabled: enabled,
enabled,
convert_text: true,
}
}
@@ -192,8 +195,7 @@ fn convert_quotes_to_curly(original_text: &str) -> String {
preceded_by_whitespace = original_char.is_whitespace();
converted_char
})
.collect()
}).collect()
}
/// Prints a "backtrace" of some `Error`.
@@ -228,6 +230,12 @@ mod tests {
render_markdown("[example_anchor](example.md#anchor)", false),
"<p><a href=\"example.html#anchor\">example_anchor</a></p>\n"
);
// this anchor contains 'md' inside of it
assert_eq!(
render_markdown("[phantom data](foo.html#phantomdata)", false),
"<p><a href=\"foo.html#phantomdata\">phantom data</a></p>\n"
);
}
#[test]
@@ -328,28 +336,42 @@ more text with spaces
#[test]
fn it_generates_anchors() {
assert_eq!(
id_from_content("## `--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes"
);
assert_eq!(
id_from_content("## Method-call expressions"),
"method-call-expressions"
);
assert_eq!(id_from_content("## **Bold** title"), "bold-title");
assert_eq!(id_from_content("## `Code` title"), "code-title");
}
#[test]
fn it_generates_anchors_from_non_ascii_initial() {
assert_eq!(
id_from_content("## `--passes`: add more rustdoc passes"),
"--passes-add-more-rustdoc-passes"
);
assert_eq!(
id_from_content("## 中文標題 CJK title"),
"中文標題-cjk-title"
);
assert_eq!(id_from_content("## Über"), "Über");
}
#[test]
fn it_normalizes_ids() {
assert_eq!(
normalize_id("`--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes"
"--passes-add-more-rustdoc-passes"
);
assert_eq!(
normalize_id("Method-call 🐙 expressions \u{1f47c}"),
"method-call--expressions-"
);
assert_eq!(normalize_id("_-_12345"), "a_-_12345");
assert_eq!(normalize_id("12345"), "a12345");
assert_eq!(normalize_id("_-_12345"), "_-_12345");
assert_eq!(normalize_id("12345"), "12345");
assert_eq!(normalize_id("中文"), "中文");
assert_eq!(normalize_id("にほんご"), "にほんご");
assert_eq!(normalize_id("한국어"), "한국어");
assert_eq!(normalize_id(""), "");
}
}

View File

@@ -11,14 +11,14 @@ use tempfile::{Builder as TempFileBuilder, TempDir};
#[test]
fn passing_alternate_backend() {
let (md, _temp) = dummy_book_with_backend("passing", "true");
let (md, _temp) = dummy_book_with_backend("passing", success_cmd());
md.build().unwrap();
}
#[test]
fn failing_alternate_backend() {
let (md, _temp) = dummy_book_with_backend("failing", "false");
let (md, _temp) = dummy_book_with_backend("failing", fail_cmd());
md.build().unwrap_err();
}
@@ -84,3 +84,19 @@ fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) {
(md, temp)
}
fn fail_cmd() -> &'static str {
if cfg!(windows) {
r#"cmd.exe /c "exit 1""#
} else {
"false"
}
}
fn success_cmd() -> &'static str {
if cfg!(windows) {
r#"cmd.exe /c "exit 0""#
} else {
"true"
}
}

80
tests/build_process.rs Normal file
View File

@@ -0,0 +1,80 @@
extern crate mdbook;
mod dummy_book;
use dummy_book::DummyBook;
use mdbook::book::Book;
use mdbook::config::Config;
use mdbook::errors::*;
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::renderer::{RenderContext, Renderer};
use mdbook::MDBook;
use std::sync::{Arc, Mutex};
struct Spy(Arc<Mutex<Inner>>);
#[derive(Debug, Default)]
struct Inner {
run_count: usize,
rendered_with: Vec<String>,
}
impl Preprocessor for Spy {
fn name(&self) -> &str {
"dummy"
}
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
let mut inner = self.0.lock().unwrap();
inner.run_count += 1;
inner.rendered_with.push(ctx.renderer.clone());
Ok(book)
}
}
impl Renderer for Spy {
fn name(&self) -> &str {
"dummy"
}
fn render(&self, _ctx: &RenderContext) -> Result<()> {
let mut inner = self.0.lock().unwrap();
inner.run_count += 1;
Ok(())
}
}
#[test]
fn mdbook_runs_preprocessors() {
let spy: Arc<Mutex<Inner>> = Default::default();
let temp = DummyBook::new().build().unwrap();
let cfg = Config::default();
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
book.with_preprecessor(Spy(Arc::clone(&spy)));
book.build().unwrap();
let inner = spy.lock().unwrap();
assert_eq!(inner.run_count, 1);
assert_eq!(inner.rendered_with.len(), 1);
assert_eq!(
"html", inner.rendered_with[0],
"We should have been run with the default HTML renderer"
);
}
#[test]
fn mdbook_runs_renderers() {
let spy: Arc<Mutex<Inner>> = Default::default();
let temp = DummyBook::new().build().unwrap();
let cfg = Config::default();
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
book.with_renderer(Spy(Arc::clone(&spy)));
book.build().unwrap();
let inner = spy.lock().unwrap();
assert_eq!(inner.run_count, 1);
}

View File

@@ -0,0 +1,58 @@
extern crate mdbook;
mod dummy_book;
use dummy_book::DummyBook;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
use mdbook::MDBook;
fn example() -> CmdPreprocessor {
CmdPreprocessor::new(
"nop-preprocessor".to_string(),
"cargo run --example nop-preprocessor --".to_string(),
)
}
#[test]
fn example_supports_whatever() {
let cmd = example();
let got = cmd.supports_renderer("whatever");
assert_eq!(got, true);
}
#[test]
fn example_doesnt_support_not_supported() {
let cmd = example();
let got = cmd.supports_renderer("not-supported");
assert_eq!(got, false);
}
#[test]
fn ask_the_preprocessor_to_blow_up() {
let dummy_book = DummyBook::new();
let temp = dummy_book.build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
md.with_preprecessor(example());
md.config
.set("preprocessor.nop-preprocessor.blow-up", true)
.unwrap();
let got = md.build();
assert!(got.is_err());
}
#[test]
fn process_the_dummy_book() {
let dummy_book = DummyBook::new();
let temp = dummy_book.build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
md.with_preprecessor(example());
md.build().unwrap();
}

View File

@@ -8,6 +8,7 @@
- [Includes](first/includes.md)
- [Recursive](first/recursive.md)
- [Second Chapter](second.md)
- [Nested Chapter](second/nested.md)
---

View File

@@ -0,0 +1,4 @@
# Testing relative links for the print page
When we link to [the first section](../first/nested.md), it should work on
both the print page and the non-print page.

View File

@@ -31,7 +31,7 @@ const TOC_TOP_LEVEL: &[&'static str] = &[
"Introduction",
];
const TOC_SECOND_LEVEL: &[&'static str] =
&["1.1. Nested Chapter", "1.2. Includes", "1.3. Recursive"];
&["1.1. Nested Chapter", "1.2. Includes", "2.1. Nested Chapter", "1.3. Recursive"];
/// Make sure you can load the dummy book and build it without panicking.
#[test]
@@ -109,6 +109,20 @@ fn check_correct_cross_links_in_nested_dir() {
);
}
#[test]
fn check_correct_relative_links_in_print_page() {
let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap();
md.build().unwrap();
let first = temp.path().join("book");
assert_contains_strings(
first.join("print.html"),
&[r##"<a href="second/../first/nested.html">the first section</a>,"##],
);
}
#[test]
fn rendered_code_has_playpen_stuff() {
let temp = DummyBook::new().build().unwrap();
@@ -174,7 +188,8 @@ fn chapter_files_were_rendered_to_html() {
.filter(|path| path.file_name().and_then(OsStr::to_str) != Some("SUMMARY.md"));
for chapter in chapter_files {
let rendered_location = temp.path()
let rendered_location = temp
.path()
.join(chapter.strip_prefix(&src).unwrap())
.with_extension("html");
assert!(
@@ -213,7 +228,8 @@ fn check_second_toc_level() {
let pred = descendants!(Class("chapter"), Name("li"), Name("li"), Name("a"));
let mut children_of_children: Vec<_> = doc.find(pred)
let mut children_of_children: Vec<_> = doc
.find(pred)
.map(|elem| elem.text().trim().to_string())
.collect();
children_of_children.sort();
@@ -231,7 +247,8 @@ fn check_first_toc_level() {
let pred = descendants!(Class("chapter"), Name("li"), Name("a"));
let mut children: Vec<_> = doc.find(pred)
let mut children: Vec<_> = doc
.find(pred)
.map(|elem| elem.text().trim().to_string())
.collect();
children.sort();
@@ -244,7 +261,8 @@ fn check_spacers() {
let doc = root_index_html().unwrap();
let should_be = 1;
let num_spacers = doc.find(Class("chapter").descendant(Name("li").and(Class("spacer"))))
let num_spacers = doc
.find(Class("chapter").descendant(Name("li").and(Class("spacer"))))
.count();
assert_eq!(num_spacers, should_be);
}
@@ -439,7 +457,7 @@ mod search {
assert_eq!(docs[&some_section]["body"], "");
assert_eq!(
docs[&summary]["body"],
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Second Chapter Conclusion"
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Second Chapter Nested Chapter Conclusion"
);
assert_eq!(docs[&summary]["breadcrumbs"], "First Chapter » Summary");
assert_eq!(docs[&conclusion]["body"], "I put &lt;HTML&gt; in here!");

View File

@@ -9,6 +9,7 @@
"first/includes.html#includes",
"first/includes.html#summary",
"second.html#second-chapter",
"second/nested.html#testing-relative-links-for-the-print-page",
"conclusion.html#conclusion"
],
"index": {
@@ -24,6 +25,11 @@
"breadcrumbs": 1,
"title": 1
},
"10": {
"body": 3,
"breadcrumbs": 1,
"title": 1
},
"2": {
"body": 2,
"breadcrumbs": 2,
@@ -50,7 +56,7 @@
"title": 1
},
"7": {
"body": 12,
"body": 14,
"breadcrumbs": 3,
"title": 1
},
@@ -60,9 +66,9 @@
"title": 2
},
"9": {
"body": 3,
"breadcrumbs": 1,
"title": 1
"body": 10,
"breadcrumbs": 7,
"title": 5
}
},
"docs": {
@@ -78,6 +84,12 @@
"id": "1",
"title": "Introduction"
},
"10": {
"body": "I put &lt;HTML&gt; in here!",
"breadcrumbs": "Conclusion",
"id": "10",
"title": "Conclusion"
},
"2": {
"body": "more text.",
"breadcrumbs": "First Chapter",
@@ -109,7 +121,7 @@
"title": "Includes"
},
"7": {
"body": "Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Second Chapter Conclusion",
"body": "Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Second Chapter Nested Chapter Conclusion",
"breadcrumbs": "First Chapter » Summary",
"id": "7",
"title": "Summary"
@@ -121,13 +133,13 @@
"title": "Second Chapter"
},
"9": {
"body": "I put &lt;HTML&gt; in here!",
"breadcrumbs": "Conclusion",
"body": "When we link to the first section , it should work on both the print page and the non-print page.",
"breadcrumbs": "Second Chapter » Testing relative links for the print page",
"id": "9",
"title": "Conclusion"
"title": "Testing relative links for the print page"
}
},
"length": 10,
"length": 11,
"save": true
},
"fields": [
@@ -206,6 +218,18 @@
}
}
}
},
"t": {
"df": 0,
"docs": {},
"h": {
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
}
}
}
},
@@ -251,7 +275,7 @@
"tf": 1.0
},
"7": {
"tf": 1.7320508075688773
"tf": 2.0
},
"8": {
"tf": 1.0
@@ -296,10 +320,10 @@
"s": {
"df": 2,
"docs": {
"7": {
"10": {
"tf": 1.0
},
"9": {
"7": {
"tf": 1.0
}
}
@@ -420,13 +444,16 @@
"df": 0,
"docs": {},
"t": {
"df": 2,
"df": 3,
"docs": {
"2": {
"tf": 1.0
},
"7": {
"tf": 1.0
},
"9": {
"tf": 1.0
}
}
}
@@ -485,7 +512,7 @@
"0": {
"tf": 1.0
},
"9": {
"10": {
"tf": 1.0
}
}
@@ -683,6 +710,14 @@
"tf": 1.0
}
}
},
"k": {
"df": 1,
"docs": {
"9": {
"tf": 1.4142135623730951
}
}
}
}
},
@@ -709,7 +744,7 @@
"t": {
"df": 1,
"docs": {
"9": {
"10": {
"tf": 1.0
}
}
@@ -791,14 +826,42 @@
"tf": 1.0
},
"7": {
"tf": 1.0
"tf": 1.4142135623730951
}
}
}
}
},
"o": {
"df": 0,
"docs": {},
"n": {
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
}
}
},
"p": {
"a": {
"df": 0,
"docs": {},
"g": {
"df": 0,
"docs": {},
"e": {
"df": 1,
"docs": {
"9": {
"tf": 1.7320508075688772
}
}
}
}
},
"df": 0,
"docs": {},
"r": {
@@ -871,8 +934,12 @@
"df": 0,
"docs": {},
"t": {
"df": 0,
"docs": {},
"df": 1,
"docs": {
"9": {
"tf": 1.7320508075688772
}
},
"l": {
"df": 0,
"docs": {},
@@ -935,7 +1002,7 @@
"t": {
"df": 1,
"docs": {
"9": {
"10": {
"tf": 1.0
}
}
@@ -967,7 +1034,15 @@
}
},
"df": 0,
"docs": {}
"docs": {},
"l": {
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
}
},
"u": {
"df": 0,
@@ -1050,13 +1125,16 @@
"df": 0,
"docs": {},
"n": {
"df": 2,
"df": 3,
"docs": {
"3": {
"tf": 1.0
},
"5": {
"tf": 1.0
},
"9": {
"tf": 1.0
}
}
}
@@ -1170,8 +1248,12 @@
"df": 0,
"docs": {}
},
"df": 0,
"docs": {}
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
}
},
"x": {
@@ -1200,6 +1282,14 @@
"r": {
"df": 0,
"docs": {},
"k": {
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
},
"l": {
"d": {
"df": 1,
@@ -1280,13 +1370,25 @@
"df": 2,
"docs": {
"0": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
},
"7": {
"tf": 1.0
}
}
}
},
"t": {
"df": 0,
"docs": {},
"h": {
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
}
}
}
},
@@ -1323,13 +1425,13 @@
"df": 0,
"docs": {},
"r": {
"df": 6,
"df": 7,
"docs": {
"2": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
},
"4": {
"tf": 1.7320508075688773
"tf": 1.7320508075688772
},
"5": {
"tf": 1.0
@@ -1338,10 +1440,13 @@
"tf": 1.0
},
"7": {
"tf": 2.0
"tf": 2.23606797749979
},
"8": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
},
"9": {
"tf": 1.0
}
}
}
@@ -1383,11 +1488,11 @@
"s": {
"df": 2,
"docs": {
"10": {
"tf": 1.4142135623730951
},
"7": {
"tf": 1.0
},
"9": {
"tf": 1.4142135623730952
}
}
}
@@ -1419,7 +1524,7 @@
"df": 2,
"docs": {
"0": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
},
"7": {
"tf": 1.0
@@ -1507,10 +1612,10 @@
"df": 0,
"docs": {},
"t": {
"df": 5,
"df": 6,
"docs": {
"2": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
},
"4": {
"tf": 1.0
@@ -1522,7 +1627,10 @@
"tf": 1.0
},
"7": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
},
"9": {
"tf": 1.0
}
}
}
@@ -1581,7 +1689,7 @@
"0": {
"tf": 1.0
},
"9": {
"10": {
"tf": 1.0
}
}
@@ -1636,7 +1744,7 @@
"df": 2,
"docs": {
"6": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
},
"7": {
"tf": 1.0
@@ -1728,7 +1836,7 @@
"df": 2,
"docs": {
"1": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
},
"7": {
"tf": 1.0
@@ -1779,6 +1887,14 @@
"tf": 1.0
}
}
},
"k": {
"df": 1,
"docs": {
"9": {
"tf": 1.7320508075688772
}
}
}
}
},
@@ -1805,7 +1921,7 @@
"t": {
"df": 1,
"docs": {
"9": {
"10": {
"tf": 1.0
}
}
@@ -1884,17 +2000,45 @@
"df": 2,
"docs": {
"4": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
},
"7": {
"tf": 1.0
"tf": 1.4142135623730951
}
}
}
}
},
"o": {
"df": 0,
"docs": {},
"n": {
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
}
}
},
"p": {
"a": {
"df": 0,
"docs": {},
"g": {
"df": 0,
"docs": {},
"e": {
"df": 1,
"docs": {
"9": {
"tf": 2.0
}
}
}
}
},
"df": 0,
"docs": {},
"r": {
@@ -1967,8 +2111,12 @@
"df": 0,
"docs": {},
"t": {
"df": 0,
"docs": {},
"df": 1,
"docs": {
"9": {
"tf": 2.0
}
},
"l": {
"df": 0,
"docs": {},
@@ -2031,7 +2179,7 @@
"t": {
"df": 1,
"docs": {
"9": {
"10": {
"tf": 1.0
}
}
@@ -2063,7 +2211,15 @@
}
},
"df": 0,
"docs": {}
"docs": {},
"l": {
"df": 1,
"docs": {
"9": {
"tf": 1.4142135623730951
}
}
}
},
"u": {
"df": 0,
@@ -2122,13 +2278,16 @@
"docs": {},
"n": {
"d": {
"df": 2,
"df": 3,
"docs": {
"7": {
"tf": 1.0
},
"8": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
},
"9": {
"tf": 1.0
}
}
},
@@ -2146,13 +2305,16 @@
"df": 0,
"docs": {},
"n": {
"df": 2,
"df": 3,
"docs": {
"3": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
},
"5": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
},
"9": {
"tf": 1.0
}
}
}
@@ -2216,7 +2378,7 @@
"df": 1,
"docs": {
"7": {
"tf": 1.4142135623730952
"tf": 1.4142135623730951
}
}
}
@@ -2266,8 +2428,12 @@
"df": 0,
"docs": {}
},
"df": 0,
"docs": {}
"df": 1,
"docs": {
"9": {
"tf": 1.4142135623730951
}
}
}
},
"x": {
@@ -2296,6 +2462,14 @@
"r": {
"df": 0,
"docs": {},
"k": {
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
},
"l": {
"d": {
"df": 1,
@@ -2388,7 +2562,7 @@
"s": {
"df": 1,
"docs": {
"9": {
"10": {
"tf": 1.0
}
}
@@ -2511,6 +2685,26 @@
}
}
},
"l": {
"df": 0,
"docs": {},
"i": {
"df": 0,
"docs": {},
"n": {
"df": 0,
"docs": {},
"k": {
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
}
}
}
},
"n": {
"df": 0,
"docs": {},
@@ -2531,6 +2725,62 @@
}
}
},
"p": {
"a": {
"df": 0,
"docs": {},
"g": {
"df": 0,
"docs": {},
"e": {
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
}
}
},
"df": 0,
"docs": {},
"r": {
"df": 0,
"docs": {},
"i": {
"df": 0,
"docs": {},
"n": {
"df": 0,
"docs": {},
"t": {
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
}
}
}
}
},
"r": {
"df": 0,
"docs": {},
"e": {
"df": 0,
"docs": {},
"l": {
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
}
}
},
"s": {
"df": 0,
"docs": {},
@@ -2609,6 +2859,26 @@
}
}
}
},
"t": {
"df": 0,
"docs": {},
"e": {
"df": 0,
"docs": {},
"s": {
"df": 0,
"docs": {},
"t": {
"df": 1,
"docs": {
"9": {
"tf": 1.0
}
}
}
}
}
}
}
}

View File

@@ -4,14 +4,8 @@ mod dummy_book;
use dummy_book::DummyBook;
use mdbook::book::Book;
use mdbook::config::Config;
use mdbook::errors::*;
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::MDBook;
use std::sync::{Arc, Mutex};
#[test]
fn mdbook_can_correctly_test_a_passing_book() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
@@ -27,30 +21,3 @@ fn mdbook_detects_book_with_failing_tests() {
assert!(md.test(vec![]).is_err());
}
#[test]
fn mdbook_runs_preprocessors() {
let has_run: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
struct DummyPreprocessor(Arc<Mutex<bool>>);
impl Preprocessor for DummyPreprocessor {
fn name(&self) -> &str {
"dummy"
}
fn run(&self, _ctx: &PreprocessorContext, _book: &mut Book) -> Result<()> {
*self.0.lock().unwrap() = true;
Ok(())
}
}
let temp = DummyBook::new().build().unwrap();
let cfg = Config::default();
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
book.with_preprecessor(DummyPreprocessor(Arc::clone(&has_run)));
book.build().unwrap();
assert!(*has_run.lock().unwrap())
}