Merge branch 'rust-lang:master' into #1564
This commit is contained in:
commit
085ec42609
|
@ -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
|
||||||
|
|
67
CHANGELOG.md
67
CHANGELOG.md
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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).
|
|
@ -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.)
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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/).
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
# }
|
# }
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
257
src/book/mod.rs
257
src/book/mod.rs
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
74
src/main.rs
74
src/main.rs
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(§ion)?;
|
out.write(section)?;
|
||||||
out.write("</strong> ")?;
|
out.write("</strong> ")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -175,3 +175,7 @@ blockquote {
|
||||||
margin: 5px 0px;
|
margin: 5px 0px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-no-output {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
mod build;
|
||||||
|
mod test;
|
|
@ -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());
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
mod cli;
|
||||||
|
mod dummy_book;
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Capybara capybara capybara.
|
||||||
|
|
||||||
|
Capybara capybara capybara.
|
|
@ -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 <HTML> in here!");
|
assert_eq!(docs[&conclusion]["body"], "I put <HTML> 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
Loading…
Reference in New Issue