Merge branch 'rust-lang:master' into #1564

This commit is contained in:
Kynthus Auoeau 2021-10-23 15:48:33 +09:00 committed by GitHub
commit 085ec42609
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1604 additions and 662 deletions

View File

@ -31,7 +31,7 @@ jobs:
rust: stable rust: stable
- build: msrv - build: msrv
os: ubuntu-latest os: ubuntu-latest
rust: 1.45.0 rust: 1.46.0
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Install Rust - name: Install Rust

View File

@ -1,5 +1,72 @@
# Changelog # Changelog
## mdBook 0.4.13
[e6629cd...f55028b](https://github.com/rust-lang/mdBook/compare/e6629cd...f55028b)
### Added
- Added the ability to specify the preprocessor order.
[#1607](https://github.com/rust-lang/mdBook/pull/1607)
### Fixed
- Include chapters with no headers in the search index
[#1637](https://github.com/rust-lang/mdBook/pull/1637)
- Switched to the `opener` crate for opening a web browser, which should fix
some issues with blocking.
[#1656](https://github.com/rust-lang/mdBook/pull/1656)
- Fixed clicking the border of the theme switcher breaking the theme selection.
[#1651](https://github.com/rust-lang/mdBook/pull/1651)
## mdBook 0.4.12
[14add9c...8b4e488](https://github.com/rust-lang/mdBook/compare/14add9c...8b4e488)
### Changed
- Reverted the change to update to highlight.js 11, as it broke hidden code lines.
[#1597](https://github.com/rust-lang/mdBook/pull/1621)
## mdBook 0.4.11
[e440094...2cf00d0](https://github.com/rust-lang/mdBook/compare/e440094...2cf00d0)
### Added
- Added support for Rust 2021 edition.
[#1596](https://github.com/rust-lang/mdBook/pull/1596)
- Added `mdbook completions` subcommand which provides shell completions.
[#1425](https://github.com/rust-lang/mdBook/pull/1425)
- Added `--title` and `--ignore` flags to `mdbook init` to avoid the
interactive input.
[#1559](https://github.com/rust-lang/mdBook/pull/1559)
### Changed
- If running a Rust example does not have any output, it now displays the text
"No output" instead of not showing anything.
[#1599](https://github.com/rust-lang/mdBook/pull/1599)
- Code block language tags can now be separated by space or tab (along with
commas) to match the behavior of other sites like GitHub and rustdoc.
[#1469](https://github.com/rust-lang/mdBook/pull/1469)
- Updated `warp` (the web server) to the latest version.
This also updates the minimum supported Rust version to 1.46.
[#1612](https://github.com/rust-lang/mdBook/pull/1612)
- Updated to highlight.js 11. This has various highlighting improvements.
[#1597](https://github.com/rust-lang/mdBook/pull/1597)
### Fixed
- Inline code blocks inside a header are no longer highlighted when
`output.html.playground.editable` is `true`.
[#1613](https://github.com/rust-lang/mdBook/pull/1613)
## mdBook 0.4.10
[2f7293a...dc2062a](https://github.com/rust-lang/mdBook/compare/2f7293a...dc2062a)
### Changed
- Reverted breaking change in 0.4.9 that removed the `__non_exhaustive` marker
on the `Book` struct.
[#1572](https://github.com/rust-lang/mdBook/pull/1572)
- Updated handlebars to 4.0.
[#1550](https://github.com/rust-lang/mdBook/pull/1550)
- Removed the `chapter_begin` id on the print page's chapter separators.
[#1541](https://github.com/rust-lang/mdBook/pull/1541)
## mdBook 0.4.9 ## mdBook 0.4.9
[7e01cf9...d325c60](https://github.com/rust-lang/mdBook/compare/7e01cf9...d325c60) [7e01cf9...d325c60](https://github.com/rust-lang/mdBook/compare/7e01cf9...d325c60)

3
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,3 @@
# The Rust Code of Conduct
The Code of Conduct for this repository [can be found online](https://www.rust-lang.org/conduct.html).

View File

@ -6,7 +6,6 @@ If you have come here to learn how to contribute to mdBook, we have some tips fo
First of all, don't hesitate to ask questions! First of all, don't hesitate to ask questions!
Use the [issue tracker](https://github.com/rust-lang/mdBook/issues), no question is too simple. Use the [issue tracker](https://github.com/rust-lang/mdBook/issues), no question is too simple.
If we don't respond in a couple of days, ping us @Michael-F-Bryan, @budziq, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
### Issues to work on ### Issues to work on
@ -46,7 +45,7 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
0. Navigate into the newly created `mdBook` directory 0. Navigate into the newly created `mdBook` directory
0. Run `cargo build` 0. Run `cargo build`
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`. The resulting binary can be found in `mdBook/target/debug/` under the name `mdbook` or `mdbook.exe`.
### Code Quality ### Code Quality
@ -106,3 +105,26 @@ and [rustfmt](https://github.com/rust-lang/rustfmt) on the code first.
This is not a requirement though and will never block a pull-request from being merged. This is not a requirement though and will never block a pull-request from being merged.
That's it, happy contributions! :tada: :tada: :tada: That's it, happy contributions! :tada: :tada: :tada:
## Browser compatibility and testing
Currently we don't have a strict browser compatibility matrix due to our limited resources.
We generally strive to keep mdBook compatible with a relatively recent browser on all of the most major platforms.
That is, supporting Chrome, Safari, Firefox, Edge on Windows, macOS, Linux, iOS, and Android.
If possible, do your best to avoid breaking older browser releases.
Any change to the HTML or styling is encouraged to manually check on as many browsers and platforms that you can.
Unfortunately at this time we don't have any automated UI or browser testing, so your assistance in testing is appreciated.
## Updating higlight.js
The following are instructions for updating [highlight.js](https://highlightjs.org/).
1. Clone the repository at <https://github.com/highlightjs/highlight.js>
1. Check out a tagged release (like `10.1.1`).
1. Run `npm install`
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx properties r scala x86asm yaml`
1. Compare the language list that it spits out to the one in [`syntax-highlighting.md`](https://github.com/camelid/mdBook/blob/master/guide/src/format/theme/syntax-highlighting.md). If any are missing, add them to the list and rebuild (and update these docs). If any are added to the common set, add them to `syntax-highlighting.md`.
1. Copy `build/highlight.min.js` to mdbook's directory [`highlight.js`](https://github.com/rust-lang/mdBook/blob/master/src/theme/highlight.js).
1. Be sure to check the highlight.js [CHANGES](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md) for any breaking changes. Breaking changes that would affect users will need to wait until the next major release.
1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. (TODO: It would be nice to have a demo file in the repo to help with this.)

591
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "mdbook" name = "mdbook"
version = "0.4.9" version = "0.4.13"
authors = [ authors = [
"Mathieu David <mathieudavid@mathieudavid.org>", "Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>", "Michael-F-Bryan <michaelfbryan@gmail.com>",
@ -24,7 +24,7 @@ handlebars = "4.0"
lazy_static = "1.0" lazy_static = "1.0"
log = "0.4" log = "0.4"
memchr = "2.0" memchr = "2.0"
open = "1.1" opener = "0.5"
pulldown-cmark = "0.7.0" pulldown-cmark = "0.7.0"
regex = "1.0.0" regex = "1.0.0"
serde = "1.0" serde = "1.0"
@ -33,6 +33,7 @@ serde_json = "1.0"
shlex = "1" shlex = "1"
tempfile = "3.0" tempfile = "3.0"
toml = "0.5.1" toml = "0.5.1"
topological-sort = "0.1.0"
# Watch feature # Watch feature
notify = { version = "4.0", optional = true } notify = { version = "4.0", optional = true }
@ -40,14 +41,16 @@ gitignore = { version = "1.0", optional = true }
# Serve feature # Serve feature
futures-util = { version = "0.3.4", optional = true } futures-util = { version = "0.3.4", optional = true }
tokio = { version = "0.2.18", features = ["macros"], optional = true } tokio = { version = "1", features = ["macros", "rt-multi-thread"], optional = true }
warp = { version = "0.2.2", default-features = false, features = ["websocket"], optional = true } warp = { version = "0.3.1", default-features = false, features = ["websocket"], optional = true }
# Search feature # Search feature
elasticlunr-rs = { version = "2.3", optional = true, default-features = false } elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
ammonia = { version = "3", optional = true } ammonia = { version = "3", optional = true }
[dev-dependencies] [dev-dependencies]
assert_cmd = "1"
predicates = "2"
select = "0.5" select = "0.5"
semver = "0.11.0" semver = "0.11.0"
pretty_assertions = "0.6" pretty_assertions = "0.6"

View File

@ -163,6 +163,7 @@ of a book in order to validate links or run tests. Some existing renderers are:
preprocessors. preprocessors.
- [`linkcheck`] - a backend which will check that all links are valid - [`linkcheck`] - a backend which will check that all links are valid
- [`epub`] - an experimental EPUB generator - [`epub`] - an experimental EPUB generator
- [`man`] - a backend that generates manual pages from the book
> **Note for Developers:** Feel free to send us a PR if you've developed your > **Note for Developers:** Feel free to send us a PR if you've developed your
> own plugin and want it mentioned here. > own plugin and want it mentioned here.
@ -234,3 +235,4 @@ All the code in this repository is released under the ***Mozilla Public License
[master-docs]: http://rust-lang.github.io/mdBook/ [master-docs]: http://rust-lang.github.io/mdBook/
[`linkcheck`]: https://crates.io/crates/mdbook-linkcheck [`linkcheck`]: https://crates.io/crates/mdbook-linkcheck
[`epub`]: https://crates.io/crates/mdbook-epub [`epub`]: https://crates.io/crates/mdbook-epub
[`man`]: https://crates.io/crates/mdbook-man

View File

@ -37,7 +37,7 @@ fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
let book_version = Version::parse(&ctx.mdbook_version)?; let book_version = Version::parse(&ctx.mdbook_version)?;
let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?; let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
if version_req.matches(&book_version) != true { if !version_req.matches(&book_version) {
eprintln!( eprintln!(
"Warning: The {} plugin was built against version {} of mdbook, \ "Warning: The {} plugin was built against version {} of mdbook, \
but we're being called from version {}", but we're being called from version {}",
@ -55,7 +55,7 @@ fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! { fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
let renderer = sub_args.value_of("renderer").expect("Required argument"); let renderer = sub_args.value_of("renderer").expect("Required argument");
let supported = pre.supports_renderer(&renderer); let supported = pre.supports_renderer(renderer);
// Signal whether the renderer is supported by exiting with 1 or 0. // Signal whether the renderer is supported by exiting with 1 or 0.
if supported { if supported {

View File

@ -1,25 +1,40 @@
# mdBook # Introduction
**mdBook** is a command line tool and Rust crate to create books using Markdown **mdBook** is a command line tool and Rust crate to create books with Markdown. The output resembles tools like Gitbook,
(as by the [CommonMark](https://commonmark.org/) specification) files. It's very and is ideal for creating product or API documentation, tutorials, course materials or anything that requires a clean,
similar to Gitbook but written in [Rust](http://www.rust-lang.org). easily navigable and customizable presentation. mdBook is written in [Rust](https://www.rust-lang.org); its performance
and simplicity made it ideal for use as a tool to publish directly to hosted websites such
as [GitHub Pages](https://pages.github.com) via automation. This guide, in fact, serves as both the mdBook documentation
and a fine example of what mdBook produces.
What you are reading serves as an example of the output of mdBook and at the mdBook includes built in support for both preprocessing your Markdown and alternative renderers for producing formats
same time as a high-level documentation. other than HTML. These facilities also enable other functionality such as
validation. [Searching](https://crates.io/search?q=mdbook&sort=relevance) Rust's [crates.io](https://crates.io) is a
great way to discover more extensions.
mdBook is free and open source, you can find the source code on ## API Documentation
[GitHub](https://github.com/rust-lang/mdBook). Issues and feature
requests can be posted on the [GitHub issue
tracker](https://github.com/rust-lang/mdBook/issues).
## API docs In addition to the above features, mdBook also has a Rust [API](https://docs.rs/mdbook/*/mdbook/). This allows you to
write your own preprocessor or renderer, as well as incorporate mdBook features into other applications.
The [For Developers](for_developers) section of this guide contains more information and some examples.
Alongside this book you can also read the [API ## Markdown
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 mdBook's [parser](https://github.com/raphlinus/pulldown-cmark) adheres to the [CommonMark](https://commonmark.org/)
overview. specification. You can take a quick [tutorial](https://commonmark.org/help/tutorial/),
or [try out](https://spec.commonmark.org/dingus/) CommonMark in real time. For a more in-depth experience, check out the
[Markdown Guide](https://www.markdownguide.org).
## Contributing
mdBook is free and open source. You can find the source code on
[GitHub](https://github.com/rust-lang/mdBook) and issues and feature requests can be posted on
the [GitHub issue tracker](https://github.com/rust-lang/mdBook/issues). mdBook relies on the community to fix bugs and
add features: if you'd like to contribute, please read
the [CONTRIBUTING](https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md) guide and consider opening
a [pull request](https://github.com/rust-lang/mdBook/pulls).
## License ## License
mdBook, all the source code, is released under the [Mozilla Public License The mdBook source and documentation are released under
v2.0](https://www.mozilla.org/MPL/2.0/). the [Mozilla Public License v2.0](https://www.mozilla.org/MPL/2.0/).

View File

@ -1,6 +1,6 @@
# Summary # Summary
- [mdBook](README.md) - [Introduction](README.md)
- [Command Line Tool](cli/README.md) - [Command Line Tool](cli/README.md)
- [init](cli/init.md) - [init](cli/init.md)
- [build](cli/build.md) - [build](cli/build.md)

View File

@ -52,3 +52,19 @@ 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 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. overwrite a specific file, just delete it and the default file will be used.
#### --title
Specify a title for the book. If not supplied, an interactive prompt will ask for
a title.
```bash
mdbook init --title="my amazing book"
```
#### --ignore
Create a `.gitignore` file configured to ignore the `book` directory created when [building] a book.
If not supplied, an interactive prompt will ask whether it should be created.
[building]: build.md

View File

@ -43,7 +43,17 @@ mdbook test path/to/book
The `--library-path` (`-L`) option allows you to add directories to the library The `--library-path` (`-L`) option allows you to add directories to the library
search path used by `rustdoc` when it builds and tests the examples. Multiple search path used by `rustdoc` when it builds and tests the examples. Multiple
directories can be specified with multiple options (`-L foo -L bar`) or with a directories can be specified with multiple options (`-L foo -L bar`) or with a
comma-delimited list (`-L foo,bar`). comma-delimited list (`-L foo,bar`). The path should point to the Cargo
[build cache](https://doc.rust-lang.org/cargo/guide/build-cache.html) `deps` directory that
contains the build output of your project. For example, if your Rust project's book is in a directory
named `my-book`, the following command would include the crate's dependencies when running `test`:
```shell
mdbook test my-book -L target/debug/deps/
```
See the `rustdoc` command-line [documentation](https://doc.rust-lang.org/rustdoc/command-line-arguments.html#-l--library-path-where-to-look-for-dependencies)
for more information.
#### --dest-dir #### --dest-dir

View File

@ -26,7 +26,7 @@ before_script:
- cargo install-update -a - cargo install-update -a
script: script:
- mdbook build path/to/mybook && mdbook test path/to/mybook - mdbook build && mdbook test # In case of custom book path: mdbook build path/to/mybook && mdbook test path/to/mybook
``` ```
## Deploying Your Book to GitHub Pages ## Deploying Your Book to GitHub Pages
@ -50,10 +50,10 @@ deploy:
provider: pages provider: pages
skip-cleanup: true skip-cleanup: true
github-token: $GITHUB_TOKEN github-token: $GITHUB_TOKEN
local-dir: path/to/mybook/book local-dir: book # In case of custom book path: path/to/mybook/book
keep-history: false keep-history: false
on: on:
branch: master branch: main
``` ```
That's it! That's it!
@ -77,7 +77,7 @@ before_script:
- cargo install-update -a - cargo install-update -a
script: script:
- mdbook build path/to/mybook && mdbook test path/to/mybook - mdbook build && mdbook test # In case of custom book path: mdbook build path/to/mybook && mdbook test path/to/mybook
deploy: deploy:
provider: pages provider: pages
@ -85,10 +85,10 @@ deploy:
edge: true edge: true
cleanup: false cleanup: false
github-token: $GITHUB_TOKEN github-token: $GITHUB_TOKEN
local-dir: path/to/mybook/book local-dir: book # In case of custom book path: path/to/mybook/book
keep-history: false keep-history: false
on: on:
branch: master branch: main
target_branch: gh-pages target_branch: gh-pages
``` ```
@ -140,15 +140,15 @@ pages:
- export PATH="$PATH:$CARGO_HOME/bin" - export PATH="$PATH:$CARGO_HOME/bin"
- mdbook --version || cargo install mdbook - mdbook --version || cargo install mdbook
script: script:
- mdbook build -d public - mdbook build -d public
only: rules:
- master - if: '$CI_COMMIT_REF_NAME == "master"'
artifacts: artifacts:
paths: paths:
- public - public
cache: cache:
paths: paths:
- $CARGO_HOME/bin - $CARGO_HOME/bin
``` ```
After you commit and push this new file, GitLab CI will run and your book will be available! After you commit and push this new file, GitLab CI will run and your book will be available!

View File

@ -13,6 +13,7 @@ rough example of how this is accomplished in practice.
- [mdbook-epub] - an EPUB renderer - [mdbook-epub] - an EPUB renderer
- [mdbook-test] - a program to run the book's contents through [rust-skeptic] to - [mdbook-test] - a program to run the book's contents through [rust-skeptic] to
verify everything compiles and runs correctly (similar to `rustdoc --test`) verify everything compiles and runs correctly (similar to `rustdoc --test`)
- [mdbook-man] - generate manual pages from the book
This page will step you through creating your own alternative backend in the form This page will step you through creating your own alternative backend in the form
of a simple word counting program. Although it will be written in Rust, there's of a simple word counting program. Although it will be written in Rust, there's
@ -377,6 +378,7 @@ the source code or ask questions.
[mdbook-linkcheck]: https://github.com/Michael-F-Bryan/mdbook-linkcheck [mdbook-linkcheck]: https://github.com/Michael-F-Bryan/mdbook-linkcheck
[mdbook-epub]: https://github.com/Michael-F-Bryan/mdbook-epub [mdbook-epub]: https://github.com/Michael-F-Bryan/mdbook-epub
[mdbook-test]: https://github.com/Michael-F-Bryan/mdbook-test [mdbook-test]: https://github.com/Michael-F-Bryan/mdbook-test
[mdbook-man]: https://github.com/vv9k/mdbook-man
[rust-skeptic]: https://github.com/budziq/rust-skeptic [rust-skeptic]: https://github.com/budziq/rust-skeptic
[`RenderContext`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html [`RenderContext`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html
[`RenderContext::from_json()`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html#method.from_json [`RenderContext::from_json()`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html#method.from_json

View File

@ -34,8 +34,15 @@ command = "python3 /path/to/foo.py"
renderer = ["html", "epub"] renderer = ["html", "epub"]
``` ```
In typical unix style, all inputs to the plugin will be written to `stdin` as Once the preprocessor has been defined and the build process starts, mdBook executes the command defined in the `preprocessor.foo.command` key twice.
JSON and `mdbook` will read from `stdout` if it is expecting output. The first time it runs the preprocessor to determine if it supports the given renderer.
mdBook passes two arguments to the process: the first argument is the string `supports` and the second argument is the renderer name.
The preprocessor should exit with a status code 0 if it supports the given renderer, or return a non-zero exit code if it does not.
If the preprocessor supports the renderer, then mdbook runs it a second time, passing JSON data into stdin.
The JSON consists of an array of `[context, book]` where `context` is the serialized object [`PreprocessorContext`] and `book` is a [`Book`] object containing the content of the book.
The preprocessor should return the JSON format of the [`Book`] object to stdout, with any modifications it wishes to perform.
The easiest way to get started is by creating your own implementation of the 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 `Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
@ -106,6 +113,33 @@ fn remove_emphasis(
For everything else, have a look [at the complete example][example]. For everything else, have a look [at the complete example][example].
## Implementing a preprocessor with a different language
The fact that mdBook utilizes stdin and stdout to communicate with the preprocessors makes it easy to implement them in a language other than Rust.
The following code shows how to implement a simple preprocessor in Python, which will modify the content of the first chapter.
The example below follows the configuration shown above with `preprocessor.foo.command` actually pointing to a Python script.
```python
import json
import sys
if __name__ == '__main__':
if len(sys.argv) > 1: # we check if we received any argument
if sys.argv[1] == "supports":
# then we are good to return an exit status code of 0, since the other argument will just be the renderer's name
sys.exit(0)
# load both the context and the book representations from stdin
context, book = json.load(sys.stdin)
# and now, we can just modify the content of the first chapter
book['sections'][0]['Chapter']['content'] = '# Hello'
# we are done with the book's modification, we can just print it to stdout,
print(json.dumps(book))
```
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html [preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
[pc]: https://crates.io/crates/pulldown-cmark [pc]: https://crates.io/crates/pulldown-cmark
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark [pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
@ -113,3 +147,5 @@ For everything else, have a look [at the complete example][example].
[an example no-op preprocessor]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs [an example no-op preprocessor]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input [`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 [`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut
[`PreprocessorContext`]: https://docs.rs/mdbook/latest/mdbook/preprocess/struct.PreprocessorContext.html
[`Book`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html

View File

@ -63,8 +63,8 @@ Options for the Rust language, relevant to running tests and playground
integration. integration.
- **edition**: Rust edition to use by default for the code snippets. Default - **edition**: Rust edition to use by default for the code snippets. Default
is "2015". Individual code blocks can be controlled with the `edition2015` is "2015". Individual code blocks can be controlled with the `edition2015`,
or `edition2018` annotations, such as: `edition2018` or `edition2021` annotations, such as:
~~~text ~~~text
```rust,edition2015 ```rust,edition2015

View File

@ -56,3 +56,25 @@ be overridden by adding a `command` field.
[preprocessor.random] [preprocessor.random]
command = "python random.py" command = "python random.py"
``` ```
### Require A Certain Order
The order in which preprocessors are run can be controlled with the `before` and `after` fields.
For example, suppose you want your `linenos` preprocessor to process lines that may have been `{{#include}}`d; then you want it to run after the built-in `links` preprocessor, which you can require using either the `before` or `after` field:
```toml
[preprocessor.linenos]
after = [ "links" ]
```
or
```toml
[preprocessor.links]
before = [ "linenos" ]
```
It would also be possible, though redundant, to specify both of the above in the same config file.
Preprocessors having the same priority specified through `before` and `after` are sorted by name.
Any infinite loops will be detected and produce an error.

View File

@ -188,13 +188,13 @@ Enable it by adding an empty table to your `book.toml` as follows:
There are no configuration options for the Markdown renderer at this time; There are no configuration options for the Markdown renderer at this time;
only whether it is enabled or disabled. only whether it is enabled or disabled.
See [the preprocessors documentation](#configuring-preprocessors) for how to See [the preprocessors documentation](preprocessors.md) for how to
specify which preprocessors should run before the Markdown renderer. specify which preprocessors should run before the Markdown renderer.
### Custom Renderers ### Custom Renderers
A custom renderer can be enabled by adding a `[output.foo]` table to your A custom renderer can be enabled by adding a `[output.foo]` table to your
`book.toml`. Similar to [preprocessors](#configuring-preprocessors) this will `book.toml`. Similar to [preprocessors](preprocessors.md) this will
instruct `mdbook` to pass a representation of the book to `mdbook-foo` for instruct `mdbook` to pass a representation of the book to `mdbook-foo` for
rendering. See the [alternative backends] chapter for more detail. rendering. See the [alternative backends] chapter for more detail.

View File

@ -21,7 +21,7 @@ Will render as
```rust ```rust
# fn main() { # fn main() {
let x = 5; let x = 5;
let y = 7; let y = 6;
println!("{}", x + y); println!("{}", x + y);
# } # }

View File

@ -15,8 +15,10 @@ shout-out to them!
- [projektir](https://github.com/projektir) - [projektir](https://github.com/projektir)
- [Phaiax](https://github.com/Phaiax) - [Phaiax](https://github.com/Phaiax)
- Matt Ickstadt ([mattico](https://github.com/mattico)) - Matt Ickstadt ([mattico](https://github.com/mattico))
- Weihang Lo ([@weihanglo](https://github.com/weihanglo)) - Weihang Lo ([weihanglo](https://github.com/weihanglo))
- Avision Ho ([@avisionh](https://github.com/avisionh)) - Avision Ho ([avisionh](https://github.com/avisionh))
- Vivek Akupatni ([@apatniv](https://github.com/apatniv)) - Vivek Akupatni ([apatniv](https://github.com/apatniv))
- Eric Huss ([ehuss](https://github.com/ehuss))
- Josh Rotenberg ([joshrotenberg](https://github.com/joshrotenberg))
If you feel you're missing from this list, feel free to add yourself in a PR. If you feel you're missing from this list, feel free to add yourself in a PR.

View File

@ -22,7 +22,7 @@ pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book>
.with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?; .with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
if cfg.create_missing { if cfg.create_missing {
create_missing(&src_dir, &summary).with_context(|| "Unable to create missing chapters")?; create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
} }
load_book_from_disk(&summary, src_dir) load_book_from_disk(&summary, src_dir)
@ -74,10 +74,10 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
/// [`iter()`]: #method.iter /// [`iter()`]: #method.iter
/// [`for_each_mut()`]: #method.for_each_mut /// [`for_each_mut()`]: #method.for_each_mut
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Book { pub struct Book {
/// The sections in this book. /// The sections in this book.
pub sections: Vec<BookItem>, pub sections: Vec<BookItem>,
__non_exhaustive: (),
} }
impl Book { impl Book {
@ -225,7 +225,10 @@ pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P)
chapters.push(chapter); chapters.push(chapter);
} }
Ok(Book { sections: chapters }) Ok(Book {
sections: chapters,
__non_exhaustive: (),
})
} }
fn load_summary_item<P: AsRef<Path> + Clone>( fn load_summary_item<P: AsRef<Path> + Clone>(
@ -378,7 +381,7 @@ And here is some \
root.nested_items.push(second.clone().into()); root.nested_items.push(second.clone().into());
root.nested_items.push(SummaryItem::Separator); root.nested_items.push(SummaryItem::Separator);
root.nested_items.push(second.clone().into()); root.nested_items.push(second.into());
(root, temp_dir) (root, temp_dir)
} }
@ -451,7 +454,7 @@ And here is some \
sub_items: vec![ sub_items: vec![
BookItem::Chapter(nested.clone()), BookItem::Chapter(nested.clone()),
BookItem::Separator, BookItem::Separator,
BookItem::Chapter(nested.clone()), BookItem::Chapter(nested),
], ],
}); });

View File

@ -20,6 +20,7 @@ use std::process::Command;
use std::string::ToString; use std::string::ToString;
use tempfile::Builder as TempFileBuilder; use tempfile::Builder as TempFileBuilder;
use toml::Value; use toml::Value;
use topological_sort::TopologicalSort;
use crate::errors::*; use crate::errors::*;
use crate::preprocess::{ use crate::preprocess::{
@ -274,6 +275,10 @@ impl MDBook {
RustEdition::E2018 => { RustEdition::E2018 => {
cmd.args(&["--edition", "2018"]); cmd.args(&["--edition", "2018"]);
} }
RustEdition::E2021 => {
cmd.args(&["--edition", "2021"])
.args(&["-Z", "unstable-options"]);
}
} }
} }
@ -368,12 +373,7 @@ fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
renderers renderers
} }
fn default_preprocessors() -> Vec<Box<dyn Preprocessor>> { const DEFAULT_PREPROCESSORS: &[&'static str] = &["links", "index"];
vec![
Box::new(LinkPreprocessor::new()),
Box::new(IndexPreprocessor::new()),
]
}
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool { fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
let name = pre.name(); let name = pre.name();
@ -382,36 +382,127 @@ fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
/// Look at the `MDBook` and try to figure out what preprocessors to run. /// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> { fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
let mut preprocessors = Vec::new(); // Collect the names of all preprocessors intended to be run, and the order
// in which they should be run.
let mut preprocessor_names = TopologicalSort::<String>::new();
if config.build.use_default_preprocessors { if config.build.use_default_preprocessors {
preprocessors.extend(default_preprocessors()); for name in DEFAULT_PREPROCESSORS {
preprocessor_names.insert(name.to_string());
}
} }
if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) { if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
for key in preprocessor_table.keys() { for (name, table) in preprocessor_table.iter() {
match key.as_ref() { preprocessor_names.insert(name.to_string());
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())), let exists = |name| {
name => preprocessors.push(interpret_custom_preprocessor( (config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
name, || preprocessor_table.contains_key(name)
&preprocessor_table[name], };
)),
if let Some(before) = table.get("before") {
let before = before.as_array().ok_or_else(|| {
Error::msg(format!(
"Expected preprocessor.{}.before to be an array",
name
))
})?;
for after in before {
let after = after.as_str().ok_or_else(|| {
Error::msg(format!(
"Expected preprocessor.{}.before to contain strings",
name
))
})?;
if !exists(after) {
// Only warn so that preprocessors can be toggled on and off (e.g. for
// troubleshooting) without having to worry about order too much.
warn!(
"preprocessor.{}.after contains \"{}\", which was not found",
name, after
);
} else {
preprocessor_names.add_dependency(name, after);
}
}
}
if let Some(after) = table.get("after") {
let after = after.as_array().ok_or_else(|| {
Error::msg(format!(
"Expected preprocessor.{}.after to be an array",
name
))
})?;
for before in after {
let before = before.as_str().ok_or_else(|| {
Error::msg(format!(
"Expected preprocessor.{}.after to contain strings",
name
))
})?;
if !exists(before) {
// See equivalent warning above for rationale
warn!(
"preprocessor.{}.before contains \"{}\", which was not found",
name, before
);
} else {
preprocessor_names.add_dependency(before, name);
}
}
} }
} }
} }
Ok(preprocessors) // Now that all links have been established, queue preprocessors in a suitable order
let mut preprocessors = Vec::with_capacity(preprocessor_names.len());
// `pop_all()` returns an empty vector when no more items are not being depended upon
for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
.take_while(|names| !names.is_empty())
{
// The `topological_sort` crate does not guarantee a stable order for ties, even across
// runs of the same program. Thus, we break ties manually by sorting.
// Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
// values ([1]), which may not be an alphabetical sort.
// As mentioned in [1], doing so depends on locale, which is not desirable for deciding
// preprocessor execution order.
// [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
names.sort();
for name in names {
let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
"links" => Box::new(LinkPreprocessor::new()),
"index" => Box::new(IndexPreprocessor::new()),
_ => {
// The only way to request a custom preprocessor is through the `preprocessor`
// table, so it must exist, be a table, and contain the key.
let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name];
let command = get_custom_preprocessor_cmd(&name, table);
Box::new(CmdPreprocessor::new(name, command))
}
};
preprocessors.push(preprocessor);
}
}
// "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
// Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
if preprocessor_names.is_empty() {
Ok(preprocessors)
} else {
Err(Error::msg("Cyclic dependency detected in preprocessors"))
}
} }
fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocessor> { fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
let command = table table
.get("command") .get("command")
.and_then(Value::as_str) .and_then(Value::as_str)
.map(ToString::to_string) .map(ToString::to_string)
.unwrap_or_else(|| format!("mdbook-{}", key)); .unwrap_or_else(|| format!("mdbook-{}", key))
Box::new(CmdPreprocessor::new(key.to_string(), command))
} }
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> { fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
@ -511,8 +602,8 @@ mod tests {
assert!(got.is_ok()); assert!(got.is_ok());
assert_eq!(got.as_ref().unwrap().len(), 2); assert_eq!(got.as_ref().unwrap().len(), 2);
assert_eq!(got.as_ref().unwrap()[0].name(), "links"); assert_eq!(got.as_ref().unwrap()[0].name(), "index");
assert_eq!(got.as_ref().unwrap()[1].name(), "index"); assert_eq!(got.as_ref().unwrap()[1].name(), "links");
} }
#[test] #[test]
@ -559,9 +650,123 @@ mod tests {
// make sure the `preprocessor.random` table exists // make sure the `preprocessor.random` table exists
let random = cfg.get_preprocessor("random").unwrap(); let random = cfg.get_preprocessor("random").unwrap();
let random = interpret_custom_preprocessor("random", &Value::Table(random.clone())); let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
assert_eq!(random.cmd(), "python random.py"); assert_eq!(random, "python random.py");
}
#[test]
fn preprocessor_before_must_be_array() {
let cfg_str = r#"
[preprocessor.random]
before = 0
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg).is_err());
}
#[test]
fn preprocessor_after_must_be_array() {
let cfg_str = r#"
[preprocessor.random]
after = 0
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg).is_err());
}
#[test]
fn preprocessor_order_is_honored() {
let cfg_str = r#"
[preprocessor.random]
before = [ "last" ]
after = [ "index" ]
[preprocessor.last]
after = [ "links", "index" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg).unwrap();
let index = |name| {
preprocessors
.iter()
.enumerate()
.find(|(_, preprocessor)| preprocessor.name() == name)
.unwrap()
.0
};
let assert_before = |before, after| {
if index(before) >= index(after) {
eprintln!("Preprocessor order:");
for preprocessor in &preprocessors {
eprintln!(" {}", preprocessor.name());
}
panic!("{} should come before {}", before, after);
}
};
assert_before("index", "random");
assert_before("index", "last");
assert_before("random", "last");
assert_before("links", "last");
}
#[test]
fn cyclic_dependencies_are_detected() {
let cfg_str = r#"
[preprocessor.links]
before = [ "index" ]
[preprocessor.index]
before = [ "links" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg).is_err());
}
#[test]
fn dependencies_dont_register_undefined_preprocessors() {
let cfg_str = r#"
[preprocessor.links]
before = [ "random" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg).unwrap();
assert!(preprocessors
.iter()
.find(|preprocessor| preprocessor.name() == "random")
.is_none());
}
#[test]
fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
let cfg_str = r#"
[preprocessor.random]
before = [ "links" ]
[build]
use-default-preprocessors = false
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg).unwrap();
assert!(preprocessors
.iter()
.find(|preprocessor| preprocessor.name() == "links")
.is_none());
} }
#[test] #[test]

View File

@ -382,7 +382,7 @@ impl<'a> SummaryParser<'a> {
} }
Some(ev @ Event::Start(Tag::List(..))) => { Some(ev @ Event::Start(Tag::List(..))) => {
self.back(ev); self.back(ev);
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?; let mut bunch_of_items = self.parse_nested_numbered(root_number)?;
// if we've resumed after something like a rule the root sections // 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 // will be numbered from 1. We need to manually go back and update

View File

@ -1,5 +1,5 @@
use crate::get_book_dir; use crate::get_book_dir;
use clap::{App, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use mdbook::config; use mdbook::config;
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::MDBook; use mdbook::MDBook;
@ -18,6 +18,21 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
) )
.arg_from_usage("--theme 'Copies the default theme into your source folder'") .arg_from_usage("--theme 'Copies the default theme into your source folder'")
.arg_from_usage("--force 'Skips confirmation prompts'") .arg_from_usage("--force 'Skips confirmation prompts'")
.arg(
Arg::with_name("title")
.long("title")
.takes_value(true)
.help("Sets the book title")
.required(false),
)
.arg(
Arg::with_name("ignore")
.long("ignore")
.takes_value(true)
.possible_values(&["none", "git"])
.help("Creates a VCS ignore file (i.e. .gitignore)")
.required(false),
)
} }
// Init command implementation // Init command implementation
@ -25,7 +40,6 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut builder = MDBook::init(&book_dir); let mut builder = MDBook::init(&book_dir);
let mut config = config::Config::default(); let mut config = config::Config::default();
// If flag `--theme` is present, copy theme to src // If flag `--theme` is present, copy theme to src
if args.is_present("theme") { if args.is_present("theme") {
let theme_dir = book_dir.join("theme"); let theme_dir = book_dir.join("theme");
@ -45,13 +59,23 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
} }
} }
println!("\nDo you want a .gitignore to be created? (y/n)"); if let Some(ignore) = args.value_of("ignore") {
match ignore {
if confirm() { "git" => builder.create_gitignore(true),
builder.create_gitignore(true); _ => builder.create_gitignore(false),
};
} else {
println!("\nDo you want a .gitignore to be created? (y/n)");
if confirm() {
builder.create_gitignore(true);
}
} }
config.book.title = request_book_title(); config.book.title = if args.is_present("title") {
args.value_of("title").map(String::from)
} else {
request_book_title()
};
if let Some(author) = get_author_name() { if let Some(author) = get_author_name() {
debug!("Obtained user name from gitconfig: {:?}", author); debug!("Obtained user name from gitconfig: {:?}", author);

View File

@ -467,6 +467,9 @@ pub struct RustConfig {
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
/// Rust edition to use for the code. /// Rust edition to use for the code.
pub enum RustEdition { pub enum RustEdition {
/// The 2021 edition of Rust
#[serde(rename = "2021")]
E2021,
/// The 2018 edition of Rust /// The 2018 edition of Rust
#[serde(rename = "2018")] #[serde(rename = "2018")]
E2018, E2018,
@ -855,6 +858,26 @@ mod tests {
assert_eq!(got.rust, rust_should_be); assert_eq!(got.rust, rust_should_be);
} }
#[test]
fn edition_2021() {
let src = r#"
[book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David"]
src = "./source"
[rust]
edition = "2021"
"#;
let rust_should_be = RustConfig {
edition: Some(RustEdition::E2021),
};
let got = Config::from_str(src).unwrap();
assert_eq!(got.rust, rust_should_be);
}
#[test] #[test]
fn load_arbitrary_output_type() { fn load_arbitrary_output_type() {
#[derive(Debug, Deserialize, PartialEq)] #[derive(Debug, Deserialize, PartialEq)]

View File

@ -3,8 +3,9 @@ extern crate clap;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
use anyhow::anyhow;
use chrono::Local; use chrono::Local;
use clap::{App, AppSettings, ArgMatches}; use clap::{App, AppSettings, Arg, ArgMatches, Shell, SubCommand};
use env_logger::Builder; use env_logger::Builder;
use log::LevelFilter; use log::LevelFilter;
use mdbook::utils; use mdbook::utils;
@ -20,7 +21,40 @@ const VERSION: &str = concat!("v", crate_version!());
fn main() { fn main() {
init_logger(); init_logger();
// Create a list of valid arguments and sub-commands let app = create_clap_app();
// Check which subcomamnd the user ran...
let res = match app.get_matches().subcommand() {
("init", Some(sub_matches)) => cmd::init::execute(sub_matches),
("build", Some(sub_matches)) => cmd::build::execute(sub_matches),
("clean", Some(sub_matches)) => cmd::clean::execute(sub_matches),
#[cfg(feature = "watch")]
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches),
#[cfg(feature = "serve")]
("serve", Some(sub_matches)) => cmd::serve::execute(sub_matches),
("test", Some(sub_matches)) => cmd::test::execute(sub_matches),
("completions", Some(sub_matches)) => (|| {
let shell: Shell = sub_matches
.value_of("shell")
.ok_or_else(|| anyhow!("Shell name missing."))?
.parse()
.map_err(|s| anyhow!("Invalid shell: {}", s))?;
create_clap_app().gen_completions_to("mdbook", shell, &mut std::io::stdout().lock());
Ok(())
})(),
(_, _) => unreachable!(),
};
if let Err(e) = res {
utils::log_backtrace(&e);
std::process::exit(101);
}
}
/// Create a list of valid arguments and sub-commands
fn create_clap_app<'a, 'b>() -> App<'a, 'b> {
let app = App::new(crate_name!()) let app = App::new(crate_name!())
.about(crate_description!()) .about(crate_description!())
.author("Mathieu David <mathieudavid@mathieudavid.org>") .author("Mathieu David <mathieudavid@mathieudavid.org>")
@ -35,31 +69,26 @@ fn main() {
.subcommand(cmd::init::make_subcommand()) .subcommand(cmd::init::make_subcommand())
.subcommand(cmd::build::make_subcommand()) .subcommand(cmd::build::make_subcommand())
.subcommand(cmd::test::make_subcommand()) .subcommand(cmd::test::make_subcommand())
.subcommand(cmd::clean::make_subcommand()); .subcommand(cmd::clean::make_subcommand())
.subcommand(
SubCommand::with_name("completions")
.about("Generate shell completions for your shell to stdout")
.arg(
Arg::with_name("shell")
.takes_value(true)
.possible_values(&Shell::variants())
.help("the shell to generate completions for")
.value_name("SHELL")
.required(true),
),
);
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
let app = app.subcommand(cmd::watch::make_subcommand()); let app = app.subcommand(cmd::watch::make_subcommand());
#[cfg(feature = "serve")] #[cfg(feature = "serve")]
let app = app.subcommand(cmd::serve::make_subcommand()); let app = app.subcommand(cmd::serve::make_subcommand());
// Check which subcomamnd the user ran... app
let res = match app.get_matches().subcommand() {
("init", Some(sub_matches)) => cmd::init::execute(sub_matches),
("build", Some(sub_matches)) => cmd::build::execute(sub_matches),
("clean", Some(sub_matches)) => cmd::clean::execute(sub_matches),
#[cfg(feature = "watch")]
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches),
#[cfg(feature = "serve")]
("serve", Some(sub_matches)) => cmd::serve::execute(sub_matches),
("test", Some(sub_matches)) => cmd::test::execute(sub_matches),
(_, _) => unreachable!(),
};
if let Err(e) = res {
utils::log_backtrace(&e);
std::process::exit(101);
}
} }
fn init_logger() { fn init_logger() {
@ -103,7 +132,8 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
} }
fn open<P: AsRef<OsStr>>(path: P) { fn open<P: AsRef<OsStr>>(path: P) {
if let Err(e) = open::that(path) { info!("Opening web browser");
if let Err(e) = opener::open(path) {
error!("Error opening web browser: {}", e); error!("Error opening web browser: {}", e);
} }
} }

View File

@ -49,7 +49,7 @@ impl CmdPreprocessor {
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) { fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
let stdin = child.stdin.take().expect("Child has stdin"); let stdin = child.stdin.take().expect("Child has stdin");
if let Err(e) = self.write_input(stdin, &book, &ctx) { if let Err(e) = self.write_input(stdin, book, ctx) {
// Looks like the backend hung up before we could finish // Looks like the backend hung up before we could finish
// sending it the render context. Log the error and keep going // sending it the render context. Log the error and keep going
warn!("Error writing the RenderContext to the backend, {}", e); warn!("Error writing the RenderContext to the backend, {}", e);

View File

@ -54,16 +54,14 @@ impl HtmlHandlebars {
let content = ch.content.clone(); let content = ch.content.clone();
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes); let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
let fixed_content = utils::render_markdown_with_path( let fixed_content =
&ch.content, utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path));
ctx.html_config.curly_quotes,
Some(&path),
);
if !ctx.is_index { if !ctx.is_index {
// Add page break between chapters // Add page break between chapters
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before // See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
// Add both two CSS properties because of the compatibility issue // Add both two CSS properties because of the compatibility issue
print_content.push_str(r#"<div id="chapter_begin" style="break-before: page; page-break-before: always;"></div>"#); print_content
.push_str(r#"<div style="break-before: page; page-break-before: always;"></div>"#);
} }
print_content.push_str(&fixed_content); print_content.push_str(&fixed_content);
@ -177,7 +175,7 @@ impl HtmlHandlebars {
let rendered = let rendered =
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition); self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
let output_file = get_404_output_file(&html_config.input_404); let output_file = get_404_output_file(&html_config.input_404);
utils::fs::write_file(&destination, output_file, rendered.as_bytes())?; utils::fs::write_file(destination, output_file, rendered.as_bytes())?;
debug!("Creating 404.html ✓"); debug!("Creating 404.html ✓");
Ok(()) Ok(())
} }
@ -222,10 +220,10 @@ impl HtmlHandlebars {
} }
write_file(destination, "css/variables.css", &theme.variables_css)?; write_file(destination, "css/variables.css", &theme.variables_css)?;
if let Some(contents) = &theme.favicon_png { if let Some(contents) = &theme.favicon_png {
write_file(destination, "favicon.png", &contents)?; write_file(destination, "favicon.png", contents)?;
} }
if let Some(contents) = &theme.favicon_svg { if let Some(contents) = &theme.favicon_svg {
write_file(destination, "favicon.svg", &contents)?; write_file(destination, "favicon.svg", contents)?;
} }
write_file(destination, "highlight.css", &theme.highlight_css)?; write_file(destination, "highlight.css", &theme.highlight_css)?;
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?; write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
@ -508,7 +506,7 @@ impl Renderer for HtmlHandlebars {
debug!("Register handlebars helpers"); debug!("Register handlebars helpers");
self.register_hbs_helpers(&mut handlebars, &html_config); self.register_hbs_helpers(&mut handlebars, &html_config);
let mut data = make_data(&ctx.root, &book, &ctx.config, &html_config, &theme)?; let mut data = make_data(&ctx.root, book, &ctx.config, &html_config, &theme)?;
// Print version // Print version
let mut print_content = String::new(); let mut print_content = String::new();
@ -551,14 +549,14 @@ impl Renderer for HtmlHandlebars {
let rendered = let rendered =
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition); self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
utils::fs::write_file(&destination, "print.html", rendered.as_bytes())?; utils::fs::write_file(destination, "print.html", rendered.as_bytes())?;
debug!("Creating print.html ✓"); debug!("Creating print.html ✓");
} }
debug!("Copy static files"); debug!("Copy static files");
self.copy_static_files(&destination, &theme, &html_config) self.copy_static_files(destination, &theme, &html_config)
.with_context(|| "Unable to copy across static files")?; .with_context(|| "Unable to copy across static files")?;
self.copy_additional_css_and_js(&html_config, &ctx.root, &destination) self.copy_additional_css_and_js(&html_config, &ctx.root, destination)
.with_context(|| "Unable to copy across additional CSS and JS")?; .with_context(|| "Unable to copy across additional CSS and JS")?;
// Render search index // Render search index
@ -566,7 +564,7 @@ impl Renderer for HtmlHandlebars {
{ {
let search = html_config.search.unwrap_or_default(); let search = html_config.search.unwrap_or_default();
if search.enable { if search.enable {
super::search::create_files(&search, &destination, &book)?; super::search::create_files(&search, destination, book)?;
} }
} }
@ -574,7 +572,7 @@ impl Renderer for HtmlHandlebars {
.context("Unable to emit redirects")?; .context("Unable to emit redirects")?;
// Copy all remaining files, avoid a recursive copy from/to the book build dir // Copy all remaining files, avoid a recursive copy from/to the book build dir
utils::fs::copy_files_except_ext(&src_dir, &destination, true, Some(&build_dir), &["md"])?; utils::fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?;
Ok(()) Ok(())
} }
@ -835,13 +833,15 @@ fn add_playground_pre(
{ {
let contains_e2015 = classes.contains("edition2015"); let contains_e2015 = classes.contains("edition2015");
let contains_e2018 = classes.contains("edition2018"); let contains_e2018 = classes.contains("edition2018");
let edition_class = if contains_e2015 || contains_e2018 { let contains_e2021 = classes.contains("edition2021");
let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 {
// the user forced edition, we should not overwrite it // the user forced edition, we should not overwrite it
"" ""
} else { } else {
match edition { match edition {
Some(RustEdition::E2015) => " edition2015", Some(RustEdition::E2015) => " edition2015",
Some(RustEdition::E2018) => " edition2018", Some(RustEdition::E2018) => " edition2018",
Some(RustEdition::E2021) => " edition2021",
None => "", None => "",
} }
}; };
@ -980,7 +980,7 @@ mod tests {
]; ];
for (src, should_be) in inputs { for (src, should_be) in inputs {
let got = build_header_links(&src); let got = build_header_links(src);
assert_eq!(got, should_be); assert_eq!(got, should_be);
} }
} }
@ -1063,4 +1063,28 @@ mod tests {
assert_eq!(&*got, *should_be); assert_eq!(&*got, *should_be);
} }
} }
#[test]
fn add_playground_edition2021() {
let inputs = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2021\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}\n</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
];
for (src, should_be) in &inputs {
let got = add_playground_pre(
src,
&Playground {
editable: true,
..Playground::default()
},
Some(RustEdition::E2021),
);
assert_eq!(&*got, *should_be);
}
}
} }

View File

@ -91,7 +91,7 @@ fn find_chapter(
match item.get("path") { match item.get("path") {
Some(path) if !path.is_empty() => { Some(path) if !path.is_empty() => {
if let Some(previous) = previous { if let Some(previous) = previous {
if let Some(item) = target.find(&base_path, &path, &item, &previous)? { if let Some(item) = target.find(&base_path, path, &item, &previous)? {
return Ok(Some(item)); return Ok(Some(item));
} }
} }

View File

@ -142,7 +142,7 @@ impl HelperDef for RenderToc {
// Section does not necessarily exist // Section does not necessarily exist
if let Some(section) = item.get("section") { if let Some(section) = item.get("section") {
out.write("<strong aria-hidden=\"true\">")?; out.write("<strong aria-hidden=\"true\">")?;
out.write(&section)?; out.write(section)?;
out.write("</strong> ")?; out.write("</strong> ")?;
} }
} }

View File

@ -17,10 +17,10 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
let mut doc_urls = Vec::with_capacity(book.sections.len()); let mut doc_urls = Vec::with_capacity(book.sections.len());
for item in book.iter() { for item in book.iter() {
render_item(&mut index, &search_config, &mut doc_urls, item)?; render_item(&mut index, search_config, &mut doc_urls, item)?;
} }
let index = write_to_json(index, &search_config, doc_urls)?; let index = write_to_json(index, search_config, doc_urls)?;
debug!("Writing search index ✓"); debug!("Writing search index ✓");
if index.len() > 10_000_000 { if index.len() > 10_000_000 {
warn!("searchindex.json is very large ({} bytes)", index.len()); warn!("searchindex.json is very large ({} bytes)", index.len());
@ -134,7 +134,7 @@ fn render_item(
// in an HtmlBlock tag. We must collect consecutive Html events // in an HtmlBlock tag. We must collect consecutive Html events
// into a block ourselves. // into a block ourselves.
while let Some(Event::Html(html)) = p.peek() { while let Some(Event::Html(html)) = p.peek() {
html_block.push_str(&html); html_block.push_str(html);
p.next(); p.next();
} }
@ -165,7 +165,12 @@ fn render_item(
} }
} }
if !heading.is_empty() { if !body.is_empty() || !heading.is_empty() {
if heading.is_empty() {
if let Some(chapter) = breadcrumbs.first() {
heading = chapter.clone();
}
}
// Make sure the last section is added to the index // Make sure the last section is added to the index
add_doc( add_doc(
index, index,

View File

@ -108,9 +108,12 @@ function playground_text(playground) {
let text = playground_text(code_block); let text = playground_text(code_block);
let classes = code_block.querySelector('code').classList; let classes = code_block.querySelector('code').classList;
let has_2018 = classes.contains("edition2018"); let edition = "2015";
let edition = has_2018 ? "2018" : "2015"; if(classes.contains("edition2018")) {
edition = "2018";
} else if(classes.contains("edition2021")) {
edition = "2021";
}
var params = { var params = {
version: "stable", version: "stable",
optimize: "0", optimize: "0",
@ -133,7 +136,15 @@ function playground_text(playground) {
body: JSON.stringify(params) body: JSON.stringify(params)
}) })
.then(response => response.json()) .then(response => response.json())
.then(response => result_block.innerText = response.result) .then(response => {
if (response.result.trim() === '') {
result_block.innerText = "No output";
result_block.classList.add("result-no-output");
} else {
result_block.innerText = response.result;
result_block.classList.remove("result-no-output");
}
})
.catch(error => result_block.innerText = "Playground Communication: " + error.message); .catch(error => result_block.innerText = "Playground Communication: " + error.message);
} }
@ -151,12 +162,13 @@ function playground_text(playground) {
if (window.ace) { if (window.ace) {
// language-rust class needs to be removed for editable // language-rust class needs to be removed for editable
// blocks or highlightjs will capture events // blocks or highlightjs will capture events
Array code_nodes
.from(document.querySelectorAll('code.editable')) .filter(function (node) {return node.classList.contains("editable"); })
.forEach(function (block) { block.classList.remove('language-rust'); }); .forEach(function (block) { block.classList.remove('language-rust'); });
Array Array
.from(document.querySelectorAll('code:not(.editable)')) code_nodes
.filter(function (node) {return !node.classList.contains("editable"); })
.forEach(function (block) { hljs.highlightBlock(block); }); .forEach(function (block) { hljs.highlightBlock(block); });
} else { } else {
code_nodes.forEach(function (block) { hljs.highlightBlock(block); }); code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
@ -359,7 +371,14 @@ function playground_text(playground) {
}); });
themePopup.addEventListener('click', function (e) { themePopup.addEventListener('click', function (e) {
var theme = e.target.id || e.target.parentElement.id; var theme;
if (e.target.className === "theme") {
theme = e.target.id;
} else if (e.target.parentElement.className === "theme") {
theme = e.target.parentElement.id;
} else {
return;
}
set_theme(theme); set_theme(theme);
}); });

View File

@ -175,3 +175,7 @@ blockquote {
margin: 5px 0px; margin: 5px 0px;
font-weight: bold; font-weight: bold;
} }
.result-no-output {
font-style: italic;
}

View File

@ -67,7 +67,7 @@
--links: #2b79a2; --links: #2b79a2;
--inline-code-color: #c5c8c6;; --inline-code-color: #c5c8c6;
--theme-popup-bg: #141617; --theme-popup-bg: #141617;
--theme-popup-border: #43484d; --theme-popup-border: #43484d;
@ -147,7 +147,7 @@
--links: #2b79a2; --links: #2b79a2;
--inline-code-color: #c5c8c6;; --inline-code-color: #c5c8c6;
--theme-popup-bg: #161923; --theme-popup-bg: #161923;
--theme-popup-border: #737480; --theme-popup-border: #737480;
@ -228,7 +228,7 @@
--links: #2b79a2; --links: #2b79a2;
--inline-code-color: #c5c8c6;; --inline-code-color: #c5c8c6;
--theme-popup-bg: #141617; --theme-popup-bg: #141617;
--theme-popup-border: #43484d; --theme-popup-border: #43484d;

View File

@ -247,7 +247,7 @@ mod tests {
} }
if let Err(e) = if let Err(e) =
copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, None, &["md"]) copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"])
{ {
panic!("Error while executing the function:\n{:?}", e); panic!("Error while executing the function:\n{:?}", e);
} }

View File

@ -228,7 +228,14 @@ impl EventQuoteConverter {
fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> { fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
match event { match event {
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info))) => { Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info))) => {
let info: String = info.chars().filter(|ch| !ch.is_whitespace()).collect(); let info: String = info
.chars()
.map(|x| match x {
' ' | '\t' => ',',
_ => x,
})
.filter(|ch| !ch.is_whitespace())
.collect();
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::from(info)))) Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::from(info))))
} }
@ -372,7 +379,7 @@ more text with spaces
``` ```
"#; "#;
let expected = r#"<pre><code class="language-rust,no_run,,,should_panic,,property_3"></code></pre> let expected = r#"<pre><code class="language-rust,,,,,no_run,,,should_panic,,,,property_3"></code></pre>
"#; "#;
assert_eq!(render_markdown(input, false), expected); assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected); assert_eq!(render_markdown(input, true), expected);

29
tests/cli/build.rs Normal file
View File

@ -0,0 +1,29 @@
use crate::dummy_book::DummyBook;
use assert_cmd::Command;
#[test]
fn mdbook_cli_dummy_book_generates_index_html() {
let temp = DummyBook::new().build().unwrap();
// doesn't exist before
assert!(!temp.path().join("book").exists());
let mut cmd = Command::cargo_bin("mdbook").unwrap();
cmd.arg("build").current_dir(temp.path());
cmd.assert()
.success()
.stderr(
predicates::str::is_match(r##"Stack depth exceeded in first[\\/]recursive.md."##)
.unwrap(),
)
.stderr(predicates::str::contains(
r##"[INFO] (mdbook::book): Running the html backend"##,
));
// exists afterward
assert!(temp.path().join("book").exists());
let index_file = temp.path().join("book/index.html");
assert!(index_file.exists());
}

2
tests/cli/mod.rs Normal file
View File

@ -0,0 +1,2 @@
mod build;
mod test;

34
tests/cli/test.rs Normal file
View File

@ -0,0 +1,34 @@
use crate::dummy_book::DummyBook;
use assert_cmd::Command;
use predicates::boolean::PredicateBooleanExt;
#[test]
fn mdbook_cli_can_correctly_test_a_passing_book() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut cmd = Command::cargo_bin("mdbook").unwrap();
cmd.arg("test").current_dir(temp.path());
cmd.assert().success()
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]README.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]intro.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]first[\\/]index.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]first[\\/]nested.md""##).unwrap())
.stderr(predicates::str::is_match(r##"rustdoc returned an error:\n\n"##).unwrap().not())
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap().not());
}
#[test]
fn mdbook_cli_detects_book_with_failing_tests() {
let temp = DummyBook::new().with_passing_test(false).build().unwrap();
let mut cmd = Command::cargo_bin("mdbook").unwrap();
cmd.arg("test").current_dir(temp.path());
cmd.assert().failure()
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]README.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]intro.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]first[\\/]index.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]first[\\/]nested.md""##).unwrap())
.stderr(predicates::str::is_match(r##"rustdoc returned an error:\n\n"##).unwrap())
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap());
}

2
tests/cli_tests.rs Normal file
View File

@ -0,0 +1,2 @@
mod cli;
mod dummy_book;

View File

@ -12,6 +12,7 @@
- [Recursive](first/recursive.md) - [Recursive](first/recursive.md)
- [Markdown](first/markdown.md) - [Markdown](first/markdown.md)
- [Unicode](first/unicode.md) - [Unicode](first/unicode.md)
- [No Headers](first/no-headers.md)
- [Second Chapter](second.md) - [Second Chapter](second.md)
- [Nested Chapter](second/nested.md) - [Nested Chapter](second/nested.md)

View File

@ -0,0 +1,3 @@
Capybara capybara capybara.
Capybara capybara capybara.

View File

@ -34,6 +34,7 @@ const TOC_SECOND_LEVEL: &[&str] = &[
"1.3. Recursive", "1.3. Recursive",
"1.4. Markdown", "1.4. Markdown",
"1.5. Unicode", "1.5. Unicode",
"1.6. No Headers",
"2.1. Nested Chapter", "2.1. Nested Chapter",
]; ];
@ -264,7 +265,7 @@ fn root_index_html() -> Result<Document> {
fn check_second_toc_level() { fn check_second_toc_level() {
let doc = root_index_html().unwrap(); let doc = root_index_html().unwrap();
let mut should_be = Vec::from(TOC_SECOND_LEVEL); let mut should_be = Vec::from(TOC_SECOND_LEVEL);
should_be.sort(); should_be.sort_unstable();
let pred = descendants!( let pred = descendants!(
Class("chapter"), Class("chapter"),
@ -288,7 +289,7 @@ fn check_first_toc_level() {
let mut should_be = Vec::from(TOC_TOP_LEVEL); let mut should_be = Vec::from(TOC_TOP_LEVEL);
should_be.extend(TOC_SECOND_LEVEL); should_be.extend(TOC_SECOND_LEVEL);
should_be.sort(); should_be.sort_unstable();
let pred = descendants!( let pred = descendants!(
Class("chapter"), Class("chapter"),
@ -535,7 +536,7 @@ fn redirects_are_emitted_correctly() {
let mut redirect_file = md.build_dir_for("html"); let mut redirect_file = md.build_dir_for("html");
// append everything except the bits that make it absolute // append everything except the bits that make it absolute
// (e.g. "/" or "C:\") // (e.g. "/" or "C:\")
redirect_file.extend(remove_absolute_components(&original)); redirect_file.extend(remove_absolute_components(original));
let contents = fs::read_to_string(&redirect_file).unwrap(); let contents = fs::read_to_string(&redirect_file).unwrap();
assert!(contents.contains(redirect)); assert!(contents.contains(redirect));
} }
@ -552,7 +553,7 @@ fn edit_url_has_default_src_dir_edit_url() {
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}" edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
"#; "#;
write_file(&temp.path(), "book.toml", book_toml.as_bytes()).unwrap(); write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path()).unwrap();
md.build().unwrap(); md.build().unwrap();
@ -560,7 +561,7 @@ fn edit_url_has_default_src_dir_edit_url() {
let index_html = temp.path().join("book").join("index.html"); let index_html = temp.path().join("book").join("index.html");
assert_contains_strings( assert_contains_strings(
index_html, index_html,
&vec![ &[
r#"href="https://github.com/rust-lang/mdBook/edit/master/guide/src/README.md" title="Suggest an edit""#, r#"href="https://github.com/rust-lang/mdBook/edit/master/guide/src/README.md" title="Suggest an edit""#,
], ],
); );
@ -578,7 +579,7 @@ fn edit_url_has_configured_src_dir_edit_url() {
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}" edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
"#; "#;
write_file(&temp.path(), "book.toml", book_toml.as_bytes()).unwrap(); write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
let md = MDBook::load(temp.path()).unwrap(); let md = MDBook::load(temp.path()).unwrap();
md.build().unwrap(); md.build().unwrap();
@ -586,7 +587,7 @@ fn edit_url_has_configured_src_dir_edit_url() {
let index_html = temp.path().join("book").join("index.html"); let index_html = temp.path().join("book").join("index.html");
assert_contains_strings( assert_contains_strings(
index_html, index_html,
&vec![ &[
r#"href="https://github.com/rust-lang/mdBook/edit/master/guide/src2/README.md" title="Suggest an edit""#, r#"href="https://github.com/rust-lang/mdBook/edit/master/guide/src2/README.md" title="Suggest an edit""#,
], ],
); );
@ -611,7 +612,7 @@ mod search {
let index = fs::read_to_string(index).unwrap(); let index = fs::read_to_string(index).unwrap();
let index = index.trim_start_matches("Object.assign(window.search, "); let index = index.trim_start_matches("Object.assign(window.search, ");
let index = index.trim_end_matches(");"); let index = index.trim_end_matches(");");
serde_json::from_str(&index).unwrap() serde_json::from_str(index).unwrap()
} }
#[test] #[test]
@ -631,6 +632,7 @@ mod search {
let introduction = get_doc_ref("intro.html#introduction"); let introduction = get_doc_ref("intro.html#introduction");
let some_section = get_doc_ref("first/index.html#some-section"); let some_section = get_doc_ref("first/index.html#some-section");
let summary = get_doc_ref("first/includes.html#summary"); let summary = get_doc_ref("first/includes.html#summary");
let no_headers = get_doc_ref("first/no-headers.html");
let conclusion = get_doc_ref("conclusion.html#conclusion"); let conclusion = get_doc_ref("conclusion.html#conclusion");
let bodyidx = &index["index"]["index"]["body"]["root"]; let bodyidx = &index["index"]["index"]["body"]["root"];
@ -644,13 +646,21 @@ mod search {
assert_eq!(docs[&some_section]["body"], ""); assert_eq!(docs[&some_section]["body"], "");
assert_eq!( assert_eq!(
docs[&summary]["body"], docs[&summary]["body"],
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode Second Chapter Nested Chapter Conclusion" "Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Second Chapter Nested Chapter Conclusion"
); );
assert_eq!( assert_eq!(
docs[&summary]["breadcrumbs"], docs[&summary]["breadcrumbs"],
"First Chapter » Includes » Summary" "First Chapter » Includes » Summary"
); );
assert_eq!(docs[&conclusion]["body"], "I put &lt;HTML&gt; in here!"); assert_eq!(docs[&conclusion]["body"], "I put &lt;HTML&gt; in here!");
assert_eq!(
docs[&no_headers]["breadcrumbs"],
"First Chapter » No Headers"
);
assert_eq!(
docs[&no_headers]["body"],
"Capybara capybara capybara. Capybara capybara capybara."
);
} }
// Setting this to `true` may cause issues with `cargo watch`, // Setting this to `true` may cause issues with `cargo watch`,

File diff suppressed because it is too large Load Diff