mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 12:41:16 -05:00
Compare commits
205 Commits
smart-prep
...
v0.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c4d2070f7 | ||
|
|
50d5917530 | ||
|
|
9cd47eb80f | ||
|
|
4932df2570 | ||
|
|
11d31c989c | ||
|
|
e5ace6d6a4 | ||
|
|
e7c3d02c61 | ||
|
|
d8a68ba3f6 | ||
|
|
d29a79349c | ||
|
|
d6088c8a57 | ||
|
|
b91e5c8807 | ||
|
|
6199e4df79 | ||
|
|
2d11eb05fe | ||
|
|
3d45e40693 | ||
|
|
228e99ba11 | ||
|
|
4b569edadd | ||
|
|
3e652b5bfc | ||
|
|
ba41d73dc3 | ||
|
|
1ce1401263 | ||
|
|
00b3d9cf86 | ||
|
|
bb3398bdbb | ||
|
|
19c26217c0 | ||
|
|
a2029f0a78 | ||
|
|
7c33ac800c | ||
|
|
d371001ab8 | ||
|
|
d73504eb23 | ||
|
|
abddd7c6f7 | ||
|
|
31e36f85e7 | ||
|
|
92a7b0cdcd | ||
|
|
592140db5b | ||
|
|
3a0eeb4bbb | ||
|
|
a9dae326fa | ||
|
|
abba959add | ||
|
|
ea15e55829 | ||
|
|
d07bd9fed4 | ||
|
|
b83c55f7ef | ||
|
|
69a08ef390 | ||
|
|
1cd1151790 | ||
|
|
84d4063e4a | ||
|
|
07830f7f11 | ||
|
|
828b7d05c5 | ||
|
|
379004efcb | ||
|
|
2497e77bf1 | ||
|
|
0c2292b9aa | ||
|
|
4386a10e87 | ||
|
|
3cfed10098 | ||
|
|
a655d5d241 | ||
|
|
f8c3a2deea | ||
|
|
b226d2fc55 | ||
|
|
53ba0d6655 | ||
|
|
43ead86ecc | ||
|
|
1d3ec7e0c7 | ||
|
|
f4017376a9 | ||
|
|
672cf456eb | ||
|
|
8dce00d54d | ||
|
|
4f7c299de7 | ||
|
|
04e74bfa1b | ||
|
|
4026a586a1 | ||
|
|
71281bff10 | ||
|
|
8542f7f29d | ||
|
|
fe492d1cb9 | ||
|
|
481c2f2194 | ||
|
|
882014860c | ||
|
|
e3ec751a3f | ||
|
|
fc565df86b | ||
|
|
ec8e63145c | ||
|
|
2752c88c46 | ||
|
|
e7befd19bc | ||
|
|
644b8e132c | ||
|
|
8e82ae534a | ||
|
|
6a8a5b7642 | ||
|
|
c3284a2ae9 | ||
|
|
df12cc55c8 | ||
|
|
cb4a3e0711 | ||
|
|
9194a40acd | ||
|
|
506996808b | ||
|
|
5163c5ab75 | ||
|
|
ecfaed1e02 | ||
|
|
8bb5426441 | ||
|
|
a674c9eff1 | ||
|
|
7ab939f8f2 | ||
|
|
581187098c | ||
|
|
ab7802a9a9 | ||
|
|
345acb8597 | ||
|
|
6380526e93 | ||
|
|
4560bdeb47 | ||
|
|
0aa3a9045a | ||
|
|
b30b58b565 | ||
|
|
c6220fba83 | ||
|
|
652eab6e7e | ||
|
|
5726a8afd6 | ||
|
|
7e26a8430d | ||
|
|
07a64b110a | ||
|
|
dd69e03ff5 | ||
|
|
7f3a0ff6a0 | ||
|
|
aea317e173 | ||
|
|
f9454615b1 | ||
|
|
39211291d9 | ||
|
|
f01fe854fa | ||
|
|
6eeaaaa44d | ||
|
|
357ebcf7ce | ||
|
|
1a4f38eace | ||
|
|
1d3f83eede | ||
|
|
9712347b9c | ||
|
|
f73d42d994 | ||
|
|
a647017e4b | ||
|
|
a66d44190e | ||
|
|
01fd7a76f0 | ||
|
|
99dc62f9c3 | ||
|
|
b891fd5a12 | ||
|
|
02fa7b0a11 | ||
|
|
8b2e1c2daa | ||
|
|
88d2f69138 | ||
|
|
cb94053779 | ||
|
|
0a8707b1e6 | ||
|
|
0dc2728fa9 | ||
|
|
9b02cd7496 | ||
|
|
11f86f4511 | ||
|
|
4abac12c04 | ||
|
|
d7c7d91005 | ||
|
|
9243cf9d95 | ||
|
|
d2470730fc | ||
|
|
6a2e2461fb | ||
|
|
3faa3e42f0 | ||
|
|
9c8fae4704 | ||
|
|
9b6f5a9840 | ||
|
|
62af2367bb | ||
|
|
37808b7e08 | ||
|
|
b37f21a09b | ||
|
|
966632a724 | ||
|
|
c7281459f9 | ||
|
|
ae3f87ad0c | ||
|
|
c068703028 | ||
|
|
6cbc41d413 | ||
|
|
25c1ca1275 | ||
|
|
acbb951240 | ||
|
|
9e96165d8f | ||
|
|
5c5ef2f86b | ||
|
|
23ac06e2eb | ||
|
|
2ddbb37f49 | ||
|
|
a481735fa2 | ||
|
|
954cfa86e5 | ||
|
|
7e52da3c1b | ||
|
|
4e8d051bd1 | ||
|
|
78ee8e43bb | ||
|
|
b675b91980 | ||
|
|
3d8db7f25c | ||
|
|
3d37e24c14 | ||
|
|
eb19d2d654 | ||
|
|
1052ee92e1 | ||
|
|
3598e905aa | ||
|
|
3f002979c4 | ||
|
|
742dbbc917 | ||
|
|
991a725c26 | ||
|
|
317c7731da | ||
|
|
4c17b11ed0 | ||
|
|
005dfc55bf | ||
|
|
8c86031384 | ||
|
|
5151aae07e | ||
|
|
42b87e0fbc | ||
|
|
33add4b532 | ||
|
|
b0513ee771 | ||
|
|
b4538da9c3 | ||
|
|
7ac3e50b37 | ||
|
|
13a9aab2b2 | ||
|
|
eccec9bb52 | ||
|
|
e63f53fe47 | ||
|
|
2c20c99d4a | ||
|
|
c6125b184f | ||
|
|
dfb6e3cb10 | ||
|
|
cffc385b0c | ||
|
|
e73928f933 | ||
|
|
41071a5dd9 | ||
|
|
f6a7432569 | ||
|
|
89ea60e7a5 | ||
|
|
10b69e60c8 | ||
|
|
336e08fe50 | ||
|
|
5bfdf9fcc8 | ||
|
|
29f8b791f1 | ||
|
|
877bf37d18 | ||
|
|
d2565af000 | ||
|
|
599e47f1f1 | ||
|
|
0c31ab2953 | ||
|
|
b1c7c54108 | ||
|
|
f654c42426 | ||
|
|
0c926b3e88 | ||
|
|
e4eddb3f26 | ||
|
|
adec78e7f5 | ||
|
|
5cd5e4764c | ||
|
|
132f4fd358 | ||
|
|
1d72cea972 | ||
|
|
1aa1194d79 | ||
|
|
304234c122 | ||
|
|
729c94a7e4 | ||
|
|
df874cdbdb | ||
|
|
5dce539928 | ||
|
|
206a00915b | ||
|
|
ced74ca4dd | ||
|
|
09667c9956 | ||
|
|
d729a762fe | ||
|
|
43b3d157d9 | ||
|
|
a9f3be6f44 | ||
|
|
34356b87a0 | ||
|
|
48c97dadd0 | ||
|
|
65198a7632 |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
* text=auto eol=lf
|
||||
*.rs rust
|
||||
*.woff -text
|
||||
*.ttf -text
|
||||
*.otf -text
|
||||
*.png -text
|
||||
*.woff binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.png binary
|
||||
|
||||
70
.travis.yml
70
.travis.yml
@@ -1,42 +1,76 @@
|
||||
language: rust
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
cache:
|
||||
timeout: 360
|
||||
cargo: true
|
||||
|
||||
before_cache:
|
||||
- chmod -R a+r $HOME/.cargo
|
||||
cache:
|
||||
directories:
|
||||
- "$HOME/.cargo"
|
||||
- "$HOME/.cache/sccache"
|
||||
|
||||
env:
|
||||
global:
|
||||
- CRATE_NAME=mdbook
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- rust: stable
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
- rust: beta
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
- rust: nightly
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
- rust: 1.34.0 # Minimum required Rust version
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
|
||||
- rust: stable
|
||||
os: osx
|
||||
env: TARGET=x86_64-apple-darwin
|
||||
|
||||
before_install:
|
||||
- |
|
||||
export RUSTC_WRAPPER=sccache;
|
||||
cd "$(mktemp -d)";
|
||||
case "$TRAVIS_OS_NAME" in
|
||||
linux )
|
||||
travis_retry curl -sSL 'https://github.com/mozilla/sccache/releases/download/0.2.9/sccache-0.2.9-x86_64-unknown-linux-musl.tar.gz' | tar -xzf - --strip-components=1 &&
|
||||
sudo cp sccache /usr/local/bin/sccache;
|
||||
;;
|
||||
osx )
|
||||
travis_retry curl -sSL 'https://github.com/mozilla/sccache/releases/download/0.2.9/sccache-0.2.9-x86_64-apple-darwin.tar.gz' | tar -xzf - --strip-components=1 &&
|
||||
sudo cp sccache /usr/local/bin/sccache;
|
||||
;;
|
||||
* ) unset RUSTC_WRAPPER;;
|
||||
esac;
|
||||
cd "$TRAVIS_BUILD_DIR";
|
||||
|
||||
script:
|
||||
- cargo test --all
|
||||
- cargo test --all --no-default-features
|
||||
- |
|
||||
if [ "$TARGET" = x86_64-unknown-linux-gnu ] && [ "$TRAVIS_RUST_VERSION" = stable ]; then
|
||||
rustup component add rustfmt && \
|
||||
cargo fmt --all -- --check;
|
||||
fi
|
||||
|
||||
before_deploy:
|
||||
- cargo run -- build book-example
|
||||
- sh ci/before_deploy.sh
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
- secure: cURRWBr034iqBz/ifD7uOunBfNR30YxIXfgLX0osWz+iafkVbhDGYYz9sBmRraqO2P7L2koEXMADVb/md1kI2+ykiq/ml+l9zuEAZPVmvSGUN7ZD+7s+lu3l5OBPG5z175T+b2q2q2m8XVR7TW20ra4QbE0bq06KAoOyjSgQVBTSCYsL9uTsGwiVRMEqqJT/BmKhKJNkpGsTKyBSKkOXvfeAAbE260vXUDEN9TYdJ3fvteRrpwLX56ee64gIZUq0RjDc4SKIEqilM6iUtNMvurqaewYNGkiXKRruV6BPCHxEHo6NNT46kOJLBJTf7gZw//dWhSoWpg9P0gdAnPWm407kSa3F7aJ1eRShAFQ4BLyfz9efTqm+jP3fOp7Mm7igSh9w6caSRuOnSsUf5+raRQ8E5Y9HsWGzzpZQk24Fx9EGZ04EeDSdpZAFz+jcbMpHf8t2p4CEx0CCNwYvKx6EydMKbMF5QteQ8SQkXNLhv7Rz2OgtXWYZPRVCMfQfOplsi2InsLCrQxTgwh+6u654SqVSgaHG+IncEAxBrdWy4rHcg7qereUcKfcY3k96vaDxdn/T2c00Ig0aNFR91YnixGMd6J6tQgDcRK9jh6fUm1CCBE9hT+pNUmtgYKuWBoLZexUZFFnfuBed0WciBot1bGDDamndqKq0jJiAzg+GMHk=
|
||||
- provider: releases
|
||||
api_key: "$GITHUB_TOKEN"
|
||||
file_glob: true
|
||||
file: "$CRATE_NAME-$TRAVIS_TAG-$TARGET.*"
|
||||
on:
|
||||
condition: "$TRAVIS_RUST_VERSION = stable"
|
||||
tags: true
|
||||
skip_cleanup: true
|
||||
- provider: pages
|
||||
local_dir: book-example/book
|
||||
skip_cleanup: true
|
||||
github_token: "$GITHUB_TOKEN"
|
||||
keep_history: false
|
||||
on:
|
||||
condition: $TRAVIS_OS_NAME = "linux" && $TRAVIS_RUST_VERSION = "stable"
|
||||
tags: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
|
||||
212
CHANGELOG.md
Normal file
212
CHANGELOG.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.3.1
|
||||
[69a08ef...9cd47eb](https://github.com/rust-lang-nursery/mdBook/compare/69a08ef...9cd47eb)
|
||||
|
||||
### Added
|
||||
- 🔥 Added ability to include files using anchor points instead of line numbers.
|
||||
[#851](https://github.com/rust-lang-nursery/mdBook/pull/851)
|
||||
- Added `language` configuration value to set the language of the book, which
|
||||
will affect things like the `<html lang="en">` tag.
|
||||
[#941](https://github.com/rust-lang-nursery/mdBook/pull/941)
|
||||
|
||||
### Changed
|
||||
- Updated to handlebars 2.0.
|
||||
[#977](https://github.com/rust-lang-nursery/mdBook/pull/977)
|
||||
|
||||
### Fixed
|
||||
- Fixed memory leak warning.
|
||||
[#967](https://github.com/rust-lang-nursery/mdBook/pull/967)
|
||||
- Fix more print.html links.
|
||||
[#963](https://github.com/rust-lang-nursery/mdBook/pull/963)
|
||||
- Fixed crash on some unicode input.
|
||||
[#978](https://github.com/rust-lang-nursery/mdBook/pull/978)
|
||||
|
||||
## mdBook 0.3.0
|
||||
[6cbc41d...69a08ef](https://github.com/rust-lang-nursery/mdBook/compare/6cbc41d...69a08ef)
|
||||
|
||||
### Added
|
||||
- Added ability to resize the sidebar.
|
||||
[#849](https://github.com/rust-lang-nursery/mdBook/pull/849)
|
||||
- Added `load_with_config_and_summary` function to `MDBook` to be able to
|
||||
build a book with a custom `Summary`.
|
||||
[#883](https://github.com/rust-lang-nursery/mdBook/pull/883)
|
||||
- Set `noindex` on `print.html` page to prevent robots from indexing it.
|
||||
[#844](https://github.com/rust-lang-nursery/mdBook/pull/844)
|
||||
- Added support for ~~strikethrough~~ and GitHub-style tasklists.
|
||||
[#952](https://github.com/rust-lang-nursery/mdBook/pull/952)
|
||||
|
||||
### Changed
|
||||
- Command-line help output is now colored.
|
||||
[#861](https://github.com/rust-lang-nursery/mdBook/pull/861)
|
||||
- The build directory is now deleted before rendering starts, instead of after
|
||||
if finishes.
|
||||
[#878](https://github.com/rust-lang-nursery/mdBook/pull/878)
|
||||
- Removed dependency on `same-file` crate.
|
||||
[#903](https://github.com/rust-lang-nursery/mdBook/pull/903)
|
||||
- 💥 Renamed `with_preprecessor` to `with_preprocessor`.
|
||||
[#906](https://github.com/rust-lang-nursery/mdBook/pull/906)
|
||||
- Updated ACE editor to 1.4.4, should remove a JavaScript console warning.
|
||||
[#935](https://github.com/rust-lang-nursery/mdBook/pull/935)
|
||||
- Dependencies have been updated.
|
||||
[#934](https://github.com/rust-lang-nursery/mdBook/pull/934)
|
||||
[#945](https://github.com/rust-lang-nursery/mdBook/pull/945)
|
||||
- Highlight.js has been updated. This fixes some TOML highlighting, and adds
|
||||
Julia support.
|
||||
[#942](https://github.com/rust-lang-nursery/mdBook/pull/942)
|
||||
- 🔥 Updated to pulldown-cmark 0.5. This may have significant changes to the
|
||||
formatting of existing books, as the newer version has more accurate
|
||||
interpretation of the CommonMark spec and a large number of bug fixes and
|
||||
changes.
|
||||
[#898](https://github.com/rust-lang-nursery/mdBook/pull/898)
|
||||
- The `diff` language should now highlight correctly.
|
||||
[#943](https://github.com/rust-lang-nursery/mdBook/pull/943)
|
||||
- Make the blank region of a header not clickable.
|
||||
[#948](https://github.com/rust-lang-nursery/mdBook/pull/948)
|
||||
- Rustdoc tests now use the preprocessed content instead of the raw,
|
||||
unpreprocessed content.
|
||||
[#891](https://github.com/rust-lang-nursery/mdBook/pull/891)
|
||||
|
||||
### Fixed
|
||||
- Fixed file change detection so that `mdbook serve` only reloads once when
|
||||
multiple files are changed at once.
|
||||
[#870](https://github.com/rust-lang-nursery/mdBook/pull/870)
|
||||
- Fixed on-hover color highlighting for links in sidebar.
|
||||
[#834](https://github.com/rust-lang-nursery/mdBook/pull/834)
|
||||
- Fixed loss of focus when clicking the "Copy" button in code blocks.
|
||||
[#867](https://github.com/rust-lang-nursery/mdBook/pull/867)
|
||||
- Fixed incorrectly stripping the path for `additional-js` files.
|
||||
[#796](https://github.com/rust-lang-nursery/mdBook/pull/796)
|
||||
- Fixed color of `code spans` that are links.
|
||||
[#905](https://github.com/rust-lang-nursery/mdBook/pull/905)
|
||||
- Fixed "next" navigation on index.html.
|
||||
[#916](https://github.com/rust-lang-nursery/mdBook/pull/916)
|
||||
- Fixed keyboard chapter navigation for `file` urls.
|
||||
[#915](https://github.com/rust-lang-nursery/mdBook/pull/915)
|
||||
- Fixed bad wrapping for inline code on some browsers.
|
||||
[#818](https://github.com/rust-lang-nursery/mdBook/pull/818)
|
||||
- Properly load an existing `SUMMARY.md` in `mdbook init`.
|
||||
[#841](https://github.com/rust-lang-nursery/mdBook/pull/841)
|
||||
- Fixed some broken links in `print.html`.
|
||||
[#871](https://github.com/rust-lang-nursery/mdBook/pull/871)
|
||||
- The Rust Playground link now supports the 2018 edition.
|
||||
[#946](https://github.com/rust-lang-nursery/mdBook/pull/946)
|
||||
|
||||
## mdBook 0.2.3 (2018-01-18)
|
||||
[2c20c99...6cbc41d](https://github.com/rust-lang-nursery/mdBook/compare/2c20c99...6cbc41d)
|
||||
|
||||
### Added
|
||||
- Added an optional button to the top of the page which will link to a git
|
||||
repository. Use the `git-repository-url` and `git-repository-icon` options
|
||||
in the `[output.html]` section to enable it and set its appearance.
|
||||
[#802](https://github.com/rust-lang-nursery/mdBook/pull/802)
|
||||
- Added a `default-theme` option to the `[output.html]` section.
|
||||
[#804](https://github.com/rust-lang-nursery/mdBook/pull/804)
|
||||
|
||||
### Changed
|
||||
- 💥 Header ID anchors no longer add an arbitrary `a` character for headers
|
||||
that start with a non-ascii-alphabetic character.
|
||||
[#788](https://github.com/rust-lang-nursery/mdBook/pull/788)
|
||||
|
||||
### Fixed
|
||||
- Fix websocket hostname usage
|
||||
[#865](https://github.com/rust-lang-nursery/mdBook/pull/865)
|
||||
- Fixing links in print.html
|
||||
[#866](https://github.com/rust-lang-nursery/mdBook/pull/866)
|
||||
|
||||
## mdBook 0.2.2 (2018-10-19)
|
||||
[7e2e095...2c20c99](https://github.com/rust-lang-nursery/mdBook/compare/7e2e095...2c20c99)
|
||||
|
||||
### Added
|
||||
- 🎉 Process-based custom preprocessors. See [the
|
||||
docs](https://rust-lang-nursery.github.io/mdBook/for_developers/preprocessors.html)
|
||||
for more.
|
||||
[#792](https://github.com/rust-lang-nursery/mdBook/pull/792)
|
||||
|
||||
- 🎉 Configurable preprocessors.
|
||||
|
||||
Added `build.use-default-preprocessors` boolean TOML key to allow disabling
|
||||
the built-in `links` and `index` preprocessors.
|
||||
|
||||
Added `[preprocessor]` TOML tables to configure each preprocessor.
|
||||
|
||||
Specifying `[preprocessor.links]` or `[preprocessor.index]` will enable the
|
||||
respective built-in preprocessor if `build.use-default-preprocessors` is
|
||||
`false`.
|
||||
|
||||
Added `fn supports_renderer(&self, renderer: &str) -> bool` to the
|
||||
`Preprocessor` trait to specify if the preprocessor supports the given
|
||||
renderer. The default implementation always returns `true`.
|
||||
|
||||
`Preprocessor::run` now takes a book by value instead of a mutable
|
||||
reference. It should return a `Book` value with the intended modifications.
|
||||
|
||||
Added `PreprocessorContext::renderer` to indicate the renderer being used.
|
||||
|
||||
[#658](https://github.com/rust-lang-nursery/mdBook/pull/658)
|
||||
[#787](https://github.com/rust-lang-nursery/mdBook/pull/787)
|
||||
|
||||
### Fixed
|
||||
- Fix paths to additional CSS and JavaScript files
|
||||
[#777](https://github.com/rust-lang-nursery/mdBook/pull/777)
|
||||
- Ensure section numbers are correctly incremented after a horizontal
|
||||
separator
|
||||
[#790](https://github.com/rust-lang-nursery/mdBook/pull/790)
|
||||
|
||||
## mdBook 0.2.1 (2018-08-22)
|
||||
[91ffca1...7e2e095](https://github.com/rust-lang-nursery/mdBook/compare/91ffca1...7e2e095)
|
||||
|
||||
### Changed
|
||||
- Update to handlebars-rs 1.0
|
||||
[#761](https://github.com/rust-lang-nursery/mdBook/pull/761)
|
||||
|
||||
### Fixed
|
||||
- Fix table colors, broken by Stylus -> CSS transition
|
||||
[#765](https://github.com/rust-lang-nursery/mdBook/pull/765)
|
||||
|
||||
## mdBook 0.2.0 (2018-08-02)
|
||||
|
||||
### Changed
|
||||
- 💥 This release changes how links are handled in mdBook. Previously, relative
|
||||
links were interpreted relative to the book's root. In `0.2.0`+ links are
|
||||
relative to the page they are in, and use the `.md` extension. This has [several
|
||||
advantages](https://github.com/rust-lang-nursery/mdBook/pull/603#issue-166701447),
|
||||
such as making links work in other markdown viewers like GitHub. You will
|
||||
likely have to change links in your book to accommodate this change. For
|
||||
example, a book with this layout:
|
||||
|
||||
```
|
||||
chapter_1/
|
||||
section_1.md
|
||||
section_2.md
|
||||
SUMMARY.md
|
||||
```
|
||||
|
||||
Previously a link in `section_1.md` to `section_2.md` would look like this:
|
||||
```markdown
|
||||
[section_2](chapter_1/section_2.html)
|
||||
```
|
||||
|
||||
Now it must be changed to this:
|
||||
```markdown
|
||||
[section_2](section_2.md)
|
||||
```
|
||||
|
||||
- 💥 `mdbook test --library-path` now accepts a comma-delimited list of
|
||||
arguments rather than taking all following arguments. This makes it easier
|
||||
to handle the trailing book directory argument without always needing to put
|
||||
` -- ` before it. Multiple instances of the option continue to be accepted:
|
||||
`mdbook test -L foo -L bar`.
|
||||
|
||||
- 💥 `mdbook serve` has some of its options renamed for clarity. See `mdbook
|
||||
help serve` for details.
|
||||
|
||||
- Embedded rust playpens now use the "stable" playground API.
|
||||
[#754](https://github.com/rust-lang-nursery/mdBook/pull/754)
|
||||
|
||||
### Fixed
|
||||
- Escaped includes (`\{{#include file.rs}}`) will now render correctly.
|
||||
[f30ce01](https://github.com/rust-lang-nursery/mdBook/commit/f30ce0184d71e342141145472bf816419d30a2c5)
|
||||
- `index.html` will now render correctly when the book's first section is
|
||||
inside a subdirectory.
|
||||
[#756](https://github.com/rust-lang-nursery/mdBook/pull/756)
|
||||
@@ -48,6 +48,54 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
|
||||
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
|
||||
|
||||
### Code Quality
|
||||
|
||||
We love code quality and Rust has some excellent tools to assist you with contributions.
|
||||
|
||||
#### Formatting Code with rustfmt
|
||||
|
||||
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
|
||||
This will ensure we have good quality source code that is better for us all to maintain.
|
||||
|
||||
[rustfmt](https://github.com/rust-lang-nursery/rustfmt) has a lot more information on the project.
|
||||
The quick guide is
|
||||
|
||||
1. Install it
|
||||
```
|
||||
rustup component add rustfmt
|
||||
```
|
||||
1. You can now run `rustfmt` on a single file simply by...
|
||||
```
|
||||
rustfmt src/path/to/your/file.rs
|
||||
```
|
||||
... or you can format the entire project with
|
||||
```
|
||||
cargo fmt
|
||||
```
|
||||
When run through `cargo` it will format all bin and lib files in the current crate.
|
||||
|
||||
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang-nursery/rustfmt)
|
||||
|
||||
|
||||
#### Finding Issues with Clippy
|
||||
|
||||
Clippy is a code analyser/linter detecting mistakes, and therfore helps to improve your code.
|
||||
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will
|
||||
help us maintain awesome code.
|
||||
|
||||
The best documentation can be found over at [rust-clippy](https://github.com/rust-lang-nursery/rust-clippy)
|
||||
|
||||
1. To install
|
||||
```
|
||||
rustup component add clippy
|
||||
```
|
||||
2. Running clippy
|
||||
```
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
Clippy has an ever growing list of checks, that are managed in [lint files](https://rust-lang-nursery.github.io/rust-clippy/master/index.html).
|
||||
|
||||
### Making a pull-request
|
||||
|
||||
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
|
||||
|
||||
1316
Cargo.lock
generated
1316
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
40
Cargo.toml
40
Cargo.toml
@@ -1,39 +1,40 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.2.2-alpha.0"
|
||||
version = "0.3.1"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
"Matt Ickstadt <mattico8@gmail.com>"
|
||||
]
|
||||
description = "Create books from markdown files"
|
||||
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
|
||||
repository = "https://github.com/rust-lang-nursery/mdBook"
|
||||
edition = "2018"
|
||||
exclude = ["/book-example/*"]
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
exclude = ["book-example/*"]
|
||||
repository = "https://github.com/rust-lang-nursery/mdBook"
|
||||
description = "Creates a book from markdown files"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.24"
|
||||
chrono = "0.4"
|
||||
handlebars = "1.0"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
clap = "2.24"
|
||||
env_logger = "0.6"
|
||||
error-chain = "0.12"
|
||||
serde_json = "1.0"
|
||||
pulldown-cmark = "0.1.2"
|
||||
handlebars = { version = "2.0", default-features = false, features = ["no_dir_source"] }
|
||||
itertools = "0.8"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.5"
|
||||
toml = "0.4"
|
||||
memchr = "2.0"
|
||||
open = "1.1"
|
||||
pulldown-cmark = "0.5"
|
||||
regex = "1.0.0"
|
||||
tempfile = "3.0"
|
||||
itertools = "0.7"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
shlex = "0.1"
|
||||
toml-query = "0.7"
|
||||
tempfile = "3.0"
|
||||
toml = "0.5.1"
|
||||
toml-query = "0.9"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "4.0", optional = true }
|
||||
@@ -41,17 +42,16 @@ notify = { version = "4.0", optional = true }
|
||||
# Serve feature
|
||||
iron = { version = "0.6", optional = true }
|
||||
staticfile = { version = "0.5", optional = true }
|
||||
ws = { version = "0.7", optional = true}
|
||||
ws = { version = "0.8", optional = true}
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
||||
ammonia = { version = "1.1", optional = true }
|
||||
ammonia = { version = "2.1.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
select = "0.4"
|
||||
pretty_assertions = "0.5"
|
||||
pretty_assertions = "0.6"
|
||||
walkdir = "2.0"
|
||||
pulldown-cmark-to-cmark = "1.1.0"
|
||||
|
||||
[features]
|
||||
default = ["output", "watch", "serve", "search"]
|
||||
|
||||
65
README.md
65
README.md
@@ -4,7 +4,7 @@
|
||||
<tr>
|
||||
<td><strong>Linux / OS X</strong></td>
|
||||
<td>
|
||||
<a href="https://travis-ci.org/rust-lang-nursery/mdBook"><img src="https://travis-ci.org/rust-lang-nursery/mdBook.svg?branch=master"></a>
|
||||
<a href="https://travis-ci.com/rust-lang-nursery/mdBook"><img src="https://travis-ci.com/rust-lang-nursery/mdBook.svg?branch=master"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -41,7 +41,7 @@ There are multiple ways to install mdBook.
|
||||
|
||||
2. **From Crates.io**
|
||||
|
||||
This requires at least [Rust] 1.20 and Cargo to be installed. Once you have installed
|
||||
This requires at least [Rust] 1.34 and Cargo to be installed. Once you have installed
|
||||
Rust, type the following in the terminal:
|
||||
|
||||
```
|
||||
@@ -57,7 +57,7 @@ There are multiple ways to install mdBook.
|
||||
another CI server, we recommend that you specify a semver version range for
|
||||
mdBook when you install it through your script!
|
||||
|
||||
This will constrain the server to install the latests **non-breaking**
|
||||
This will constrain the server to install the latest **non-breaking**
|
||||
version of mdBook and will prevent your books from failing to build because
|
||||
we released a new version. For example:
|
||||
|
||||
@@ -65,7 +65,7 @@ There are multiple ways to install mdBook.
|
||||
cargo install mdbook --vers "^0.1.0"
|
||||
```
|
||||
|
||||
3. **From Git**
|
||||
3. **From Git**
|
||||
|
||||
The version published to crates.io will ever so slightly be behind the
|
||||
version hosted here on GitHub. If you need the latest version you can build
|
||||
@@ -77,7 +77,7 @@ There are multiple ways to install mdBook.
|
||||
|
||||
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
4. **For Contributions**
|
||||
4. **For Contributions**
|
||||
|
||||
If you want to contribute to mdBook you will have to clone the repository on
|
||||
your local machine:
|
||||
@@ -145,6 +145,57 @@ explanation, check out the [User Guide].
|
||||
|
||||
Delete directory in which generated book is located.
|
||||
|
||||
### 3rd Party Plugins
|
||||
|
||||
The way a book is loaded and rendered can be configured by the user via third
|
||||
party plugins. These plugins are just programs which will be invoked during the
|
||||
build process and are split into roughly two categories, *preprocessors* and
|
||||
*renderers*.
|
||||
|
||||
Preprocessors are used to transform a book before it is sent to a renderer.
|
||||
One example would be to replace all occurrences of
|
||||
`{{#include some_file.ext}}` with the contents of that file. Some existing
|
||||
preprocessors are:
|
||||
|
||||
- `index` - a built-in preprocessor (enabled by default) which will transform
|
||||
all `README.md` chapters to `index.md` so `foo/README.md` can be accessed via
|
||||
the url `foo/` when published to a browser
|
||||
- `links` - a built-in preprocessor (enabled by default) for expanding the
|
||||
`{{# playpen}}` and `{{# include}}` helpers in a chapter.
|
||||
|
||||
Renderers are given the final book so they can do something with it. This is
|
||||
typically used for, as the name suggests, rendering the document in a particular
|
||||
format, however there's nothing stopping a renderer from doing static analysis
|
||||
of a book in order to validate links or run tests. Some existing renderers are:
|
||||
|
||||
- `html` - the built-in renderer which will generate a HTML version of the book
|
||||
- [`linkcheck`] - a backend which will check that all links are valid
|
||||
- [`epub`] - an experimental EPUB generator
|
||||
|
||||
> **Note for Developers:** Feel free to send us a PR if you've developed your
|
||||
> own plugin and want it mentioned here.
|
||||
|
||||
A preprocessor or renderer is enabled by installing the appropriate program and
|
||||
then mentioning it in the book's `book.toml` file.
|
||||
|
||||
```console
|
||||
$ cargo install mdbook-linkcheck
|
||||
$ edit book.toml && cat book.toml
|
||||
[book]
|
||||
title = "My Awesome Book"
|
||||
authors = ["Michael-F-Bryan"]
|
||||
|
||||
[output.html]
|
||||
|
||||
[output.linkcheck] # enable the "mdbook-linkcheck" renderer
|
||||
|
||||
$ mdbook build
|
||||
2018-10-20 13:57:51 [INFO] (mdbook::book): Book building has started
|
||||
2018-10-20 13:57:51 [INFO] (mdbook::book): Running the html backend
|
||||
2018-10-20 13:57:53 [INFO] (mdbook::book): Running the linkcheck backend
|
||||
```
|
||||
|
||||
For more information on the plugin system, consult the [User Guide].
|
||||
|
||||
### As a library
|
||||
|
||||
@@ -188,4 +239,6 @@ All the code in this repository is released under the ***Mozilla Public License
|
||||
[releases]: https://github.com/rust-lang-nursery/mdBook/releases
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[CLI docs]: http://rust-lang-nursery.github.io/mdBook/cli/init.html
|
||||
[master-docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
|
||||
[master-docs]: http://rust-lang-nursery.github.io/mdBook/
|
||||
[`linkcheck`]: https://crates.io/crates/mdbook-linkcheck
|
||||
[`epub`]: https://crates.io/crates/mdbook-epub
|
||||
|
||||
13
appveyor.yml
13
appveyor.yml
@@ -7,19 +7,15 @@ environment:
|
||||
RUST_CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: stable
|
||||
# Beta channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: beta
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: beta
|
||||
# Nightly channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: nightly
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: nightly
|
||||
|
||||
# Install Rust and Cargo
|
||||
install:
|
||||
# Since rust-lang-libs is currently sharing 1 builder, only run 1 job when a
|
||||
# PR is opened. Merges to master or tags will run all jobs.
|
||||
- ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -gt 0 -and ($env:TARGET -ne "x86_64-pc-windows-msvc" -or $env:RUST_CHANNEL -ne "stable") ) {Exit-AppveyorBuild}
|
||||
- ps: >-
|
||||
If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\msys64\mingw64\bin'
|
||||
@@ -51,8 +47,7 @@ before_deploy:
|
||||
deploy:
|
||||
description: 'Windows release'
|
||||
artifact: /.*\.zip/
|
||||
auth_token:
|
||||
secure: QQhjKVyz7mpjlyGhlXytbFQQfKFQWTahHkD+B0NzIUoEVqO7ZLWjnoWasvLqW4nE
|
||||
auth_token: $(GITHUB_TOKEN)
|
||||
provider: GitHub
|
||||
on:
|
||||
RUST_CHANNEL: stable
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David", "Michael-F-Bryan"]
|
||||
language = "en"
|
||||
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- [Continuous Integration](continuous-integration.md)
|
||||
- [For Developers](for_developers/README.md)
|
||||
- [Preprocessors](for_developers/preprocessors.md)
|
||||
- [Alternate Backends](for_developers/backends.md)
|
||||
- [Alternative Backends](for_developers/backends.md)
|
||||
|
||||
-----------
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ mdBook can also be installed from source
|
||||
|
||||
mdBook is written in **[Rust](https://www.rust-lang.org/)** and therefore needs
|
||||
to be compiled with **Cargo**. If you haven't already installed Rust, please go
|
||||
ahead and [install it](https://www.rust-lang.org/downloads.html) now.
|
||||
ahead and [install it](https://www.rust-lang.org/tools/install) now.
|
||||
|
||||
### Install Crates.io version
|
||||
|
||||
|
||||
@@ -29,8 +29,9 @@ your default web browser after building it.
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
-------------------
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ mdbook clean path/to/book
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to override the book's output
|
||||
directory, which will be deleted by this command. If not specified it will
|
||||
default to the value of the `build.build-dir` key in `book.toml`, or to `./book`
|
||||
relative to the book's root directory.
|
||||
directory, which will be deleted by this command. Relative paths are interpreted
|
||||
relative to the book's root directory. If not specified it will default to the
|
||||
value of the `build.build-dir` key in `book.toml`, or to `./book`.
|
||||
|
||||
```bash
|
||||
mdbook clean --dest-dir=path/to/book
|
||||
|
||||
@@ -5,6 +5,9 @@ The serve command is used to preview a book by serving it over HTTP at
|
||||
changes, rebuilding the book and refreshing clients for each change. A websocket
|
||||
connection is used to trigger the client-side refresh.
|
||||
|
||||
***Note:*** *The `serve` command is for testing a book's HTML output, and is not
|
||||
intended to be a complete HTTP server for a website.*
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
The `serve` command can take a directory as an argument to use as the book's
|
||||
@@ -34,16 +37,12 @@ configured.
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the book in your your
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the book in your
|
||||
default web browser after starting the server.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
|
||||
-----
|
||||
|
||||
***Note:*** *The `serve` command is for testing, and is not intended to be a
|
||||
complete HTTP server for a website.*
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
@@ -48,5 +48,6 @@ comma-delimited list (`-L foo,bar`).
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
@@ -22,5 +22,6 @@ your default web browser.
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
@@ -22,11 +22,11 @@ rust:
|
||||
|
||||
before_script:
|
||||
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
|
||||
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.1" mdbook)
|
||||
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.3" mdbook)
|
||||
- cargo install-update -a
|
||||
|
||||
script:
|
||||
- cd path/to/mybook && mdbook build && mdbook test
|
||||
- mdbook build path/to/mybook && mdbook test path/to/mybook
|
||||
```
|
||||
|
||||
## Deploying Your Book to GitHub Pages
|
||||
@@ -54,3 +54,36 @@ deploy:
|
||||
```
|
||||
|
||||
That's it!
|
||||
|
||||
### Deploying to GitHub Pages manually
|
||||
|
||||
If your CI doesn't support GitHub pages, or you're deploying somewhere else
|
||||
with integrations such as Github Pages:
|
||||
*note: you may want to use different tmp dirs*:
|
||||
|
||||
```console
|
||||
$> git worktree add /tmp/book gh-pages
|
||||
$> mdbook build
|
||||
$> rm -rf /tmp/book/* # this won't delete the .git directory
|
||||
$> cp -rp book/* /tmp/book/
|
||||
$> cd /tmp/book
|
||||
$> git add -A
|
||||
$> git commit 'new book message'
|
||||
$> git push origin gh-pages
|
||||
$> cd -
|
||||
```
|
||||
|
||||
Or put this into a Makefile rule:
|
||||
|
||||
```makefile
|
||||
.PHONY: deploy
|
||||
deploy: book
|
||||
@echo "====> deploying to github"
|
||||
git worktree add /tmp/book gh-pages
|
||||
rm -rf /tmp/book/*
|
||||
cp -rp book/* /tmp/book/
|
||||
cd /tmp/book && \
|
||||
git add -A && \
|
||||
git commit -m "deployed on $(shell date) by ${USER}" && \
|
||||
git push origin gh-pages
|
||||
```
|
||||
|
||||
@@ -12,14 +12,14 @@ The *For Developers* chapters are here to show you the more advanced usage of
|
||||
The two main ways a developer can hook into the book's build process is via,
|
||||
|
||||
- [Preprocessors](preprocessors.md)
|
||||
- [Alternate Backends](backends.md)
|
||||
- [Alternative Backends](backends.md)
|
||||
|
||||
|
||||
## The Build Process
|
||||
|
||||
The process of rendering a book project goes through several steps.
|
||||
|
||||
1. Load the book
|
||||
1. Load the book
|
||||
- Parse the `book.toml`, falling back to the default `Config` if it doesn't
|
||||
exist
|
||||
- Load the book chapters into memory
|
||||
@@ -41,6 +41,6 @@ The easiest way to find out how to use the `mdbook` crate is by looking at the
|
||||
explanation on the configuration system.
|
||||
|
||||
|
||||
[`MDBook`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.MDBook.html
|
||||
[API Docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
|
||||
[config]: file:///home/michael/Documents/forks/mdBook/target/doc/mdbook/config/index.html
|
||||
[`MDBook`]: https://docs.rs/mdbook/*/mdbook/book/struct.MDBook.html
|
||||
[API Docs]: https://docs.rs/mdbook/*/mdbook/
|
||||
[config]: https://docs.rs/mdbook/*/mdbook/config/index.html
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Alternate Backends
|
||||
# Alternative Backends
|
||||
|
||||
A "backend" is simply a program which `mdbook` will invoke during the book
|
||||
rendering process. This program is passed a JSON representation of the book and
|
||||
configuration information via `stdin`. Once the backend receives this
|
||||
information it is free to do whatever it wants.
|
||||
|
||||
There are already several alternate backends on GitHub which can be used as a
|
||||
There are already several alternative backends on GitHub which can be used as a
|
||||
rough example of how this is accomplished in practice.
|
||||
|
||||
- [mdbook-linkcheck] - a simple program for verifying the book doesn't contain
|
||||
@@ -14,7 +14,7 @@ rough example of how this is accomplished in practice.
|
||||
- [mdbook-test] - a program to run the book's contents through [rust-skeptic] to
|
||||
verify everything compiles and runs correctly (similar to `rustdoc --test`)
|
||||
|
||||
This page will step you through creating your own alternate 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
|
||||
no reason why it couldn't be accomplished using something like Python or Ruby.
|
||||
|
||||
@@ -24,9 +24,9 @@ no reason why it couldn't be accomplished using something like Python or Ruby.
|
||||
First you'll want to create a new binary program and add `mdbook` as a
|
||||
dependency.
|
||||
|
||||
```
|
||||
```shell
|
||||
$ cargo new --bin mdbook-wordcount
|
||||
$ cd mdbook-wordcount
|
||||
$ cd mdbook-wordcount
|
||||
$ cargo add mdbook
|
||||
```
|
||||
|
||||
@@ -52,8 +52,8 @@ fn main() {
|
||||
> **Note:** The `RenderContext` contains a `version` field. This lets backends
|
||||
figure out whether they are compatible with the version of `mdbook` it's being
|
||||
called by. This `version` comes directly from the corresponding field in
|
||||
`mdbook`'s `Cargo.toml`.
|
||||
|
||||
`mdbook`'s `Cargo.toml`.
|
||||
|
||||
It is recommended that backends use the [`semver`] crate to inspect this field
|
||||
and emit a warning if there may be a compatibility issue.
|
||||
|
||||
@@ -92,12 +92,12 @@ fn count_words(ch: &Chapter) -> usize {
|
||||
Now we've got the basics running, we want to actually use it. First, install the
|
||||
program.
|
||||
|
||||
```
|
||||
```shell
|
||||
$ cargo install
|
||||
```
|
||||
|
||||
Then `cd` to the particular book you'd like to count the words of and update its
|
||||
`book.toml` file.
|
||||
`book.toml` file.
|
||||
|
||||
```diff
|
||||
[book]
|
||||
@@ -112,7 +112,7 @@ Then `cd` to the particular book you'd like to count the words of and update its
|
||||
|
||||
When it loads a book into memory, `mdbook` will inspect your `book.toml` file to
|
||||
try and figure out which backends to use by looking for all `output.*` tables.
|
||||
If none are provided it'll fall back to using the default HTML renderer.
|
||||
If none are provided it'll fall back to using the default HTML renderer.
|
||||
|
||||
Notably, this means if you want to add your own custom backend you'll also need
|
||||
to make sure to add the HTML backend, even if its table just stays empty.
|
||||
@@ -120,7 +120,7 @@ to make sure to add the HTML backend, even if its table just stays empty.
|
||||
Now you just need to build your book like normal, and everything should *Just
|
||||
Work*.
|
||||
|
||||
```
|
||||
```shell
|
||||
$ mdbook build
|
||||
...
|
||||
2018-01-16 07:31:15 [INFO] (mdbook::renderer): Invoking the "mdbook-wordcount" renderer
|
||||
@@ -140,7 +140,7 @@ Syntax highlighting: 314
|
||||
MathJax Support: 153
|
||||
Rust code specific features: 148
|
||||
For Developers: 788
|
||||
Alternate Backends: 710
|
||||
Alternative Backends: 710
|
||||
Contributors: 85
|
||||
```
|
||||
|
||||
@@ -169,7 +169,7 @@ arguments or be an interpreted script), you can use the `command` field.
|
||||
Now imagine you don't want to count the number of words on a particular chapter
|
||||
(it might be generated text/code, etc). The canonical way to do this is via the
|
||||
usual `book.toml` configuration file by adding items to your `[output.foo]`
|
||||
table.
|
||||
table.
|
||||
|
||||
The `Config` can be treated roughly as a nested hashmap which lets you call
|
||||
methods like `get()` to access the config's contents, with a
|
||||
@@ -211,13 +211,13 @@ and then add a check to make sure we skip ignored chapters.
|
||||
+ let cfg: WordcountConfig = ctx.config
|
||||
+ .get_deserialized("output.wordcount")
|
||||
+ .unwrap_or_default();
|
||||
|
||||
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
+ if cfg.ignores.contains(&ch.name) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
}
|
||||
@@ -239,17 +239,17 @@ in [`RenderContext`].
|
||||
- use std::io;
|
||||
use mdbook::renderer::RenderContext;
|
||||
use mdbook::book::{BookItem, Chapter};
|
||||
|
||||
|
||||
fn main() {
|
||||
...
|
||||
|
||||
|
||||
+ let _ = fs::create_dir_all(&ctx.destination);
|
||||
+ let mut f = File::create(ctx.destination.join("wordcounts.txt")).unwrap();
|
||||
+
|
||||
+
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
...
|
||||
|
||||
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
+ writeln!(f, "{}: {}", ch.name, num_words).unwrap();
|
||||
@@ -276,11 +276,11 @@ like this:
|
||||
|
||||
fn main() {
|
||||
...
|
||||
|
||||
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
...
|
||||
|
||||
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
writeln!(f, "{}: {}", ch.name, num_words).unwrap();
|
||||
@@ -303,7 +303,7 @@ like this:
|
||||
|
||||
Now, if we reinstall the backend and build a book,
|
||||
|
||||
```
|
||||
```shell
|
||||
$ cargo install --force
|
||||
$ mdbook build /path/to/book
|
||||
...
|
||||
@@ -329,7 +329,7 @@ the usual `RUST_LOG` to control logging verbosity.
|
||||
## Wrapping Up
|
||||
|
||||
Although contrived, hopefully this example was enough to show how you'd create
|
||||
an alternate backend for `mdbook`. If you feel it's missing something, don't
|
||||
an alternative backend for `mdbook`. If you feel it's missing something, don't
|
||||
hesitate to create an issue in the [issue tracker] so we can improve the user
|
||||
guide.
|
||||
|
||||
@@ -342,10 +342,10 @@ the source code or ask questions.
|
||||
[mdbook-epub]: https://github.com/Michael-F-Bryan/mdbook-epub
|
||||
[mdbook-test]: https://github.com/Michael-F-Bryan/mdbook-test
|
||||
[rust-skeptic]: https://github.com/budziq/rust-skeptic
|
||||
[`RenderContext`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html
|
||||
[`RenderContext::from_json()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html#method.from_json
|
||||
[`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
|
||||
[`semver`]: https://crates.io/crates/semver
|
||||
[`Book`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html
|
||||
[`Book::iter()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html#method.iter
|
||||
[`Config`]: http://rust-lang-nursery.github.io/mdBook/mdbook/config/struct.Config.html
|
||||
[`Book`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html
|
||||
[`Book::iter()`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html#method.iter
|
||||
[`Config`]: https://docs.rs/mdbook/*/mdbook/config/struct.Config.html
|
||||
[issue tracker]: https://github.com/rust-lang-nursery/mdBook/issues
|
||||
|
||||
@@ -11,68 +11,71 @@ the book. Possible use cases are:
|
||||
mathjax equivalents
|
||||
|
||||
|
||||
## Implementing a Preprocessor
|
||||
## Hooking Into MDBook
|
||||
|
||||
A preprocessor is represented by the `Preprocessor` trait.
|
||||
MDBook uses a fairly simple mechanism for discovering third party plugins.
|
||||
A new table is added to `book.toml` (e.g. `preprocessor.foo` for the `foo`
|
||||
preprocessor) and then `mdbook` will try to invoke the `mdbook-foo` program as
|
||||
part of the build process.
|
||||
|
||||
```rust
|
||||
pub trait Preprocessor {
|
||||
fn name(&self) -> &str;
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>;
|
||||
fn supports_renderer(&self, _renderer: &str) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
While preprocessors can be hard-coded to specify which backend it should be run
|
||||
for (e.g. it doesn't make sense for MathJax to be used for non-HTML renderers)
|
||||
with the `preprocessor.foo.renderer` key.
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "My Book"
|
||||
authors = ["Michael-F-Bryan"]
|
||||
|
||||
[preprocessor.foo]
|
||||
# The command can also be specified manually
|
||||
command = "python3 /path/to/foo.py"
|
||||
# Only run the `foo` preprocessor for the HTML and EPUB renderer
|
||||
renderer = ["html", "epub"]
|
||||
```
|
||||
|
||||
Where the `PreprocessorContext` is defined as
|
||||
In typical unix style, all inputs to the plugin will be written to `stdin` as
|
||||
JSON and `mdbook` will read from `stdout` if it is expecting output.
|
||||
|
||||
The easiest way to get started is by creating your own implementation of the
|
||||
`Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
|
||||
translates inputs to the correct `Preprocessor` method. For convenience, there
|
||||
is [an example no-op preprocessor] in the `examples/` directory which can easily
|
||||
be adapted for other preprocessors.
|
||||
|
||||
<details>
|
||||
<summary>Example no-op preprocessor</summary>
|
||||
|
||||
```rust
|
||||
pub struct PreprocessorContext {
|
||||
pub root: PathBuf,
|
||||
pub config: Config,
|
||||
/// The `Renderer` this preprocessor is being used with.
|
||||
pub renderer: String,
|
||||
}
|
||||
// nop-preprocessors.rs
|
||||
|
||||
{{#include ../../../examples/nop-preprocessor.rs}}
|
||||
```
|
||||
</details>
|
||||
|
||||
The `renderer` value allows you react accordingly, for example, PDF or HTML.
|
||||
## Hints For Implementing A Preprocessor
|
||||
|
||||
## A complete Example
|
||||
By pulling in `mdbook` as a library, preprocessors can have access to the
|
||||
existing infrastructure for dealing with books.
|
||||
|
||||
The magic happens within the `run(...)` method of the
|
||||
[`Preprocessor`][preprocessor-docs] trait implementation.
|
||||
For example, a custom preprocessor could use the
|
||||
[`CmdPreprocessor::parse_input()`] function to deserialize the JSON written to
|
||||
`stdin`. Then each chapter of the `Book` can be mutated in-place via
|
||||
[`Book::for_each_mut()`], and then written to `stdout` with the `serde_json`
|
||||
crate.
|
||||
|
||||
As direct access to the chapters is not possible, you will probably end up
|
||||
iterating them using `for_each_mut(...)`:
|
||||
Chapters can be accessed either directly (by recursively iterating over
|
||||
chapters) or via the `Book::for_each_mut()` convenience method.
|
||||
|
||||
```rust
|
||||
book.for_each_mut(|item: &mut BookItem| {
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
|
||||
res = Some(
|
||||
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
|
||||
Ok(md) => {
|
||||
chapter.content = md;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
The `chapter.content` is just a string which happens to be markdown. While it's
|
||||
entirely possible to use regular expressions or do a manual find & replace,
|
||||
you'll probably want to process the input into something more computer-friendly.
|
||||
The [`pulldown-cmark`][pc] crate implements a production-quality event-based
|
||||
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] allowing you to
|
||||
translate events back into markdown text.
|
||||
|
||||
The `chapter.content` is just a markdown formatted string, and you will have to
|
||||
process it in some way. Even though it's entirely possible to implement some
|
||||
sort of manual find & replace operation, if that feels too unsafe you can use
|
||||
[`pulldown-cmark`][pc] to parse the string into events and work on them instead.
|
||||
|
||||
Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events
|
||||
back to a string.
|
||||
|
||||
The following code block shows how to remove all emphasis from markdown, and do
|
||||
so safely.
|
||||
The following code block shows how to remove all emphasis from markdown,
|
||||
without accidentally breaking the document.
|
||||
|
||||
```rust
|
||||
fn remove_emphasis(
|
||||
@@ -106,4 +109,7 @@ For everything else, have a look [at the complete example][example].
|
||||
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
|
||||
[pc]: https://crates.io/crates/pulldown-cmark
|
||||
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
|
||||
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs
|
||||
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||
[an example no-op preprocessor]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||
[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input
|
||||
[`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut
|
||||
|
||||
@@ -14,9 +14,9 @@ description = "The example book covers examples."
|
||||
build-dir = "my-example-book"
|
||||
create-missing = false
|
||||
|
||||
[preprocess.index]
|
||||
[preprocessor.index]
|
||||
|
||||
[preprocess.links]
|
||||
[preprocessor.links]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["custom.css"]
|
||||
@@ -42,6 +42,7 @@ This is general information about your book.
|
||||
- **src:** By default, the source directory is found in the directory named
|
||||
`src` directly under the root folder. But this is configurable with the `src`
|
||||
key in the configuration file.
|
||||
- **language:** The main language of the book, which is used as a language attribute `<html lang="en">` for example.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
@@ -50,6 +51,7 @@ title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
|
||||
language = "en"
|
||||
```
|
||||
|
||||
### Build options
|
||||
@@ -68,19 +70,19 @@ This controls the build process of your book.
|
||||
If you have the same, and/or other preprocessors declared via their table
|
||||
of configuration, they will run instead.
|
||||
|
||||
- For clarity, with no preprocessor configuration, the default `links` and
|
||||
- For clarity, with no preprocessor configuration, the default `links` and
|
||||
`index` will run.
|
||||
- Setting `use-default-preprocessors = false` will disable these
|
||||
default preprocessors from running.
|
||||
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
|
||||
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
|
||||
`use-default-preprocessors` that `links` it will run.
|
||||
|
||||
## Configuring Preprocessors
|
||||
|
||||
The following preprocessors are available and included by default:
|
||||
|
||||
- `links`: Expand the `{{ #playpen }}` and `{{ #include }}` handlebars helpers in
|
||||
a chapter to include the contents of a file.
|
||||
- `links`: Expand the `{{ #playpen }}` and `{{ #include }}` handlebars
|
||||
helpers in a chapter to include the contents of a file.
|
||||
- `index`: Convert all chapter files named `README.md` into `index.md`. That is
|
||||
to say, all `README.md` would be rendered to an index file `index.html` in the
|
||||
rendered book.
|
||||
@@ -92,20 +94,21 @@ The following preprocessors are available and included by default:
|
||||
build-dir = "build"
|
||||
create-missing = false
|
||||
|
||||
[preprocess.links]
|
||||
[preprocessor.links]
|
||||
|
||||
[preprocess.index]
|
||||
[preprocessor.index]
|
||||
```
|
||||
|
||||
### Custom Preprocessor Configuration
|
||||
|
||||
Like renderers, preprocessor will need to be given its own table (e.g. `[preprocessor.mathjax]`).
|
||||
In the section, you may then pass extra configuration to the preprocessor by adding key-value pairs to the table.
|
||||
Like renderers, preprocessor will need to be given its own table (e.g.
|
||||
`[preprocessor.mathjax]`). In the section, you may then pass extra
|
||||
configuration to the preprocessor by adding key-value pairs to the table.
|
||||
|
||||
For example
|
||||
|
||||
```
|
||||
[preprocess.links]
|
||||
```toml
|
||||
[preprocessor.links]
|
||||
# set the renderers this preprocessor will run for
|
||||
renderers = ["html"]
|
||||
some_extra_feature = true
|
||||
@@ -113,13 +116,26 @@ some_extra_feature = true
|
||||
|
||||
#### Locking a Preprocessor dependency to a renderer
|
||||
|
||||
You can explicitly specify that a preprocessor should run for a renderer by binding the two together.
|
||||
You can explicitly specify that a preprocessor should run for a renderer by
|
||||
binding the two together.
|
||||
|
||||
```
|
||||
```toml
|
||||
[preprocessor.mathjax]
|
||||
renderers = ["html"] # mathjax only makes sense with the HTML renderer
|
||||
```
|
||||
|
||||
### Provide Your Own Command
|
||||
|
||||
By default when you add a `[preprocessor.foo]` table to your `book.toml` file,
|
||||
`mdbook` will try to invoke the `mdbook-foo` executable. If you want to use a
|
||||
different program name or pass in command-line arguments, this behaviour can
|
||||
be overridden by adding a `command` field.
|
||||
|
||||
```toml
|
||||
[preprocessor.random]
|
||||
command = "python random.py"
|
||||
```
|
||||
|
||||
## Configuring Renderers
|
||||
|
||||
### HTML renderer options
|
||||
@@ -132,8 +148,12 @@ The following configuration options are available:
|
||||
- **theme:** mdBook comes with a default theme and all the resource files needed
|
||||
for it. But if this option is set, mdBook will selectively overwrite the theme
|
||||
files with the ones found in the specified folder.
|
||||
- **default-theme:** The theme color scheme to select by default in the
|
||||
'Change Theme' dropdown. Defaults to `light`.
|
||||
- **curly-quotes:** Convert straight quotes to curly quotes, except for those
|
||||
that occur in code blocks and code spans. Defaults to `false`.
|
||||
- **mathjax-support:** Adds support for [MathJax](mathjax.md). Defaults to
|
||||
`false`.
|
||||
- **google-analytics:** If you use Google Analytics, this option lets you enable
|
||||
it by simply specifying your ID in the configuration file.
|
||||
- **additional-css:** If you need to slightly change the appearance of your book
|
||||
@@ -149,6 +169,10 @@ The following configuration options are available:
|
||||
- **playpen:** A subtable for configuring various playpen settings.
|
||||
- **search:** A subtable for configuring the in-browser search functionality.
|
||||
mdBook must be compiled with the `search` feature enabled (on by default).
|
||||
- **git-repository-url:** A url to the git repository for the book. If provided
|
||||
an icon link will be output in the menu bar of the book.
|
||||
- **git-repository-icon:** The FontAwesome icon class to use for the git
|
||||
repository link. Defaults to `fa-github`.
|
||||
|
||||
Available configuration options for the `[output.html.playpen]` table:
|
||||
|
||||
@@ -181,32 +205,32 @@ Available configuration options for the `[output.html.search]` table:
|
||||
- **copy-js:** Copy JavaScript files for the search implementation to the output
|
||||
directory. Defaults to `true`.
|
||||
|
||||
This shows all available options in the **book.toml**:
|
||||
This shows all available HTML output options in the **book.toml**:
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
[build]
|
||||
build-dir = "book"
|
||||
create-missing = true
|
||||
preprocess = ["links", "index"]
|
||||
|
||||
[output.html]
|
||||
theme = "my-theme"
|
||||
default-theme = "light"
|
||||
curly-quotes = true
|
||||
mathjax-support = false
|
||||
google-analytics = "123456"
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
no-section-label = false
|
||||
git-repository-url = "https://github.com/rust-lang-nursery/mdBook"
|
||||
git-repository-icon = "fa-github"
|
||||
|
||||
[output.html.playpen]
|
||||
editor = "./path/to/editor"
|
||||
editable = false
|
||||
copy-js = true
|
||||
|
||||
[output.html.search]
|
||||
enable = true
|
||||
searcher = "./path/to/searcher"
|
||||
limit-results = 30
|
||||
teaser-word-count = 30
|
||||
use-boolean-and = true
|
||||
@@ -218,6 +242,16 @@ heading-split-level = 3
|
||||
copy-js = true
|
||||
```
|
||||
|
||||
### Custom Renderers
|
||||
|
||||
A custom renderer can be enabled by adding a `[output.foo]` table to your
|
||||
`book.toml`. Similar to [preprocessors](#configuring-preprocessors) this will
|
||||
instruct `mdbook` to pass a representation of the book to `mdbook-foo` for
|
||||
rendering.
|
||||
|
||||
Custom renderers will have access to all configuration within their table
|
||||
(i.e. anything under `[output.foo]`), and the command to be invoked can be
|
||||
manually specified with the `command` field.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
@@ -249,11 +283,11 @@ book's title without needing to touch your `book.toml`.
|
||||
> This means, if you so desired, you could override all book metadata when
|
||||
> building the book with something like
|
||||
>
|
||||
> ```text
|
||||
> ```shell
|
||||
> $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
|
||||
> $ mdbook build
|
||||
> ```
|
||||
|
||||
The latter case may be useful in situations where `mdbook` is invoked from a
|
||||
script or CI, where it sometimes isn't possible to update the `book.toml` before
|
||||
building.
|
||||
building.
|
||||
|
||||
@@ -35,10 +35,20 @@ With the following syntax, you can include files into your book:
|
||||
|
||||
The path to the file has to be relative from the current source file.
|
||||
|
||||
Usually, this command is used for including code snippets and examples. In this
|
||||
case, oftens one would include a specific part of the file e.g. which only
|
||||
contains the relevant lines for the example. We support four different modes of
|
||||
partial includes:
|
||||
mdBook will interpret included files as markdown. Since the include command
|
||||
is usually used for inserting code snippets and examples, you will often
|
||||
wrap the command with ```` ``` ```` to display the file contents without
|
||||
interpretting them.
|
||||
|
||||
````hbs
|
||||
```
|
||||
\{{#include file.rs}}
|
||||
```
|
||||
````
|
||||
|
||||
## Including portions of a file
|
||||
Often you only need a specific part of the file e.g. relevant lines for an
|
||||
example. We support four different modes of partial includes:
|
||||
|
||||
```hbs
|
||||
\{{#include file.rs:2}}
|
||||
@@ -53,6 +63,50 @@ the file are omitted. The third command includes all lines from line 2, i.e. the
|
||||
first line is omitted. The last command includes the excerpt of `file.rs`
|
||||
consisting of lines 2 to 10.
|
||||
|
||||
To avoid breaking your book when modifying included files, you can also
|
||||
include a specific section using anchors instead of line numbers.
|
||||
An anchor is a pair of matching lines. The line beginning an anchor must
|
||||
match the regex "ANCHOR:\s*[\w_-]+" and similarly the ending line must match
|
||||
the regex "ANCHOR_END:\s*[\w_-]+". This allows you to put anchors in
|
||||
any kind of commented line.
|
||||
|
||||
Consider the following file to include:
|
||||
```rs
|
||||
/* ANCHOR: all */
|
||||
|
||||
// ANCHOR: component
|
||||
struct Paddle {
|
||||
hello: f32,
|
||||
}
|
||||
// ANCHOR_END: component
|
||||
|
||||
////////// ANCHOR: system
|
||||
impl System for MySystem { ... }
|
||||
////////// ANCHOR_END: system
|
||||
|
||||
/* ANCHOR_END: all */
|
||||
```
|
||||
|
||||
Then in the book, all you have to do is:
|
||||
````hbs
|
||||
Here is a component:
|
||||
```rust,no_run,noplaypen
|
||||
\{{#include file.rs:component}}
|
||||
```
|
||||
|
||||
Here is a system:
|
||||
```rust,no_run,noplaypen
|
||||
\{{#include file.rs:system}}
|
||||
```
|
||||
|
||||
This is the full file.
|
||||
```rust,no_run,noplaypen
|
||||
\{{#include file.rs:all}}
|
||||
```
|
||||
````
|
||||
|
||||
Lines containing anchor patterns inside the included anchor are ignored.
|
||||
|
||||
## Inserting runnable Rust files
|
||||
|
||||
With the following syntax, you can insert runnable Rust files into your book:
|
||||
|
||||
@@ -17,9 +17,8 @@ handlebars template you can access this information by using
|
||||
|
||||
Here is a list of the properties that are exposed:
|
||||
|
||||
- ***language*** Language of the book in the form `en`. To use in <code
|
||||
class="language-html">\<html lang="{{ language }}"></code> for example. At the
|
||||
moment it is hardcoded.
|
||||
- ***language*** Language of the book in the form `en`, as specified in `book.toml` (if not specified, defaults to `en`). To use in <code
|
||||
class="language-html">\<html lang="{{ language }}"></code> for example.
|
||||
- ***title*** Title of the book, as specified in `book.toml`
|
||||
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
||||
|
||||
@@ -45,51 +44,55 @@ at your disposal.
|
||||
|
||||
### 1. toc
|
||||
|
||||
The toc helper is used like this
|
||||
The toc helper is used like this
|
||||
|
||||
```handlebars
|
||||
{{#toc}}{{/toc}}
|
||||
```
|
||||
```handlebars
|
||||
{{#toc}}{{/toc}}
|
||||
```
|
||||
|
||||
and outputs something that looks like this, depending on the structure of your book
|
||||
and outputs something that looks like this, depending on the structure of your
|
||||
book
|
||||
|
||||
```html
|
||||
<ul class="chapter">
|
||||
<li><a href="link/to/file.html">Some chapter</a></li>
|
||||
<li>
|
||||
<ul class="section">
|
||||
<li><a href="link/to/other_file.html">Some other Chapter</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
```html
|
||||
<ul class="chapter">
|
||||
<li><a href="link/to/file.html">Some chapter</a></li>
|
||||
<li>
|
||||
<ul class="section">
|
||||
<li><a href="link/to/other_file.html">Some other Chapter</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
If you would like to make a toc with another structure, you have access to the chapters property containing all the data.
|
||||
The only limitation at the moment is that you would have to do it with JavaScript instead of with a handlebars helper.
|
||||
If you would like to make a toc with another structure, you have access to the
|
||||
chapters property containing all the data. The only limitation at the moment
|
||||
is that you would have to do it with JavaScript instead of with a handlebars
|
||||
helper.
|
||||
|
||||
```html
|
||||
<script>
|
||||
var chapters = {{chapters}};
|
||||
// Processing here
|
||||
</script>
|
||||
```
|
||||
```html
|
||||
<script>
|
||||
var chapters = {{chapters}};
|
||||
// Processing here
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. previous / next
|
||||
|
||||
The previous and next helpers expose a `link` and `name` property to the previous and next chapters.
|
||||
The previous and next helpers expose a `link` and `name` property to the
|
||||
previous and next chapters.
|
||||
|
||||
They are used like this
|
||||
They are used like this
|
||||
|
||||
```handlebars
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="nav-chapters previous">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
```
|
||||
```handlebars
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="nav-chapters previous">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
```
|
||||
|
||||
The inner html will only be rendered if the previous / next chapter exists.
|
||||
Of course the inner html can be changed to your liking.
|
||||
The inner html will only be rendered if the previous / next chapter exists.
|
||||
Of course the inner html can be changed to your liking.
|
||||
|
||||
------
|
||||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
//! This program removes all forms of emphasis from the markdown of the book.
|
||||
extern crate mdbook;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate pulldown_cmark_to_cmark;
|
||||
|
||||
use mdbook::book::{Book, BookItem, Chapter};
|
||||
use mdbook::errors::{Error, Result};
|
||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
||||
use mdbook::MDBook;
|
||||
use pulldown_cmark::{Event, Parser, Tag};
|
||||
use pulldown_cmark_to_cmark::fmt::cmark;
|
||||
|
||||
use std::env::{args, args_os};
|
||||
use std::ffi::OsString;
|
||||
use std::process;
|
||||
|
||||
const NAME: &str = "md-links-to-html-links";
|
||||
|
||||
fn do_it(book: OsString) -> Result<()> {
|
||||
let mut book = MDBook::load(book)?;
|
||||
book.with_preprecessor(Deemphasize);
|
||||
book.build()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if args_os().count() != 2 {
|
||||
eprintln!("USAGE: {} <book>", args().next().expect("executable"));
|
||||
return;
|
||||
}
|
||||
if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
struct Deemphasize;
|
||||
|
||||
impl Preprocessor for Deemphasize {
|
||||
fn name(&self) -> &str {
|
||||
NAME
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
||||
eprintln!("Running '{}' preprocessor", self.name());
|
||||
let mut num_removed_items = 0;
|
||||
|
||||
process(&mut book.sections, &mut num_removed_items)?;
|
||||
|
||||
eprintln!(
|
||||
"{}: removed {} events from markdown stream.",
|
||||
self.name(),
|
||||
num_removed_items
|
||||
);
|
||||
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
fn process<'a, I>(items: I, num_removed_items: &mut usize) -> Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = &'a mut BookItem> + 'a,
|
||||
{
|
||||
for item in items {
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
eprintln!("{}: processing chapter '{}'", NAME, chapter.name);
|
||||
|
||||
let md = remove_emphasis(num_removed_items, chapter)?;
|
||||
chapter.content = md;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_emphasis(
|
||||
num_removed_items: &mut usize,
|
||||
chapter: &mut Chapter,
|
||||
) -> Result<String> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
|
||||
let events = Parser::new(&chapter.content).filter(|e| {
|
||||
let should_keep = match *e {
|
||||
Event::Start(Tag::Emphasis)
|
||||
| Event::Start(Tag::Strong)
|
||||
| Event::End(Tag::Emphasis)
|
||||
| Event::End(Tag::Strong) => false,
|
||||
_ => true,
|
||||
};
|
||||
if !should_keep {
|
||||
*num_removed_items += 1;
|
||||
}
|
||||
should_keep
|
||||
});
|
||||
|
||||
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
|
||||
Error::from(format!("Markdown serialization failed: {}", err))
|
||||
})
|
||||
}
|
||||
102
examples/nop-preprocessor.rs
Normal file
102
examples/nop-preprocessor.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use crate::nop_lib::Nop;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use mdbook::book::Book;
|
||||
use mdbook::errors::Error;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use std::io;
|
||||
use std::process;
|
||||
|
||||
pub fn make_app() -> App<'static, 'static> {
|
||||
App::new("nop-preprocessor")
|
||||
.about("A mdbook preprocessor which does precisely nothing")
|
||||
.subcommand(
|
||||
SubCommand::with_name("supports")
|
||||
.arg(Arg::with_name("renderer").required(true))
|
||||
.about("Check whether a renderer is supported by this preprocessor"),
|
||||
)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let matches = make_app().get_matches();
|
||||
|
||||
// Users will want to construct their own preprocessor here
|
||||
let preprocessor = Nop::new();
|
||||
|
||||
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
||||
handle_supports(&preprocessor, sub_args);
|
||||
} else if let Err(e) = handle_preprocessing(&preprocessor) {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
|
||||
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
|
||||
|
||||
if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
|
||||
// We should probably use the `semver` crate to check compatibility
|
||||
// here...
|
||||
eprintln!(
|
||||
"Warning: The {} plugin was built against version {} of mdbook, \
|
||||
but we're being called from version {}",
|
||||
pre.name(),
|
||||
mdbook::MDBOOK_VERSION,
|
||||
ctx.mdbook_version
|
||||
);
|
||||
}
|
||||
|
||||
let processed_book = pre.run(&ctx, book)?;
|
||||
serde_json::to_writer(io::stdout(), &processed_book)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
|
||||
let renderer = sub_args.value_of("renderer").expect("Required argument");
|
||||
let supported = pre.supports_renderer(&renderer);
|
||||
|
||||
// Signal whether the renderer is supported by exiting with 1 or 0.
|
||||
if supported {
|
||||
process::exit(0);
|
||||
} else {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// The actual implementation of the `Nop` preprocessor. This would usually go
|
||||
/// in your main `lib.rs` file.
|
||||
mod nop_lib {
|
||||
use super::*;
|
||||
|
||||
/// A no-op preprocessor.
|
||||
pub struct Nop;
|
||||
|
||||
impl Nop {
|
||||
pub fn new() -> Nop {
|
||||
Nop
|
||||
}
|
||||
}
|
||||
|
||||
impl Preprocessor for Nop {
|
||||
fn name(&self) -> &str {
|
||||
"nop-preprocessor"
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book, Error> {
|
||||
// In testing we want to tell the preprocessor to blow up by setting a
|
||||
// particular config value
|
||||
if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
|
||||
if nop_cfg.contains_key("blow-up") {
|
||||
return Err("Boom!!1!".into());
|
||||
}
|
||||
}
|
||||
|
||||
// we *are* a no-op preprocessor after all
|
||||
Ok(book)
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
renderer != "not-supported"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
use config::BuildConfig;
|
||||
use errors::*;
|
||||
use crate::config::BuildConfig;
|
||||
use crate::errors::*;
|
||||
|
||||
/// Load a book into memory from its `src/` directory.
|
||||
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
|
||||
@@ -82,7 +82,7 @@ impl Book {
|
||||
}
|
||||
|
||||
/// Get a depth-first iterator over the items in the book.
|
||||
pub fn iter(&self) -> BookItems {
|
||||
pub fn iter(&self) -> BookItems<'_> {
|
||||
BookItems {
|
||||
items: self.sections.iter().collect(),
|
||||
}
|
||||
@@ -116,7 +116,7 @@ where
|
||||
I: IntoIterator<Item = &'a mut BookItem>,
|
||||
{
|
||||
for item in items {
|
||||
if let &mut BookItem::Chapter(ref mut ch) = item {
|
||||
if let BookItem::Chapter(ch) = item {
|
||||
for_each_mut(func, &mut ch.sub_items);
|
||||
}
|
||||
|
||||
@@ -167,9 +167,9 @@ impl Chapter {
|
||||
) -> Chapter {
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content: content,
|
||||
content,
|
||||
path: path.into(),
|
||||
parent_names: parent_names,
|
||||
parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,7 @@ impl Chapter {
|
||||
///
|
||||
/// You need to pass in the book's source directory because all the links in
|
||||
/// `SUMMARY.md` give the chapter locations relative to it.
|
||||
fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
|
||||
pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
|
||||
debug!("Loading the book from disk");
|
||||
let src_dir = src_dir.as_ref();
|
||||
|
||||
@@ -210,7 +210,7 @@ fn load_summary_item<P: AsRef<Path>>(
|
||||
match *item {
|
||||
SummaryItem::Separator => Ok(BookItem::Separator),
|
||||
SummaryItem::Link(ref link) => {
|
||||
load_chapter(link, src_dir, parent_names).map(|c| BookItem::Chapter(c))
|
||||
load_chapter(link, src_dir, parent_names).map(BookItem::Chapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,7 +286,7 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
}
|
||||
|
||||
impl Display for Chapter {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if let Some(ref section_number) = self.number {
|
||||
write!(f, "{} ", section_number)?;
|
||||
}
|
||||
@@ -301,7 +301,7 @@ mod tests {
|
||||
use std::io::Write;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
const DUMMY_SRC: &'static str = "
|
||||
const DUMMY_SRC: &str = "
|
||||
# Dummy Chapter
|
||||
|
||||
this is some dummy text.
|
||||
@@ -317,7 +317,7 @@ And here is some \
|
||||
let chapter_path = temp.path().join("chapter_1.md");
|
||||
File::create(&chapter_path)
|
||||
.unwrap()
|
||||
.write(DUMMY_SRC.as_bytes())
|
||||
.write_all(DUMMY_SRC.as_bytes())
|
||||
.unwrap();
|
||||
|
||||
let link = Link::new("Chapter 1", chapter_path);
|
||||
@@ -333,7 +333,7 @@ And here is some \
|
||||
|
||||
File::create(&second_path)
|
||||
.unwrap()
|
||||
.write_all("Hello World!".as_bytes())
|
||||
.write_all(b"Hello World!")
|
||||
.unwrap();
|
||||
|
||||
let mut second = Link::new("Nested Chapter 1", &second_path);
|
||||
@@ -481,7 +481,8 @@ And here is some \
|
||||
.filter_map(|i| match *i {
|
||||
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
|
||||
_ => None,
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
let should_be: Vec<_> = vec![
|
||||
String::from("Chapter 1"),
|
||||
String::from("Hello World"),
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use toml;
|
||||
|
||||
use super::MDBook;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
use theme;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use crate::theme;
|
||||
|
||||
/// A helper for setting up a new book and its directory structure.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -173,15 +172,19 @@ impl BookBuilder {
|
||||
let src_dir = self.root.join(&self.config.book.src);
|
||||
|
||||
let summary = src_dir.join("SUMMARY.md");
|
||||
let mut f = File::create(&summary).chain_err(|| "Unable to create SUMMARY.md")?;
|
||||
writeln!(f, "# Summary")?;
|
||||
writeln!(f, "")?;
|
||||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||
|
||||
let chapter_1 = src_dir.join("chapter_1.md");
|
||||
let mut f = File::create(&chapter_1).chain_err(|| "Unable to create chapter_1.md")?;
|
||||
writeln!(f, "# Chapter 1")?;
|
||||
if !summary.exists() {
|
||||
trace!("No summary found creating stub summary and chapter_1.md.");
|
||||
let mut f = File::create(&summary).chain_err(|| "Unable to create SUMMARY.md")?;
|
||||
writeln!(f, "# Summary")?;
|
||||
writeln!(f)?;
|
||||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||
|
||||
let chapter_1 = src_dir.join("chapter_1.md");
|
||||
let mut f = File::create(&chapter_1).chain_err(|| "Unable to create chapter_1.md")?;
|
||||
writeln!(f, "# Chapter 1")?;
|
||||
} else {
|
||||
trace!("Existing summary found, no need to create stub files.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
220
src/book/mod.rs
220
src/book/mod.rs
@@ -16,15 +16,18 @@ pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::string::ToString;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use toml::Value;
|
||||
|
||||
use errors::*;
|
||||
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
||||
use utils;
|
||||
use crate::errors::*;
|
||||
use crate::preprocess::{
|
||||
CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
|
||||
};
|
||||
use crate::renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
||||
use crate::utils;
|
||||
|
||||
use config::Config;
|
||||
use crate::config::Config;
|
||||
|
||||
/// The object used to manage and build a book.
|
||||
pub struct MDBook {
|
||||
@@ -34,10 +37,10 @@ pub struct MDBook {
|
||||
pub config: Config,
|
||||
/// A representation of the book's contents in memory.
|
||||
pub book: Book,
|
||||
renderers: Vec<Box<Renderer>>,
|
||||
renderers: Vec<Box<dyn Renderer>>,
|
||||
|
||||
/// List of pre-processors to be run on the book
|
||||
preprocessors: Vec<Box<Preprocessor>>,
|
||||
preprocessors: Vec<Box<dyn Preprocessor>>,
|
||||
}
|
||||
|
||||
impl MDBook {
|
||||
@@ -65,7 +68,7 @@ impl MDBook {
|
||||
|
||||
config.update_from_env();
|
||||
|
||||
if log_enabled!(::log::Level::Trace) {
|
||||
if log_enabled!(log::Level::Trace) {
|
||||
for line in format!("Config: {:#?}", config).lines() {
|
||||
trace!("{}", line);
|
||||
}
|
||||
@@ -93,12 +96,34 @@ impl MDBook {
|
||||
})
|
||||
}
|
||||
|
||||
/// Load a book from its root directory using a custom config and a custom summary.
|
||||
pub fn load_with_config_and_summary<P: Into<PathBuf>>(
|
||||
book_root: P,
|
||||
config: Config,
|
||||
summary: Summary,
|
||||
) -> Result<MDBook> {
|
||||
let root = book_root.into();
|
||||
|
||||
let src_dir = root.join(&config.book.src);
|
||||
let book = book::load_book_from_disk(&summary, &src_dir)?;
|
||||
|
||||
let renderers = determine_renderers(&config);
|
||||
let preprocessors = determine_preprocessors(&config)?;
|
||||
|
||||
Ok(MDBook {
|
||||
root,
|
||||
config,
|
||||
book,
|
||||
renderers,
|
||||
preprocessors,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a flat depth-first iterator over the elements of the book,
|
||||
/// it returns an [BookItem enum](bookitem.html):
|
||||
/// `(section: String, bookitem: &BookItem)`
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate mdbook;
|
||||
/// # use mdbook::MDBook;
|
||||
/// # use mdbook::book::BookItem;
|
||||
/// # #[allow(unused_variables)]
|
||||
@@ -120,7 +145,7 @@ impl MDBook {
|
||||
/// // etc.
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn iter(&self) -> BookItems {
|
||||
pub fn iter(&self) -> BookItems<'_> {
|
||||
self.book.iter()
|
||||
}
|
||||
|
||||
@@ -157,31 +182,14 @@ impl MDBook {
|
||||
}
|
||||
|
||||
/// Run the entire build process for a particular `Renderer`.
|
||||
fn execute_build_process(&self, renderer: &Renderer) -> Result<()> {
|
||||
fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
||||
let mut preprocessed_book = self.book.clone();
|
||||
let preprocess_ctx = PreprocessorContext::new(self.root.clone(),
|
||||
self.config.clone(),
|
||||
renderer.name().to_string());
|
||||
let preprocess_ctx = PreprocessorContext::new(
|
||||
self.root.clone(),
|
||||
self.config.clone(),
|
||||
renderer.name().to_string(),
|
||||
);
|
||||
|
||||
for preprocessor in &self.preprocessors {
|
||||
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
|
||||
debug!("Running the {} preprocessor.", preprocessor.name());
|
||||
preprocessed_book =
|
||||
preprocessor.run(&preprocess_ctx, preprocessed_book)?;
|
||||
}
|
||||
}
|
||||
|
||||
info!("Running the {} backend", renderer.name());
|
||||
self.render(&preprocessed_book, renderer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
preprocessed_book: &Book,
|
||||
renderer: &Renderer,
|
||||
) -> Result<()> {
|
||||
let name = renderer.name();
|
||||
let build_dir = self.build_dir_for(name);
|
||||
if build_dir.exists() {
|
||||
@@ -195,6 +203,23 @@ impl MDBook {
|
||||
.chain_err(|| "Unable to clear output directory")?;
|
||||
}
|
||||
|
||||
for preprocessor in &self.preprocessors {
|
||||
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
|
||||
debug!("Running the {} preprocessor.", preprocessor.name());
|
||||
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
|
||||
}
|
||||
}
|
||||
|
||||
info!("Running the {} backend", renderer.name());
|
||||
self.render(&preprocessed_book, renderer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, preprocessed_book: &Book, renderer: &dyn Renderer) -> Result<()> {
|
||||
let name = renderer.name();
|
||||
let build_dir = self.build_dir_for(name);
|
||||
|
||||
let render_context = RenderContext::new(
|
||||
self.root.clone(),
|
||||
preprocessed_book.clone(),
|
||||
@@ -216,7 +241,7 @@ impl MDBook {
|
||||
}
|
||||
|
||||
/// Register a [`Preprocessor`](../preprocess/trait.Preprocessor.html) to be used when rendering the book.
|
||||
pub fn with_preprecessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
||||
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
||||
self.preprocessors.push(Box::new(preprocessor));
|
||||
self
|
||||
}
|
||||
@@ -232,9 +257,8 @@ impl MDBook {
|
||||
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
||||
|
||||
// FIXME: Is "test" the proper renderer name to use here?
|
||||
let preprocess_context = PreprocessorContext::new(self.root.clone(),
|
||||
self.config.clone(),
|
||||
"test".to_string());
|
||||
let preprocess_context =
|
||||
PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
|
||||
|
||||
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
|
||||
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
||||
@@ -244,13 +268,12 @@ impl MDBook {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
if !ch.path.as_os_str().is_empty() {
|
||||
let path = self.source_dir().join(&ch.path);
|
||||
let content = utils::fs::file_to_string(&path)?;
|
||||
info!("Testing file: {:?}", path);
|
||||
|
||||
// write preprocessed file to tempdir
|
||||
let path = temp_dir.path().join(&ch.path);
|
||||
let mut tmpf = utils::fs::create_file(&path)?;
|
||||
tmpf.write_all(content.as_bytes())?;
|
||||
tmpf.write_all(ch.content.as_bytes())?;
|
||||
|
||||
let output = Command::new("rustdoc")
|
||||
.arg(&path)
|
||||
@@ -319,19 +342,17 @@ impl MDBook {
|
||||
}
|
||||
|
||||
/// Look at the `Config` and try to figure out what renderers to use.
|
||||
fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
|
||||
let mut renderers: Vec<Box<Renderer>> = Vec::new();
|
||||
fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
|
||||
let mut renderers = Vec::new();
|
||||
|
||||
if let Some(output_table) = config.get("output").and_then(|o| o.as_table()) {
|
||||
for (key, table) in output_table.iter() {
|
||||
// the "html" backend has its own Renderer
|
||||
if let Some(output_table) = config.get("output").and_then(Value::as_table) {
|
||||
renderers.extend(output_table.iter().map(|(key, table)| {
|
||||
if key == "html" {
|
||||
renderers.push(Box::new(HtmlHandlebars::new()));
|
||||
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
|
||||
} else {
|
||||
let renderer = interpret_custom_renderer(key, table);
|
||||
renderers.push(renderer);
|
||||
interpret_custom_renderer(key, table)
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// if we couldn't find anything, add the HTML renderer as a default
|
||||
@@ -342,56 +363,59 @@ fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
|
||||
renderers
|
||||
}
|
||||
|
||||
fn default_preprocessors() -> Vec<Box<Preprocessor>> {
|
||||
fn default_preprocessors() -> Vec<Box<dyn Preprocessor>> {
|
||||
vec![
|
||||
Box::new(LinkPreprocessor::new()),
|
||||
Box::new(IndexPreprocessor::new()),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_default_preprocessor(pre: &Preprocessor) -> bool {
|
||||
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
|
||||
let name = pre.name();
|
||||
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
|
||||
}
|
||||
|
||||
/// Look at the `MDBook` and try to figure out what preprocessors to run.
|
||||
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
|
||||
let preprocessor_keys = config.get("preprocessor")
|
||||
.and_then(|value| value.as_table())
|
||||
.map(|table| table.keys());
|
||||
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
|
||||
let mut preprocessors = Vec::new();
|
||||
|
||||
let mut preprocessors = if config.build.use_default_preprocessors {
|
||||
default_preprocessors()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if config.build.use_default_preprocessors {
|
||||
preprocessors.extend(default_preprocessors());
|
||||
}
|
||||
|
||||
let preprocessor_keys = match preprocessor_keys {
|
||||
Some(keys) => keys,
|
||||
// If no preprocessor field is set, default to the LinkPreprocessor and
|
||||
// IndexPreprocessor. This allows you to disable default preprocessors
|
||||
// by setting "preprocess" to an empty list.
|
||||
None => return Ok(preprocessors),
|
||||
};
|
||||
|
||||
for key in preprocessor_keys {
|
||||
match key.as_ref() {
|
||||
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
|
||||
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
|
||||
_ => bail!("{:?} is not a recognised preprocessor", key),
|
||||
if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
|
||||
for key in preprocessor_table.keys() {
|
||||
match key.as_ref() {
|
||||
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
|
||||
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
|
||||
name => preprocessors.push(interpret_custom_preprocessor(
|
||||
name,
|
||||
&preprocessor_table[name],
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(preprocessors)
|
||||
}
|
||||
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
|
||||
fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocessor> {
|
||||
let command = table
|
||||
.get("command")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
|
||||
Box::new(CmdPreprocessor::new(key.to_string(), command.to_string()))
|
||||
}
|
||||
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
// look for the `command` field, falling back to using the key
|
||||
// prepended by "mdbook-"
|
||||
let table_dot_command = table
|
||||
.get("command")
|
||||
.and_then(|c| c.as_str())
|
||||
.map(|s| s.to_string());
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
|
||||
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
|
||||
@@ -404,7 +428,11 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
|
||||
///
|
||||
/// The `build.use-default-preprocessors` config option can be used to ensure
|
||||
/// default preprocessors always run if they support the renderer.
|
||||
fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg: &Config) -> bool {
|
||||
fn preprocessor_should_run(
|
||||
preprocessor: &dyn Preprocessor,
|
||||
renderer: &dyn Renderer,
|
||||
cfg: &Config,
|
||||
) -> bool {
|
||||
// default preprocessors should be run by default (if supported)
|
||||
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
|
||||
return preprocessor.supports_renderer(renderer.name());
|
||||
@@ -414,18 +442,19 @@ fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg
|
||||
let renderer_name = renderer.name();
|
||||
|
||||
if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
|
||||
return explicit_renderers.into_iter()
|
||||
.filter_map(|val| val.as_str())
|
||||
return explicit_renderers
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.any(|name| name == renderer_name);
|
||||
}
|
||||
|
||||
preprocessor.supports_renderer(renderer_name)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use toml::value::{Table, Value};
|
||||
|
||||
#[test]
|
||||
@@ -492,8 +521,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_complains_if_unimplemented_preprocessor() {
|
||||
let cfg_str: &'static str = r#"
|
||||
fn can_determine_third_party_preprocessors() {
|
||||
let cfg_str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
|
||||
@@ -509,14 +538,30 @@ mod tests {
|
||||
// make sure the `preprocessor.random` table exists
|
||||
assert!(cfg.get_preprocessor("random").is_some());
|
||||
|
||||
let got = determine_preprocessors(&cfg);
|
||||
let got = determine_preprocessors(&cfg).unwrap();
|
||||
|
||||
assert!(got.is_err());
|
||||
assert!(got.into_iter().any(|p| p.name() == "random"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessors_can_provide_their_own_commands() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
command = "python random.py"
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// make sure the `preprocessor.random` table exists
|
||||
let random = cfg.get_preprocessor("random").unwrap();
|
||||
let random = interpret_custom_preprocessor("random", &Value::Table(random.clone()));
|
||||
|
||||
assert_eq!(random.cmd(), "python random.py");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_respects_preprocessor_selection() {
|
||||
let cfg_str: &'static str = r#"
|
||||
let cfg_str = r#"
|
||||
[preprocessor.links]
|
||||
renderers = ["html"]
|
||||
"#;
|
||||
@@ -524,11 +569,12 @@ mod tests {
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// double-check that we can access preprocessor.links.renderers[0]
|
||||
let html = cfg.get_preprocessor("links")
|
||||
let html = cfg
|
||||
.get_preprocessor("links")
|
||||
.and_then(|links| links.get("renderers"))
|
||||
.and_then(|renderers| renderers.as_array())
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|renderers| renderers.get(0))
|
||||
.and_then(|renderer| renderer.as_str())
|
||||
.and_then(Value::as_str)
|
||||
.unwrap();
|
||||
assert_eq!(html, "html");
|
||||
let html_renderer = HtmlHandlebars::default();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use errors::*;
|
||||
use crate::errors::*;
|
||||
use memchr::{self, Memchr};
|
||||
use pulldown_cmark::{self, Event, Tag};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
@@ -195,7 +195,7 @@ macro_rules! collect_events {
|
||||
}
|
||||
|
||||
impl<'a> SummaryParser<'a> {
|
||||
fn new(text: &str) -> SummaryParser {
|
||||
fn new(text: &str) -> SummaryParser<'_> {
|
||||
let pulldown_parser = pulldown_cmark::Parser::new(text);
|
||||
|
||||
SummaryParser {
|
||||
@@ -259,7 +259,7 @@ impl<'a> SummaryParser<'a> {
|
||||
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
|
||||
}
|
||||
}
|
||||
Some(Event::Start(Tag::Link(href, _))) => {
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => {
|
||||
let link = self.parse_link(href.to_string())?;
|
||||
items.push(SummaryItem::Link(link));
|
||||
}
|
||||
@@ -280,7 +280,7 @@ impl<'a> SummaryParser<'a> {
|
||||
Err(self.parse_error("You can't have an empty link."))
|
||||
} else {
|
||||
Ok(Link {
|
||||
name: name,
|
||||
name,
|
||||
location: PathBuf::from(href.to_string()),
|
||||
number: None,
|
||||
nested_items: Vec::new(),
|
||||
@@ -292,6 +292,7 @@ impl<'a> SummaryParser<'a> {
|
||||
/// already been consumed by a previous parser.
|
||||
fn parse_numbered(&mut self) -> Result<Vec<SummaryItem>> {
|
||||
let mut items = Vec::new();
|
||||
let mut root_items = 0;
|
||||
let root_number = SectionNumber::default();
|
||||
|
||||
// we need to do this funny loop-match-if-let dance because a rule will
|
||||
@@ -308,7 +309,8 @@ impl<'a> SummaryParser<'a> {
|
||||
// if we've resumed after something like a rule the root sections
|
||||
// will be numbered from 1. We need to manually go back and update
|
||||
// them
|
||||
update_section_numbers(&mut bunch_of_items, 0, items.len() as u32);
|
||||
update_section_numbers(&mut bunch_of_items, 0, root_items);
|
||||
root_items += bunch_of_items.len() as u32;
|
||||
items.extend(bunch_of_items);
|
||||
|
||||
match self.next_event() {
|
||||
@@ -395,7 +397,7 @@ impl<'a> SummaryParser<'a> {
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Paragraph)) => continue,
|
||||
Some(Event::Start(Tag::Link(href, _))) => {
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => {
|
||||
let mut link = self.parse_link(href.to_string())?;
|
||||
|
||||
let mut number = parent.clone();
|
||||
@@ -469,13 +471,14 @@ fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
|
||||
|
||||
/// Removes the styling from a list of Markdown events and returns just the
|
||||
/// plain text.
|
||||
fn stringify_events(events: Vec<Event>) -> String {
|
||||
fn stringify_events(events: Vec<Event<'_>>) -> String {
|
||||
events
|
||||
.into_iter()
|
||||
.filter_map(|t| match t {
|
||||
Event::Text(text) => Some(text.into_owned()),
|
||||
Event::Text(text) | Event::Code(text) => Some(text.into_string()),
|
||||
_ => None,
|
||||
}).collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
|
||||
@@ -484,7 +487,7 @@ fn stringify_events(events: Vec<Event>) -> String {
|
||||
pub struct SectionNumber(pub Vec<u32>);
|
||||
|
||||
impl Display for SectionNumber {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if self.0.is_empty() {
|
||||
write!(f, "0")
|
||||
} else {
|
||||
@@ -626,7 +629,7 @@ mod tests {
|
||||
let _ = parser.stream.next(); // skip past start of paragraph
|
||||
|
||||
let href = match parser.stream.next() {
|
||||
Some(Event::Start(Tag::Link(href, _))) => href.to_string(),
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => href.to_string(),
|
||||
other => panic!("Unreachable, {:?}", other),
|
||||
};
|
||||
|
||||
@@ -724,4 +727,41 @@ mod tests {
|
||||
let got = parser.parse_numbered();
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
/// Regression test for https://github.com/rust-lang-nursery/mdBook/issues/779
|
||||
/// Ensure section numbers are correctly incremented after a horizontal separator.
|
||||
#[test]
|
||||
fn keep_numbering_after_separator() {
|
||||
let src =
|
||||
"- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Separator,
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: PathBuf::from("./second.md"),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Separator,
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Third"),
|
||||
location: PathBuf::from("./third.md"),
|
||||
number: Some(SectionNumber(vec![3])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let _ = parser.stream.next();
|
||||
|
||||
let got = parser.parse_numbered().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
use {get_book_dir, open};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
@@ -9,11 +9,14 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Builds a book from its markdown files")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
).arg_from_usage(
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
).arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
|
||||
}
|
||||
|
||||
// Build command implementation
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::get_book_dir;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::MDBook;
|
||||
use std::fs;
|
||||
@@ -10,15 +10,18 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Deletes a built book")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
).arg_from_usage(
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
Running this command deletes this directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
}
|
||||
|
||||
// Clean command implementation
|
||||
pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
|
||||
pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::load(&book_dir)?;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::get_book_dir;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::config;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
@@ -12,8 +12,10 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("init")
|
||||
.about("Creates the boilerplate structure and files for a new book")
|
||||
// the {n} denotes a newline which will properly aligned in all help messages
|
||||
.arg_from_usage("[dir] 'Directory to create the book in{n}\
|
||||
(Defaults to the Current Directory when omitted)'")
|
||||
.arg_from_usage(
|
||||
"[dir] 'Directory to create the book in{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
||||
.arg_from_usage("--force 'Skips confirmation prompts'")
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
extern crate iron;
|
||||
extern crate staticfile;
|
||||
extern crate ws;
|
||||
|
||||
use self::iron::{
|
||||
status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set,
|
||||
};
|
||||
#[cfg(feature = "watch")]
|
||||
use super::watch;
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set};
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
use std;
|
||||
use {get_book_dir, open};
|
||||
|
||||
struct ErrorRecover;
|
||||
|
||||
@@ -22,7 +15,8 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Serves a book at http://localhost:3000, and rebuilds it on changes")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
@@ -75,7 +69,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let port = args.value_of("port").unwrap();
|
||||
let ws_port = args.value_of("websocket-port").unwrap();
|
||||
let hostname = args.value_of("hostname").unwrap();
|
||||
let public_address = args.value_of("websocket-address").unwrap_or(hostname);
|
||||
let public_address = args.value_of("websocket-hostname").unwrap_or(hostname);
|
||||
let open_browser = args.is_present("open");
|
||||
|
||||
let address = format!("{}:{}", hostname, port);
|
||||
@@ -114,8 +108,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
watch::trigger_on_change(&mut book, move |path, book_dir| {
|
||||
info!("File changed: {:?}", path);
|
||||
watch::trigger_on_change(&book, move |paths, book_dir| {
|
||||
info!("Files changed: {:?}", paths);
|
||||
info!("Building book...");
|
||||
|
||||
// FIXME: This area is really ugly because we need to re-set livereload :(
|
||||
@@ -125,7 +119,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
b.config
|
||||
.set("output.html.livereload-url", &livereload_url)?;
|
||||
Ok(b)
|
||||
}).and_then(|b| b.build());
|
||||
})
|
||||
.and_then(|b| b.build());
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to load the book");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::get_book_dir;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
|
||||
@@ -9,7 +9,8 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Tests that a book's Rust code samples compile")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
@@ -30,7 +31,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let library_paths: Vec<&str> = args
|
||||
.values_of("library-path")
|
||||
.map(|v| v.collect())
|
||||
.map(std::iter::Iterator::collect)
|
||||
.unwrap_or_default();
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
extern crate notify;
|
||||
|
||||
use self::notify::Watcher;
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
use std::path::Path;
|
||||
use notify::Watcher;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use {get_book_dir, open};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
@@ -16,11 +15,14 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Watches a book's files and rebuilds it on changes")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
).arg_from_usage(
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
).arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
@@ -33,8 +35,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
open(book.build_dir_for("html").join("index.html"));
|
||||
}
|
||||
|
||||
trigger_on_change(&book, |path, book_dir| {
|
||||
info!("File changed: {:?}\nBuilding book...\n", path);
|
||||
trigger_on_change(&book, |paths, book_dir| {
|
||||
info!("Files changed: {:?}\nBuilding book...\n", paths);
|
||||
let result = MDBook::load(&book_dir).and_then(|b| b.build());
|
||||
|
||||
if let Err(e) = result {
|
||||
@@ -49,10 +51,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
/// Calls the closure when a book source file is changed, blocking indefinitely.
|
||||
pub fn trigger_on_change<F>(book: &MDBook, closure: F)
|
||||
where
|
||||
F: Fn(&Path, &Path),
|
||||
F: Fn(Vec<PathBuf>, &Path),
|
||||
{
|
||||
use self::notify::DebouncedEvent::*;
|
||||
use self::notify::RecursiveMode::*;
|
||||
use notify::DebouncedEvent::*;
|
||||
use notify::RecursiveMode::*;
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
@@ -61,14 +63,14 @@ where
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
error!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
::std::process::exit(1)
|
||||
std::process::exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.source_dir(), Recursive) {
|
||||
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
|
||||
::std::process::exit(1);
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let _ = watcher.watch(book.theme_dir(), Recursive);
|
||||
@@ -78,13 +80,24 @@ where
|
||||
|
||||
info!("Listening for changes...");
|
||||
|
||||
for event in rx.iter() {
|
||||
debug!("Received filesystem event: {:?}", event);
|
||||
match event {
|
||||
Create(path) | Write(path) | Remove(path) | Rename(_, path) => {
|
||||
closure(&path, &book.root);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
loop {
|
||||
let first_event = rx.recv().unwrap();
|
||||
sleep(Duration::from_millis(50));
|
||||
let other_events = rx.try_iter();
|
||||
|
||||
let all_events = std::iter::once(first_event).chain(other_events);
|
||||
|
||||
let paths = all_events
|
||||
.filter_map(|event| {
|
||||
debug!("Received filesystem event: {:?}", event);
|
||||
|
||||
match event {
|
||||
Create(path) | Write(path) | Remove(path) | Rename(_, path) => Some(path),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
closure(paths, &book.root);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
//! The main entrypoint of the `config` module is the `Config` struct. This acts
|
||||
//! essentially as a bag of configuration information, with a couple
|
||||
//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support
|
||||
//! for arbitrary data which is exposed to plugins and alternate backends.
|
||||
//! for arbitrary data which is exposed to plugins and alternative backends.
|
||||
//!
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! # extern crate mdbook;
|
||||
//! # use mdbook::errors::*;
|
||||
//! # extern crate toml;
|
||||
//! use std::path::PathBuf;
|
||||
//! use std::str::FromStr;
|
||||
//! use mdbook::Config;
|
||||
//! use toml::Value;
|
||||
//!
|
||||
@@ -51,18 +50,18 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use toml::value::Table;
|
||||
use toml::{self, Value};
|
||||
use toml_query::delete::TomlValueDeleteExt;
|
||||
use toml_query::insert::TomlValueInsertExt;
|
||||
use toml_query::read::TomlValueReadExt;
|
||||
|
||||
use errors::*;
|
||||
use crate::errors::*;
|
||||
|
||||
/// The overall configuration object for MDBook, essentially an in-memory
|
||||
/// representation of `book.toml`.
|
||||
@@ -75,12 +74,16 @@ pub struct Config {
|
||||
rest: Value,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
impl FromStr for Config {
|
||||
type Err = Error;
|
||||
|
||||
/// Load a `Config` from some string.
|
||||
pub fn from_str(src: &str) -> Result<Config> {
|
||||
fn from_str(src: &str) -> Result<Self> {
|
||||
toml::from_str(src).chain_err(|| Error::from("Invalid configuration file"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load the configuration file from disk.
|
||||
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
|
||||
let mut buffer = String::new();
|
||||
@@ -128,10 +131,8 @@ impl Config {
|
||||
pub fn update_from_env(&mut self) {
|
||||
debug!("Updating the config from environment variables");
|
||||
|
||||
let overrides = env::vars().filter_map(|(key, value)| match parse_env(&key) {
|
||||
Some(index) => Some((index, value)),
|
||||
None => None,
|
||||
});
|
||||
let overrides =
|
||||
env::vars().filter_map(|(key, value)| parse_env(&key).map(|index| (index, value)));
|
||||
|
||||
for (key, value) in overrides {
|
||||
trace!("{} => {}", key, value);
|
||||
@@ -148,14 +149,11 @@ impl Config {
|
||||
/// `output.html.playpen` will fetch the "playpen" out of the html output
|
||||
/// table).
|
||||
pub fn get(&self, key: &str) -> Option<&Value> {
|
||||
match self.rest.read(key) {
|
||||
Ok(inner) => inner,
|
||||
Err(_) => None,
|
||||
}
|
||||
self.rest.read(key).unwrap_or(None)
|
||||
}
|
||||
|
||||
/// Fetch a value from the `Config` so you can mutate it.
|
||||
pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Value> {
|
||||
pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
|
||||
match self.rest.read_mut(key) {
|
||||
Ok(inner) => inner,
|
||||
Err(_) => None,
|
||||
@@ -203,7 +201,9 @@ impl Config {
|
||||
} else if index.starts_with("build.") {
|
||||
self.build.update_value(&index[6..], value);
|
||||
} else {
|
||||
self.rest.insert(index, value)?;
|
||||
self.rest
|
||||
.insert(index, value)
|
||||
.map_err(ErrorKind::TomlQueryError)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -212,13 +212,13 @@ impl Config {
|
||||
/// Get the table associated with a particular renderer.
|
||||
pub fn get_renderer<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
|
||||
let key = format!("output.{}", index.as_ref());
|
||||
self.get(&key).and_then(|v| v.as_table())
|
||||
self.get(&key).and_then(Value::as_table)
|
||||
}
|
||||
|
||||
/// Get the table associated with a particular preprocessor.
|
||||
pub fn get_preprocessor<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
|
||||
let key = format!("preprocessor.{}", index.as_ref());
|
||||
self.get(&key).and_then(|v| v.as_table())
|
||||
self.get(&key).and_then(Value::as_table)
|
||||
}
|
||||
|
||||
fn from_legacy(mut table: Value) -> Config {
|
||||
@@ -265,7 +265,7 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for Config {
|
||||
fn deserialize<D: Deserializer<'de>>(de: D) -> ::std::result::Result<Self, D::Error> {
|
||||
fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
|
||||
let raw = Value::deserialize(de)?;
|
||||
|
||||
if is_legacy_format(&raw) {
|
||||
@@ -301,15 +301,15 @@ impl<'de> Deserialize<'de> for Config {
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Config {
|
||||
book: book,
|
||||
build: build,
|
||||
book,
|
||||
build,
|
||||
rest: Value::Table(table),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Config {
|
||||
fn serialize<S: Serializer>(&self, s: S) -> ::std::result::Result<S::Ok, S::Error> {
|
||||
fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
|
||||
use serde::ser::Error;
|
||||
|
||||
let mut table = self.rest.clone();
|
||||
@@ -371,6 +371,8 @@ pub struct BookConfig {
|
||||
pub src: PathBuf,
|
||||
/// Does this book support more than one language?
|
||||
pub multilingual: bool,
|
||||
/// The main language of the book.
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for BookConfig {
|
||||
@@ -381,6 +383,7 @@ impl Default for BookConfig {
|
||||
description: None,
|
||||
src: PathBuf::from("src"),
|
||||
multilingual: false,
|
||||
language: Some(String::from("en")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -415,6 +418,8 @@ impl Default for BuildConfig {
|
||||
pub struct HtmlConfig {
|
||||
/// The theme directory, if specified.
|
||||
pub theme: Option<PathBuf>,
|
||||
/// The default theme to use, defaults to 'light'
|
||||
pub default_theme: Option<String>,
|
||||
/// Use "smart quotes" instead of the usual `"` character.
|
||||
pub curly_quotes: bool,
|
||||
/// Should mathjax be enabled?
|
||||
@@ -436,10 +441,15 @@ pub struct HtmlConfig {
|
||||
/// This config item *should not be edited* by the end user.
|
||||
#[doc(hidden)]
|
||||
pub livereload_url: Option<String>,
|
||||
/// Should section labels be rendered?
|
||||
/// Don't render section labels.
|
||||
pub no_section_label: bool,
|
||||
/// Search settings. If `None`, the default will be used.
|
||||
pub search: Option<Search>,
|
||||
/// Git repository url. If `None`, the git button will not be shown.
|
||||
pub git_repository_url: Option<String>,
|
||||
/// FontAwesome icon class to use for the Git repository link.
|
||||
/// Defaults to `fa-github` if `None`.
|
||||
pub git_repository_icon: Option<String>,
|
||||
}
|
||||
|
||||
impl HtmlConfig {
|
||||
@@ -533,12 +543,10 @@ trait Updateable<'de>: Serialize + Deserialize<'de> {
|
||||
fn update_value<S: Serialize>(&mut self, key: &str, value: S) {
|
||||
let mut raw = Value::try_from(&self).expect("unreachable");
|
||||
|
||||
{
|
||||
if let Ok(value) = Value::try_from(value) {
|
||||
let _ = raw.insert(key, value);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if let Ok(value) = Value::try_from(value) {
|
||||
let _ = raw.insert(key, value);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(updated) = raw.try_into() {
|
||||
@@ -553,13 +561,14 @@ impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const COMPLEX_CONFIG: &'static str = r#"
|
||||
const COMPLEX_CONFIG: &str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
|
||||
description = "A completely useless book"
|
||||
multilingual = true
|
||||
src = "source"
|
||||
language = "ja"
|
||||
|
||||
[build]
|
||||
build-dir = "outputs"
|
||||
@@ -568,17 +577,20 @@ mod tests {
|
||||
|
||||
[output.html]
|
||||
theme = "./themedir"
|
||||
default-theme = "rust"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["./foo/bar/baz.css"]
|
||||
git-repository-url = "https://foo.com/"
|
||||
git-repository-icon = "fa-code-fork"
|
||||
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
editor = "ace"
|
||||
|
||||
[preprocess.first]
|
||||
[preprocessor.first]
|
||||
|
||||
[preprocess.second]
|
||||
[preprocessor.second]
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
@@ -591,7 +603,7 @@ mod tests {
|
||||
description: Some(String::from("A completely useless book")),
|
||||
multilingual: true,
|
||||
src: PathBuf::from("source"),
|
||||
..Default::default()
|
||||
language: Some(String::from("ja")),
|
||||
};
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("outputs"),
|
||||
@@ -607,7 +619,10 @@ mod tests {
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
|
||||
theme: Some(PathBuf::from("./themedir")),
|
||||
default_theme: Some(String::from("rust")),
|
||||
playpen: playpen_should_be,
|
||||
git_repository_url: Some(String::from("https://foo.com/")),
|
||||
git_repository_icon: Some(String::from("fa-code-fork")),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -645,10 +660,10 @@ mod tests {
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
let baz: Vec<bool> = cfg.get_deserialized("output.random.baz").unwrap();
|
||||
let got_baz: Vec<bool> = cfg.get_deserialized("output.random.baz").unwrap();
|
||||
let baz_should_be = vec![true, true, false];
|
||||
|
||||
assert_eq!(baz, baz_should_be);
|
||||
assert_eq!(got_baz, baz_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -740,7 +755,7 @@ mod tests {
|
||||
|
||||
for (src, should_be) in inputs {
|
||||
let got = parse_env(src);
|
||||
let should_be = should_be.map(|s| s.to_string());
|
||||
let should_be = should_be.map(ToString::to_string);
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
@@ -770,6 +785,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::approx_constant)]
|
||||
fn update_config_using_env_var_and_complex_value() {
|
||||
let mut cfg = Config::default();
|
||||
let key = "foo-bar.baz";
|
||||
|
||||
53
src/lib.rs
53
src/lib.rs
@@ -81,27 +81,18 @@
|
||||
//! [`Config`]: config/struct.Config.html
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(rust_2018_idioms)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
extern crate handlebars;
|
||||
extern crate itertools;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate memchr;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate regex;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
extern crate shlex;
|
||||
extern crate tempfile;
|
||||
extern crate toml;
|
||||
extern crate toml_query;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
@@ -114,31 +105,33 @@ pub mod renderer;
|
||||
pub mod theme;
|
||||
pub mod utils;
|
||||
|
||||
pub use book::BookItem;
|
||||
pub use book::MDBook;
|
||||
pub use config::Config;
|
||||
pub use renderer::Renderer;
|
||||
/// The current version of `mdbook`.
|
||||
///
|
||||
/// This is provided as a way for custom preprocessors and renderers to do
|
||||
/// compatibility checks.
|
||||
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub use crate::book::BookItem;
|
||||
pub use crate::book::MDBook;
|
||||
pub use crate::config::Config;
|
||||
pub use crate::renderer::Renderer;
|
||||
|
||||
/// The error types used through out this crate.
|
||||
pub mod errors {
|
||||
use std::path::PathBuf;
|
||||
|
||||
error_chain!{
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Io(::std::io::Error) #[doc = "A wrapper around `std::io::Error`"];
|
||||
HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"];
|
||||
HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"];
|
||||
Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
|
||||
SerdeJson(::serde_json::Error) #[doc = "JSON conversion failed"];
|
||||
}
|
||||
|
||||
links {
|
||||
TomlQuery(::toml_query::error::Error, ::toml_query::error::ErrorKind) #[doc = "A TomlQuery error"];
|
||||
Io(std::io::Error) #[doc = "A wrapper around `std::io::Error`"];
|
||||
HandlebarsRender(handlebars::RenderError) #[doc = "Handlebars rendering failed"];
|
||||
HandlebarsTemplate(Box<handlebars::TemplateError>) #[doc = "Unable to parse the template"];
|
||||
Utf8(std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
|
||||
SerdeJson(serde_json::Error) #[doc = "JSON conversion failed"];
|
||||
}
|
||||
|
||||
errors {
|
||||
/// A subprocess exited with an unsuccessful return code.
|
||||
Subprocess(message: String, output: ::std::process::Output) {
|
||||
Subprocess(message: String, output: std::process::Output) {
|
||||
description("A subprocess failed")
|
||||
display("{}: {}", message, String::from_utf8_lossy(&output.stdout))
|
||||
}
|
||||
@@ -154,12 +147,18 @@ pub mod errors {
|
||||
description("Reserved Filename")
|
||||
display("{} is reserved for internal use", filename.display())
|
||||
}
|
||||
|
||||
/// Error with a TOML file.
|
||||
TomlQueryError(inner: toml_query::error::Error) {
|
||||
description("toml_query error")
|
||||
display("{}", inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Box to halve the size of Error
|
||||
impl From<::handlebars::TemplateError> for Error {
|
||||
fn from(e: ::handlebars::TemplateError) -> Error {
|
||||
impl From<handlebars::TemplateError> for Error {
|
||||
fn from(e: handlebars::TemplateError) -> Error {
|
||||
From::from(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
17
src/main.rs
17
src/main.rs
@@ -1,12 +1,7 @@
|
||||
extern crate chrono;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate env_logger;
|
||||
extern crate error_chain;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate mdbook;
|
||||
extern crate open;
|
||||
|
||||
use chrono::Local;
|
||||
use clap::{App, AppSettings, ArgMatches};
|
||||
@@ -20,19 +15,19 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
mod cmd;
|
||||
|
||||
const NAME: &'static str = "mdBook";
|
||||
const VERSION: &'static str = concat!("v", crate_version!());
|
||||
const VERSION: &str = concat!("v", crate_version!());
|
||||
|
||||
fn main() {
|
||||
init_logger();
|
||||
|
||||
// Create a list of valid arguments and sub-commands
|
||||
let app = App::new(NAME)
|
||||
.about("Creates a book from markdown files")
|
||||
let app = App::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
.version(VERSION)
|
||||
.setting(AppSettings::GlobalVersion)
|
||||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.setting(AppSettings::ColoredHelp)
|
||||
.after_help(
|
||||
"For more information about a specific command, try `mdbook <command> --help`\n\
|
||||
The source code for mdBook is available at: https://github.com/rust-lang-nursery/mdBook",
|
||||
@@ -63,7 +58,7 @@ fn main() {
|
||||
if let Err(e) = res {
|
||||
utils::log_backtrace(&e);
|
||||
|
||||
::std::process::exit(101);
|
||||
std::process::exit(101);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +77,7 @@ fn init_logger() {
|
||||
});
|
||||
|
||||
if let Ok(var) = env::var("RUST_LOG") {
|
||||
builder.parse(&var);
|
||||
builder.parse_filters(&var);
|
||||
} else {
|
||||
// if no RUST_LOG provided, default to logging at the Info level
|
||||
builder.filter(None, LevelFilter::Info);
|
||||
|
||||
196
src/preprocess/cmd.rs
Normal file
196
src/preprocess/cmd.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::Book;
|
||||
use crate::errors::*;
|
||||
use shlex::Shlex;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
||||
/// A custom preprocessor which will shell out to a 3rd-party program.
|
||||
///
|
||||
/// # Preprocessing Protocol
|
||||
///
|
||||
/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will
|
||||
/// execute the shell command `$cmd supports $renderer`. If the renderer is
|
||||
/// supported, custom preprocessors should exit with a exit code of `0`,
|
||||
/// any other exit code be considered as unsupported.
|
||||
///
|
||||
/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)`
|
||||
/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors
|
||||
/// should then "return" a processed book by printing it to `stdout` as JSON.
|
||||
/// For convenience, the `CmdPreprocessor::parse_input()` function can be used
|
||||
/// to parse the input provided by `mdbook`.
|
||||
///
|
||||
/// Exiting with a non-zero exit code while preprocessing is considered an
|
||||
/// error. `stderr` is passed directly through to the user, so it can be used
|
||||
/// for logging or emitting warnings if desired.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// An example preprocessor is available in this project's `examples/`
|
||||
/// directory.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CmdPreprocessor {
|
||||
name: String,
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
impl CmdPreprocessor {
|
||||
/// Create a new `CmdPreprocessor`.
|
||||
pub fn new(name: String, cmd: String) -> CmdPreprocessor {
|
||||
CmdPreprocessor { name, cmd }
|
||||
}
|
||||
|
||||
/// A convenience function custom preprocessors can use to parse the input
|
||||
/// written to `stdin` by a `CmdRenderer`.
|
||||
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
|
||||
serde_json::from_reader(reader).chain_err(|| "Unable to parse the input")
|
||||
}
|
||||
|
||||
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
|
||||
let stdin = child.stdin.take().expect("Child has stdin");
|
||||
|
||||
if let Err(e) = self.write_input(stdin, &book, &ctx) {
|
||||
// Looks like the backend hung up before we could finish
|
||||
// sending it the render context. Log the error and keep going
|
||||
warn!("Error writing the RenderContext to the backend, {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_input<W: Write>(
|
||||
&self,
|
||||
writer: W,
|
||||
book: &Book,
|
||||
ctx: &PreprocessorContext,
|
||||
) -> Result<()> {
|
||||
serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// The command this `Preprocessor` will invoke.
|
||||
pub fn cmd(&self) -> &str {
|
||||
&self.cmd
|
||||
}
|
||||
|
||||
fn command(&self) -> Result<Command> {
|
||||
let mut words = Shlex::new(&self.cmd);
|
||||
let executable = match words.next() {
|
||||
Some(e) => e,
|
||||
None => bail!("Command string was empty"),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(executable);
|
||||
|
||||
for arg in words {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Preprocessor for CmdPreprocessor {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
|
||||
let mut cmd = self.command()?;
|
||||
|
||||
let mut child = cmd
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Unable to start the \"{}\" preprocessor. Is it installed?",
|
||||
self.name()
|
||||
)
|
||||
})?;
|
||||
|
||||
self.write_input_to_child(&mut child, &book, ctx);
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.chain_err(|| "Error waiting for the preprocessor to complete")?;
|
||||
|
||||
trace!("{} exited with output: {:?}", self.cmd, output);
|
||||
ensure!(
|
||||
output.status.success(),
|
||||
"The preprocessor exited unsuccessfully"
|
||||
);
|
||||
|
||||
serde_json::from_slice(&output.stdout).chain_err(|| "Unable to parse the preprocessed book")
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
debug!(
|
||||
"Checking if the \"{}\" preprocessor supports \"{}\"",
|
||||
self.name(),
|
||||
renderer
|
||||
);
|
||||
|
||||
let mut cmd = match self.command() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Unable to create the command for the \"{}\" preprocessor, {}",
|
||||
self.name(),
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let outcome = cmd
|
||||
.arg("supports")
|
||||
.arg(renderer)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.status()
|
||||
.map(|status| status.code() == Some(0));
|
||||
|
||||
if let Err(ref e) = outcome {
|
||||
if e.kind() == io::ErrorKind::NotFound {
|
||||
warn!(
|
||||
"The command wasn't found, is the \"{}\" preprocessor installed?",
|
||||
self.name
|
||||
);
|
||||
warn!("\tCommand: {}", self.cmd);
|
||||
}
|
||||
}
|
||||
|
||||
outcome.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::MDBook;
|
||||
use std::path::Path;
|
||||
|
||||
fn book_example() -> MDBook {
|
||||
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
|
||||
MDBook::load(example).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_write_and_parse_input() {
|
||||
let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string());
|
||||
let md = book_example();
|
||||
let ctx = PreprocessorContext::new(
|
||||
md.root.clone(),
|
||||
md.config.clone(),
|
||||
"some-renderer".to_string(),
|
||||
);
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
|
||||
|
||||
let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
|
||||
|
||||
assert_eq!(got_book, md.book);
|
||||
assert_eq!(got_ctx, ctx);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
use regex::Regex;
|
||||
use std::path::Path;
|
||||
|
||||
use errors::*;
|
||||
use crate::errors::*;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use book::{Book, BookItem};
|
||||
use crate::book::{Book, BookItem};
|
||||
|
||||
/// A preprocessor for converting file name `README.md` to `index.md` since
|
||||
/// `README.md` is the de facto index file in a markdown-based documentation.
|
||||
/// `README.md` is the de facto index file in markdown-based documentation.
|
||||
#[derive(Default)]
|
||||
pub struct IndexPreprocessor;
|
||||
|
||||
impl IndexPreprocessor {
|
||||
@@ -45,7 +46,10 @@ impl Preprocessor for IndexPreprocessor {
|
||||
|
||||
fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
|
||||
let file_name = readme_path.as_ref().file_name().unwrap_or_default();
|
||||
let parent_dir = index_path.as_ref().parent().unwrap_or(index_path.as_ref());
|
||||
let parent_dir = index_path
|
||||
.as_ref()
|
||||
.parent()
|
||||
.unwrap_or_else(|| index_path.as_ref());
|
||||
warn!(
|
||||
"It seems that there are both {:?} and index.md under \"{}\".",
|
||||
file_name,
|
||||
@@ -67,7 +71,7 @@ fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
|
||||
RE.is_match(
|
||||
path.as_ref()
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use errors::*;
|
||||
use crate::errors::*;
|
||||
use crate::utils::{take_anchored_lines, take_lines};
|
||||
use regex::{CaptureMatches, Captures, Regex};
|
||||
use std::fs;
|
||||
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
|
||||
use std::path::{Path, PathBuf};
|
||||
use utils::fs::file_to_string;
|
||||
use utils::take_lines;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use book::{Book, BookItem};
|
||||
use crate::book::{Book, BookItem};
|
||||
|
||||
const ESCAPE_CHAR: char = '\\';
|
||||
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
|
||||
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
|
||||
/// helpers in a chapter.
|
||||
#[derive(Default)]
|
||||
pub struct LinkPreprocessor;
|
||||
|
||||
impl LinkPreprocessor {
|
||||
@@ -62,13 +63,13 @@ where
|
||||
let mut previous_end_index = 0;
|
||||
let mut replaced = String::new();
|
||||
|
||||
for playpen in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..playpen.start_index]);
|
||||
for link in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..link.start_index]);
|
||||
|
||||
match playpen.render_with_path(&path) {
|
||||
match link.render_with_path(&path) {
|
||||
Ok(new_content) => {
|
||||
if depth < MAX_LINK_NESTED_DEPTH {
|
||||
if let Some(rel_path) = playpen.link.relative_path(path) {
|
||||
if let Some(rel_path) = link.link_type.relative_path(path) {
|
||||
replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1));
|
||||
} else {
|
||||
replaced.push_str(&new_content);
|
||||
@@ -79,13 +80,17 @@ where
|
||||
source.display()
|
||||
);
|
||||
}
|
||||
previous_end_index = playpen.end_index;
|
||||
previous_end_index = link.end_index;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error updating \"{}\", {}", playpen.link_text, e);
|
||||
error!("Error updating \"{}\", {}", link.link_text, e);
|
||||
for cause in e.iter().skip(1) {
|
||||
warn!("Caused By: {}", cause);
|
||||
}
|
||||
|
||||
// This should make sure we include the raw `{{# ... }}` snippet
|
||||
// in the page content if there are any errors.
|
||||
previous_end_index = playpen.start_index;
|
||||
previous_end_index = link.start_index;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,6 +106,7 @@ enum LinkType<'a> {
|
||||
IncludeRangeFrom(PathBuf, RangeFrom<usize>),
|
||||
IncludeRangeTo(PathBuf, RangeTo<usize>),
|
||||
IncludeRangeFull(PathBuf, RangeFull),
|
||||
IncludeAnchor(PathBuf, String),
|
||||
Playpen(PathBuf, Vec<&'a str>),
|
||||
}
|
||||
|
||||
@@ -113,6 +119,7 @@ impl<'a> LinkType<'a> {
|
||||
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeAnchor(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
|
||||
}
|
||||
}
|
||||
@@ -128,37 +135,43 @@ fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
|
||||
fn parse_include_path(path: &str) -> LinkType<'static> {
|
||||
let mut parts = path.split(':');
|
||||
let path = parts.next().unwrap().into();
|
||||
// subtract 1 since line numbers usually begin with 1
|
||||
let start = parts
|
||||
.next()
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.map(|val| val.saturating_sub(1));
|
||||
|
||||
let next_element = parts.next();
|
||||
let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) {
|
||||
// subtract 1 since line numbers usually begin with 1
|
||||
Some(value.saturating_sub(1))
|
||||
} else if let Some(anchor) = next_element {
|
||||
if anchor == "" {
|
||||
None
|
||||
} else {
|
||||
return LinkType::IncludeAnchor(path, String::from(anchor));
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let end = parts.next();
|
||||
let has_end = end.is_some();
|
||||
let end = end.and_then(|s| s.parse::<usize>().ok());
|
||||
match start {
|
||||
Some(start) => match end {
|
||||
Some(end) => LinkType::IncludeRange(
|
||||
path,
|
||||
Range {
|
||||
start: start,
|
||||
end: end,
|
||||
},
|
||||
),
|
||||
None => if has_end {
|
||||
LinkType::IncludeRangeFrom(path, RangeFrom { start: start })
|
||||
} else {
|
||||
LinkType::IncludeRange(
|
||||
path,
|
||||
Range {
|
||||
start: start,
|
||||
end: start + 1,
|
||||
},
|
||||
)
|
||||
},
|
||||
Some(end) => LinkType::IncludeRange(path, Range { start, end }),
|
||||
None => {
|
||||
if has_end {
|
||||
LinkType::IncludeRangeFrom(path, RangeFrom { start })
|
||||
} else {
|
||||
LinkType::IncludeRange(
|
||||
path,
|
||||
Range {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
None => match end {
|
||||
Some(end) => LinkType::IncludeRangeTo(path, RangeTo { end: end }),
|
||||
Some(end) => LinkType::IncludeRangeTo(path, RangeTo { end }),
|
||||
None => LinkType::IncludeRangeFull(path, RangeFull),
|
||||
},
|
||||
}
|
||||
@@ -168,7 +181,7 @@ fn parse_include_path(path: &str) -> LinkType<'static> {
|
||||
struct Link<'a> {
|
||||
start_index: usize,
|
||||
end_index: usize,
|
||||
link: LinkType<'a>,
|
||||
link_type: LinkType<'a>,
|
||||
link_text: &'a str,
|
||||
}
|
||||
|
||||
@@ -192,11 +205,11 @@ impl<'a> Link<'a> {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
link_type.and_then(|lnk| {
|
||||
link_type.and_then(|lnk_type| {
|
||||
cap.get(0).map(|mat| Link {
|
||||
start_index: mat.start(),
|
||||
end_index: mat.end(),
|
||||
link: lnk,
|
||||
link_type: lnk_type,
|
||||
link_text: mat.as_str(),
|
||||
})
|
||||
})
|
||||
@@ -204,23 +217,82 @@ impl<'a> Link<'a> {
|
||||
|
||||
fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
|
||||
let base = base.as_ref();
|
||||
match self.link {
|
||||
match self.link_type {
|
||||
// omit the escape char
|
||||
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
||||
LinkType::IncludeRange(ref pat, ref range) => file_to_string(base.join(pat))
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRangeFrom(ref pat, ref range) => file_to_string(base.join(pat))
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRangeTo(ref pat, ref range) => file_to_string(base.join(pat))
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRangeFull(ref pat, _) => file_to_string(base.join(pat))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRange(ref pat, ref range) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
fs::read_to_string(&target)
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeRangeFrom(ref pat, ref range) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
fs::read_to_string(&target)
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeRangeTo(ref pat, ref range) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
fs::read_to_string(&target)
|
||||
.map(|s| take_lines(&s, *range))
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeRangeFull(ref pat, _) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
fs::read_to_string(&target).chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeAnchor(ref pat, ref anchor) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
fs::read_to_string(&target)
|
||||
.map(|s| take_anchored_lines(&s, anchor))
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::Playpen(ref pat, ref attrs) => {
|
||||
let contents = file_to_string(base.join(pat))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text))?;
|
||||
let target = base.join(pat);
|
||||
|
||||
let contents = fs::read_to_string(&target).chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display()
|
||||
)
|
||||
})?;
|
||||
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
||||
Ok(format!(
|
||||
"```{}{}\n{}\n```\n",
|
||||
@@ -247,7 +319,7 @@ impl<'a> Iterator for LinkIter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_links(contents: &str) -> LinkIter {
|
||||
fn find_links(contents: &str) -> LinkIter<'_> {
|
||||
// lazily compute following regex
|
||||
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
|
||||
lazy_static! {
|
||||
@@ -260,7 +332,8 @@ fn find_links(contents: &str) -> LinkIter {
|
||||
\s+ # separating whitespace
|
||||
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
|
||||
\s*\}\} # whitespace and link closing parens"
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
LinkIter(RE.captures_iter(contents))
|
||||
}
|
||||
@@ -325,13 +398,13 @@ mod tests {
|
||||
Link {
|
||||
start_index: 22,
|
||||
end_index: 42,
|
||||
link: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
|
||||
link_type: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
|
||||
link_text: "{{#playpen file.rs}}",
|
||||
},
|
||||
Link {
|
||||
start_index: 47,
|
||||
end_index: 68,
|
||||
link: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
|
||||
link_type: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
|
||||
link_text: "{{#playpen test.rs }}",
|
||||
},
|
||||
]
|
||||
@@ -348,7 +421,7 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 48,
|
||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
|
||||
link_type: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
|
||||
link_text: "{{#include file.rs:10:20}}",
|
||||
}]
|
||||
);
|
||||
@@ -364,7 +437,7 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 45,
|
||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
|
||||
link_type: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
|
||||
link_text: "{{#include file.rs:10}}",
|
||||
}]
|
||||
);
|
||||
@@ -380,7 +453,7 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
|
||||
link_type: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
|
||||
link_text: "{{#include file.rs:10:}}",
|
||||
}]
|
||||
);
|
||||
@@ -396,7 +469,7 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
|
||||
link_type: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
|
||||
link_text: "{{#include file.rs::20}}",
|
||||
}]
|
||||
);
|
||||
@@ -412,7 +485,7 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 44,
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_type: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_text: "{{#include file.rs::}}",
|
||||
}]
|
||||
);
|
||||
@@ -428,12 +501,31 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 42,
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_type: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_text: "{{#include file.rs}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_with_anchor() {
|
||||
let s = "Some random text with {{#include file.rs:anchor}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 49,
|
||||
link_type: LinkType::IncludeAnchor(
|
||||
PathBuf::from("file.rs"),
|
||||
String::from("anchor")
|
||||
),
|
||||
link_text: "{{#include file.rs:anchor}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_escaped_link() {
|
||||
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
|
||||
@@ -446,7 +538,7 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: LinkType::Escaped,
|
||||
link_type: LinkType::Escaped,
|
||||
link_text: "\\{{#playpen file.rs editable}}",
|
||||
}]
|
||||
);
|
||||
@@ -465,13 +557,13 @@ mod tests {
|
||||
Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
|
||||
link_type: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
|
||||
link_text: "{{#playpen file.rs editable }}",
|
||||
},
|
||||
Link {
|
||||
start_index: 89,
|
||||
end_index: 136,
|
||||
link: LinkType::Playpen(
|
||||
link_type: LinkType::Playpen(
|
||||
PathBuf::from("my.rs"),
|
||||
vec!["editable", "no_run", "should_panic"],
|
||||
),
|
||||
@@ -495,7 +587,7 @@ mod tests {
|
||||
Link {
|
||||
start_index: 38,
|
||||
end_index: 58,
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_type: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_text: "{{#include file.rs}}",
|
||||
}
|
||||
);
|
||||
@@ -504,7 +596,7 @@ mod tests {
|
||||
Link {
|
||||
start_index: 63,
|
||||
end_index: 112,
|
||||
link: LinkType::Escaped,
|
||||
link_type: LinkType::Escaped,
|
||||
link_text: "\\{{#contents are insignifficant in escaped link}}",
|
||||
}
|
||||
);
|
||||
@@ -513,7 +605,7 @@ mod tests {
|
||||
Link {
|
||||
start_index: 130,
|
||||
end_index: 177,
|
||||
link: LinkType::Playpen(
|
||||
link_type: LinkType::Playpen(
|
||||
PathBuf::from("my.rs"),
|
||||
vec!["editable", "no_run", "should_panic"]
|
||||
),
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
//! Book preprocessing.
|
||||
|
||||
pub use self::cmd::CmdPreprocessor;
|
||||
pub use self::index::IndexPreprocessor;
|
||||
pub use self::links::LinkPreprocessor;
|
||||
|
||||
mod cmd;
|
||||
mod index;
|
||||
mod links;
|
||||
|
||||
use book::Book;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Extra information for a `Preprocessor` to give them more context when
|
||||
/// processing a book.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PreprocessorContext {
|
||||
/// The location of the book directory on disk.
|
||||
pub root: PathBuf,
|
||||
@@ -21,12 +24,22 @@ pub struct PreprocessorContext {
|
||||
pub config: Config,
|
||||
/// The `Renderer` this preprocessor is being used with.
|
||||
pub renderer: String,
|
||||
/// The calling `mdbook` version.
|
||||
pub mdbook_version: String,
|
||||
#[serde(skip)]
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
impl PreprocessorContext {
|
||||
/// Create a new `PreprocessorContext`.
|
||||
pub(crate) fn new(root: PathBuf, config: Config, renderer: String) -> Self {
|
||||
PreprocessorContext { root, config, renderer }
|
||||
PreprocessorContext {
|
||||
root,
|
||||
config,
|
||||
renderer,
|
||||
mdbook_version: crate::MDBOOK_VERSION.to_string(),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use book::{Book, BookItem};
|
||||
use config::{Config, HtmlConfig, Playpen};
|
||||
use errors::*;
|
||||
use renderer::html_handlebars::helpers;
|
||||
use renderer::{RenderContext, Renderer};
|
||||
use theme::{self, playpen_editor, Theme};
|
||||
use utils;
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::config::{Config, HtmlConfig, Playpen};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::theme::{self, playpen_editor, Theme};
|
||||
use crate::utils;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
@@ -13,7 +13,6 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use handlebars::Handlebars;
|
||||
use regex::{Captures, Regex};
|
||||
use serde_json;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HtmlHandlebars;
|
||||
@@ -26,79 +25,77 @@ impl HtmlHandlebars {
|
||||
fn render_item(
|
||||
&self,
|
||||
item: &BookItem,
|
||||
mut ctx: RenderItemContext,
|
||||
mut ctx: RenderItemContext<'_>,
|
||||
print_content: &mut String,
|
||||
) -> Result<()> {
|
||||
// FIXME: This should be made DRY-er and rely less on mutable state
|
||||
match *item {
|
||||
BookItem::Chapter(ref ch) => {
|
||||
let content = ch.content.clone();
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
print_content.push_str(&content);
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
let content = ch.content.clone();
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
|
||||
// Update the context with data for this file
|
||||
let path = ch
|
||||
.path
|
||||
.to_str()
|
||||
.chain_err(|| "Could not convert path to str")?;
|
||||
let filepath = Path::new(&ch.path).with_extension("html");
|
||||
let fixed_content = utils::render_markdown_with_path(
|
||||
&ch.content,
|
||||
ctx.html_config.curly_quotes,
|
||||
Some(&ch.path),
|
||||
);
|
||||
print_content.push_str(&fixed_content);
|
||||
|
||||
// "print.html" is used for the print page.
|
||||
if ch.path == Path::new("print.md") {
|
||||
bail!(ErrorKind::ReservedFilenameError(ch.path.clone()));
|
||||
};
|
||||
// Update the context with data for this file
|
||||
let path = ch
|
||||
.path
|
||||
.to_str()
|
||||
.chain_err(|| "Could not convert path to str")?;
|
||||
let filepath = Path::new(&ch.path).with_extension("html");
|
||||
|
||||
// Non-lexical lifetimes needed :'(
|
||||
let title: String;
|
||||
{
|
||||
let book_title = ctx
|
||||
.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
title = ch.name.clone() + " - " + book_title;
|
||||
}
|
||||
// "print.html" is used for the print page.
|
||||
if ch.path == Path::new("print.md") {
|
||||
bail!(ErrorKind::ReservedFilenameError(ch.path.clone()));
|
||||
};
|
||||
|
||||
ctx.data.insert("path".to_owned(), json!(path));
|
||||
ctx.data.insert("content".to_owned(), json!(content));
|
||||
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
|
||||
ctx.data.insert("title".to_owned(), json!(title));
|
||||
ctx.data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&ch.path)),
|
||||
);
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
|
||||
let rendered = self.post_process(rendered, &ctx.html_config.playpen);
|
||||
|
||||
// Write to file
|
||||
debug!("Creating {}", filepath.display());
|
||||
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
|
||||
|
||||
if ctx.is_index {
|
||||
ctx.data.insert("path".to_owned(), json!("index.html"));
|
||||
ctx.data.insert("path_to_root".to_owned(), json!(""));
|
||||
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
|
||||
let rendered_index =
|
||||
self.post_process(rendered_index, &ctx.html_config.playpen);
|
||||
debug!("Creating index.html from {}", path);
|
||||
utils::fs::write_file(
|
||||
&ctx.destination,
|
||||
"index.html",
|
||||
rendered_index.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
// Non-lexical lifetimes needed :'(
|
||||
let title: String;
|
||||
{
|
||||
let book_title = ctx
|
||||
.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
title = ch.name.clone() + " - " + book_title;
|
||||
}
|
||||
|
||||
ctx.data.insert("path".to_owned(), json!(path));
|
||||
ctx.data.insert("content".to_owned(), json!(content));
|
||||
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
|
||||
ctx.data.insert("title".to_owned(), json!(title));
|
||||
ctx.data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&ch.path)),
|
||||
);
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
|
||||
let rendered = self.post_process(rendered, &ctx.html_config.playpen);
|
||||
|
||||
// Write to file
|
||||
debug!("Creating {}", filepath.display());
|
||||
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
|
||||
|
||||
if ctx.is_index {
|
||||
ctx.data.insert("path".to_owned(), json!("index.md"));
|
||||
ctx.data.insert("path_to_root".to_owned(), json!(""));
|
||||
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
|
||||
let rendered_index = self.post_process(rendered_index, &ctx.html_config.playpen);
|
||||
debug!("Creating index.html from {}", path);
|
||||
utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
|
||||
fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String {
|
||||
let rendered = build_header_links(&rendered);
|
||||
let rendered = fix_code_blocks(&rendered);
|
||||
@@ -113,7 +110,7 @@ impl HtmlHandlebars {
|
||||
theme: &Theme,
|
||||
html_config: &HtmlConfig,
|
||||
) -> Result<()> {
|
||||
use utils::fs::write_file;
|
||||
use crate::utils::fs::write_file;
|
||||
|
||||
write_file(
|
||||
destination,
|
||||
@@ -214,6 +211,7 @@ impl HtmlHandlebars {
|
||||
);
|
||||
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
|
||||
handlebars.register_helper("next", Box::new(helpers::navigation::next));
|
||||
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
|
||||
}
|
||||
|
||||
/// Copy across any additional CSS and JavaScript files which the book
|
||||
@@ -327,7 +325,7 @@ impl Renderer for HtmlHandlebars {
|
||||
handlebars: &handlebars,
|
||||
destination: destination.to_path_buf(),
|
||||
data: data.clone(),
|
||||
is_index: is_index,
|
||||
is_index,
|
||||
html_config: html_config.clone(),
|
||||
};
|
||||
self.render_item(item, ctx, &mut print_content)?;
|
||||
@@ -381,7 +379,10 @@ fn make_data(
|
||||
let html = config.html_config().unwrap_or_default();
|
||||
|
||||
let mut data = serde_json::Map::new();
|
||||
data.insert("language".to_owned(), json!("en"));
|
||||
data.insert(
|
||||
"language".to_owned(),
|
||||
json!(config.book.language.clone().unwrap_or_default()),
|
||||
);
|
||||
data.insert(
|
||||
"book_title".to_owned(),
|
||||
json!(config.book.title.clone().unwrap_or_default()),
|
||||
@@ -395,6 +396,12 @@ fn make_data(
|
||||
data.insert("livereload".to_owned(), json!(livereload));
|
||||
}
|
||||
|
||||
let default_theme = match html_config.default_theme {
|
||||
Some(ref theme) => theme,
|
||||
None => "light",
|
||||
};
|
||||
data.insert("default_theme".to_owned(), json!(default_theme));
|
||||
|
||||
// Add google analytics tag
|
||||
if let Some(ref ga) = config.html_config().and_then(|html| html.google_analytics) {
|
||||
data.insert("google_analytics".to_owned(), json!(ga));
|
||||
@@ -422,13 +429,7 @@ fn make_data(
|
||||
for script in &html.additional_js {
|
||||
match script.strip_prefix(root) {
|
||||
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => js.push(
|
||||
script
|
||||
.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str"),
|
||||
),
|
||||
Err(_) => js.push(script.to_str().expect("Could not convert to str")),
|
||||
}
|
||||
}
|
||||
data.insert("additional_js".to_owned(), json!(js));
|
||||
@@ -454,6 +455,15 @@ fn make_data(
|
||||
)
|
||||
}
|
||||
|
||||
if let Some(ref git_repository_url) = html_config.git_repository_url {
|
||||
data.insert("git_repository_url".to_owned(), json!(git_repository_url));
|
||||
}
|
||||
let git_repository_icon = match html_config.git_repository_icon {
|
||||
Some(ref git_repository_icon) => git_repository_icon,
|
||||
None => "fa-github",
|
||||
};
|
||||
data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
|
||||
|
||||
let mut chapters = vec![];
|
||||
|
||||
for item in book.iter() {
|
||||
@@ -487,25 +497,26 @@ fn make_data(
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Goes through the rendered HTML, making sure all header tags are wrapped in
|
||||
/// an anchor so people can link to sections directly.
|
||||
/// Goes through the rendered HTML, making sure all header tags have
|
||||
/// an anchor respectively so people can link to sections directly.
|
||||
fn build_header_links(html: &str) -> String {
|
||||
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let level = caps[1]
|
||||
.parse()
|
||||
.expect("Regex should ensure we only ever get numbers here");
|
||||
|
||||
wrap_header_with_link(level, &caps[2], &mut id_counter)
|
||||
}).into_owned()
|
||||
insert_link_into_header(level, &caps[2], &mut id_counter)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Wraps a single header tag with a link, making sure each tag gets its own
|
||||
/// Insert a sinle link into a header, making sure each link gets its own
|
||||
/// unique ID by appending an auto-incremented number (if necessary).
|
||||
fn wrap_header_with_link(
|
||||
fn insert_link_into_header(
|
||||
level: usize,
|
||||
content: &str,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
@@ -522,7 +533,7 @@ fn wrap_header_with_link(
|
||||
*id_count += 1;
|
||||
|
||||
format!(
|
||||
r##"<a class="header" href="#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
|
||||
r##"<h{level}><a class="header" href="#{id}" id="{id}">{text}</a></h{level}>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content
|
||||
@@ -540,7 +551,7 @@ fn wrap_header_with_link(
|
||||
fn fix_code_blocks(html: &str) -> String {
|
||||
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let before = &caps[1];
|
||||
let classes = &caps[2].replace(",", " ");
|
||||
let after = &caps[3];
|
||||
@@ -551,13 +562,14 @@ fn fix_code_blocks(html: &str) -> String {
|
||||
classes = classes,
|
||||
after = after
|
||||
)
|
||||
}).into_owned()
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
@@ -587,7 +599,8 @@ fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||
// not language-rust, so no-op
|
||||
text.to_owned()
|
||||
}
|
||||
}).into_owned()
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn partition_source(s: &str) -> (String, String) {
|
||||
@@ -597,7 +610,7 @@ fn partition_source(s: &str) -> (String, String) {
|
||||
|
||||
for line in s.lines() {
|
||||
let trimline = line.trim();
|
||||
let header = trimline.chars().all(|c| c.is_whitespace()) || trimline.starts_with("#![");
|
||||
let header = trimline.chars().all(char::is_whitespace) || trimline.starts_with("#![");
|
||||
if !header || after_header {
|
||||
after_header = true;
|
||||
after.push_str(line);
|
||||
@@ -628,27 +641,27 @@ mod tests {
|
||||
let inputs = vec![
|
||||
(
|
||||
"blah blah <h1>Foo</h1>",
|
||||
r##"blah blah <a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
|
||||
r##"blah blah <h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1>",
|
||||
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
|
||||
r##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
|
||||
),
|
||||
(
|
||||
"<h3>Foo^bar</h3>",
|
||||
r##"<a class="header" href="#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
|
||||
r##"<h3><a class="header" href="#foobar" id="foobar">Foo^bar</a></h3>"##,
|
||||
),
|
||||
(
|
||||
"<h4></h4>",
|
||||
r##"<a class="header" href="#" id=""><h4></h4></a>"##,
|
||||
r##"<h4><a class="header" href="#" id=""></a></h4>"##,
|
||||
),
|
||||
(
|
||||
"<h4><em>Hï</em></h4>",
|
||||
r##"<a class="header" href="#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
|
||||
r##"<h4><a class="header" href="#hï" id="hï"><em>Hï</em></a></h4>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1><h3>Foo</h3>",
|
||||
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a><a class="header" href="#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
|
||||
r##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1><h3><a class="header" href="#foo-1" id="foo-1">Foo</a></h3>"##,
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod navigation;
|
||||
pub mod theme;
|
||||
pub mod toc;
|
||||
|
||||
@@ -2,9 +2,8 @@ use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
|
||||
use serde_json;
|
||||
|
||||
use utils;
|
||||
use crate::utils;
|
||||
|
||||
type StringMap = BTreeMap<String, String>;
|
||||
|
||||
@@ -18,13 +17,13 @@ impl Target {
|
||||
/// Returns target if found.
|
||||
fn find(
|
||||
&self,
|
||||
base_path: &String,
|
||||
current_path: &String,
|
||||
base_path: &str,
|
||||
current_path: &str,
|
||||
current_item: &StringMap,
|
||||
previous_item: &StringMap,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
match self {
|
||||
&Target::Next => {
|
||||
match *self {
|
||||
Target::Next => {
|
||||
let previous_path = previous_item
|
||||
.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
|
||||
@@ -34,7 +33,7 @@ impl Target {
|
||||
}
|
||||
}
|
||||
|
||||
&Target::Previous => {
|
||||
Target::Previous => {
|
||||
if current_path == base_path {
|
||||
return Ok(Some(previous_item.clone()));
|
||||
}
|
||||
@@ -47,18 +46,19 @@ impl Target {
|
||||
|
||||
fn find_chapter(
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
rc: &mut RenderContext<'_>,
|
||||
target: Target,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
debug!("Get data from context");
|
||||
|
||||
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
|
||||
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
|
||||
let base_path = rc
|
||||
.evaluate_absolute(ctx, "path", true)?
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
@@ -86,18 +86,19 @@ fn find_chapter(
|
||||
}
|
||||
|
||||
fn render(
|
||||
_h: &Helper,
|
||||
_h: &Helper<'_, '_>,
|
||||
r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
rc: &mut RenderContext<'_>,
|
||||
out: &mut dyn Output,
|
||||
chapter: &StringMap,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("Creating BTreeMap to inject in context");
|
||||
|
||||
let mut context = BTreeMap::new();
|
||||
let base_path = rc
|
||||
.evaluate_absolute(ctx, "path", false)?
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
@@ -137,11 +138,11 @@ fn render(
|
||||
}
|
||||
|
||||
pub fn previous(
|
||||
_h: &Helper,
|
||||
_h: &Helper<'_, '_>,
|
||||
r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
rc: &mut RenderContext<'_>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("previous (handlebars helper)");
|
||||
|
||||
@@ -153,11 +154,11 @@ pub fn previous(
|
||||
}
|
||||
|
||||
pub fn next(
|
||||
_h: &Helper,
|
||||
_h: &Helper<'_, '_>,
|
||||
r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
rc: &mut RenderContext<'_>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("next (handlebars helper)");
|
||||
|
||||
@@ -172,29 +173,29 @@ pub fn next(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
static TEMPLATE: &'static str =
|
||||
static TEMPLATE: &str =
|
||||
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
|
||||
|
||||
#[test]
|
||||
fn test_next_previous() {
|
||||
let data = json!({
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
@@ -209,23 +210,23 @@ mod tests {
|
||||
#[test]
|
||||
fn test_first() {
|
||||
let data = json!({
|
||||
"name": "one",
|
||||
"path": "one.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
"name": "one",
|
||||
"path": "one.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
@@ -239,23 +240,23 @@ mod tests {
|
||||
#[test]
|
||||
fn test_last() {
|
||||
let data = json!({
|
||||
"name": "three",
|
||||
"path": "three.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
"name": "three",
|
||||
"path": "three.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
|
||||
28
src/renderer/html_handlebars/helpers/theme.rs
Normal file
28
src/renderer/html_handlebars/helpers/theme.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
|
||||
|
||||
pub fn theme_option(
|
||||
h: &Helper<'_, '_>,
|
||||
_r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("theme_option (handlebars helper)");
|
||||
|
||||
let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
|
||||
RenderError::new("Param 0 with String type is required for theme_option helper.")
|
||||
})?;
|
||||
|
||||
let default_theme = rc.evaluate(ctx, "@root/default_theme")?;
|
||||
let default_theme_name = default_theme
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `default_theme`, string expected"))?;
|
||||
|
||||
out.write(param)?;
|
||||
if param.to_lowercase() == default_theme_name.to_lowercase() {
|
||||
out.write(" (default)")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use utils;
|
||||
use crate::utils;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
|
||||
use pulldown_cmark::{html, Event, Parser, Tag};
|
||||
use serde_json;
|
||||
use pulldown_cmark::{html, Event, Parser};
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -16,21 +15,22 @@ pub struct RenderToc {
|
||||
impl HelperDef for RenderToc {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
_h: &Helper,
|
||||
_: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
_h: &Helper<'reg, 'rc>,
|
||||
_r: &'reg Handlebars,
|
||||
ctx: &'rc Context,
|
||||
rc: &mut RenderContext<'reg>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
|
||||
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.as_json().clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
let current = rc
|
||||
.evaluate_absolute(ctx, "path", true)?
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
@@ -118,10 +118,7 @@ impl HelperDef for RenderToc {
|
||||
|
||||
// filter all events that are not inline code blocks
|
||||
let parser = Parser::new(name).filter(|event| match *event {
|
||||
Event::Start(Tag::Code)
|
||||
| Event::End(Tag::Code)
|
||||
| Event::InlineHtml(_)
|
||||
| Event::Text(_) => true,
|
||||
Event::Code(_) | Event::InlineHtml(_) | Event::Text(_) => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
extern crate ammonia;
|
||||
extern crate elasticlunr;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use self::elasticlunr::Index;
|
||||
use elasticlunr::Index;
|
||||
use pulldown_cmark::*;
|
||||
use serde_json;
|
||||
|
||||
use book::{Book, BookItem};
|
||||
use config::Search;
|
||||
use errors::*;
|
||||
use theme::searcher;
|
||||
use utils;
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::config::Search;
|
||||
use crate::errors::*;
|
||||
use crate::theme::searcher;
|
||||
use crate::utils;
|
||||
|
||||
/// Creates all files required for search.
|
||||
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
|
||||
@@ -35,7 +31,7 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
"searchindex.js",
|
||||
format!("window.search = {};", index).as_bytes(),
|
||||
format!("Object.assign(window.search, {});", index).as_bytes(),
|
||||
)?;
|
||||
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
||||
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
||||
@@ -54,7 +50,7 @@ fn add_doc(
|
||||
section_id: &Option<String>,
|
||||
items: &[&str],
|
||||
) {
|
||||
let url = if let &Some(ref id) = section_id {
|
||||
let url = if let Some(ref id) = *section_id {
|
||||
Cow::Owned(format!("{}#{}", anchor_base, id))
|
||||
} else {
|
||||
Cow::Borrowed(anchor_base)
|
||||
@@ -74,8 +70,8 @@ fn render_item(
|
||||
doc_urls: &mut Vec<String>,
|
||||
item: &BookItem,
|
||||
) -> Result<()> {
|
||||
let chapter = match item {
|
||||
&BookItem::Chapter(ref ch) => ch,
|
||||
let chapter = match *item {
|
||||
BookItem::Chapter(ref ch) => ch,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
@@ -85,23 +81,21 @@ fn render_item(
|
||||
.chain_err(|| "Could not convert HTML path to str")?;
|
||||
let anchor_base = utils::fs::normalize_path(filepath);
|
||||
|
||||
let mut opts = Options::empty();
|
||||
opts.insert(OPTION_ENABLE_TABLES);
|
||||
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
||||
let p = Parser::new_ext(&chapter.content, opts);
|
||||
let p = utils::new_cmark_parser(&chapter.content);
|
||||
|
||||
let mut in_header = false;
|
||||
let max_section_depth = search_config.heading_split_level as i32;
|
||||
let max_section_depth = i32::from(search_config.heading_split_level);
|
||||
let mut section_id = None;
|
||||
let mut heading = String::new();
|
||||
let mut body = String::new();
|
||||
let mut html_block = String::new();
|
||||
let mut breadcrumbs = chapter.parent_names.clone();
|
||||
let mut footnote_numbers = HashMap::new();
|
||||
|
||||
for event in p {
|
||||
match event {
|
||||
Event::Start(Tag::Header(i)) if i <= max_section_depth => {
|
||||
if heading.len() > 0 {
|
||||
if !heading.is_empty() {
|
||||
// Section finished, the next header is following now
|
||||
// Write the data to the index, and clear it for the next section
|
||||
add_doc(
|
||||
@@ -128,6 +122,13 @@ fn render_item(
|
||||
let number = footnote_numbers.len() + 1;
|
||||
footnote_numbers.entry(name).or_insert(number);
|
||||
}
|
||||
Event::Html(html) => {
|
||||
html_block.push_str(&html);
|
||||
}
|
||||
Event::End(Tag::HtmlBlock) => {
|
||||
body.push_str(&clean_html(&html_block));
|
||||
html_block.clear();
|
||||
}
|
||||
Event::Start(_) | Event::End(_) | Event::SoftBreak | Event::HardBreak => {
|
||||
// Insert spaces where HTML output would usually seperate text
|
||||
// to ensure words don't get merged together
|
||||
@@ -137,14 +138,14 @@ fn render_item(
|
||||
body.push(' ');
|
||||
}
|
||||
}
|
||||
Event::Text(text) => {
|
||||
Event::Text(text) | Event::Code(text) => {
|
||||
if in_header {
|
||||
heading.push_str(&text);
|
||||
} else {
|
||||
body.push_str(&text);
|
||||
}
|
||||
}
|
||||
Event::Html(html) | Event::InlineHtml(html) => {
|
||||
Event::InlineHtml(html) => {
|
||||
body.push_str(&clean_html(&html));
|
||||
}
|
||||
Event::FootnoteReference(name) => {
|
||||
@@ -152,10 +153,11 @@ fn render_item(
|
||||
let number = footnote_numbers.entry(name).or_insert(len);
|
||||
body.push_str(&format!(" [{}] ", number));
|
||||
}
|
||||
Event::TaskListMarker(_checked) => {}
|
||||
}
|
||||
}
|
||||
|
||||
if heading.len() > 0 {
|
||||
if !heading.is_empty() {
|
||||
// Make sure the last section is added to the index
|
||||
add_doc(
|
||||
index,
|
||||
@@ -170,7 +172,7 @@ fn render_item(
|
||||
}
|
||||
|
||||
fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
|
||||
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
|
||||
use elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
@@ -8,25 +8,22 @@
|
||||
//!
|
||||
//! The definition for [RenderContext] may be useful though.
|
||||
//!
|
||||
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/lib/index.html
|
||||
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/for_developers/index.html
|
||||
//! [RenderContext]: struct.RenderContext.html
|
||||
|
||||
pub use self::html_handlebars::HtmlHandlebars;
|
||||
|
||||
mod html_handlebars;
|
||||
|
||||
use serde_json;
|
||||
use shlex::Shlex;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use book::Book;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
|
||||
const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
|
||||
/// An arbitrary `mdbook` backend.
|
||||
///
|
||||
@@ -66,6 +63,8 @@ pub struct RenderContext {
|
||||
/// renderers to cache intermediate results, this directory is not
|
||||
/// guaranteed to be empty or even exist.
|
||||
pub destination: PathBuf,
|
||||
#[serde(skip)]
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
impl RenderContext {
|
||||
@@ -76,11 +75,12 @@ impl RenderContext {
|
||||
Q: Into<PathBuf>,
|
||||
{
|
||||
RenderContext {
|
||||
book: book,
|
||||
config: config,
|
||||
version: MDBOOK_VERSION.to_string(),
|
||||
book,
|
||||
config,
|
||||
version: crate::MDBOOK_VERSION.to_string(),
|
||||
root: root.into(),
|
||||
destination: destination.into(),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 348 KiB After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -69,3 +69,11 @@ Original by Dempfi (https://github.com/dempfi/ayu)
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #91b362;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #d96c75;
|
||||
}
|
||||
|
||||
@@ -101,11 +101,15 @@ function playpen_text(playpen) {
|
||||
}
|
||||
|
||||
let text = playpen_text(code_block);
|
||||
let classes = code_block.querySelector('code').classList;
|
||||
let has_2018 = classes.contains("edition2018");
|
||||
let edition = has_2018 ? "2018" : "2015";
|
||||
|
||||
var params = {
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
code: text
|
||||
code: text,
|
||||
edition: edition
|
||||
};
|
||||
|
||||
if (text.indexOf("#![feature") !== -1) {
|
||||
@@ -330,13 +334,11 @@ function playpen_text(playpen) {
|
||||
stylesheets.ayuHighlight.disabled = false;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = true;
|
||||
|
||||
ace_theme = "ace/theme/tomorrow_night";
|
||||
} else {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = false;
|
||||
|
||||
ace_theme = "ace/theme/dawn";
|
||||
}
|
||||
|
||||
@@ -352,7 +354,7 @@ function playpen_text(playpen) {
|
||||
|
||||
var previousTheme;
|
||||
try { previousTheme = localStorage.getItem('mdbook-theme'); } catch (e) { }
|
||||
if (previousTheme === null || previousTheme === undefined) { previousTheme = 'light'; }
|
||||
if (previousTheme === null || previousTheme === undefined) { previousTheme = default_theme; }
|
||||
|
||||
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
|
||||
|
||||
@@ -364,7 +366,7 @@ function playpen_text(playpen) {
|
||||
// Set theme
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||
|
||||
set_theme(theme);
|
||||
|
||||
@@ -435,6 +437,7 @@ function playpen_text(playpen) {
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
var sidebarToggleButton = document.getElementById("sidebar-toggle");
|
||||
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
|
||||
var firstContact = null;
|
||||
|
||||
function showSidebar() {
|
||||
@@ -474,6 +477,23 @@ function playpen_text(playpen) {
|
||||
}
|
||||
});
|
||||
|
||||
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
|
||||
|
||||
function initResize(e) {
|
||||
window.addEventListener('mousemove', resize, false);
|
||||
window.addEventListener('mouseup', stopResize, false);
|
||||
html.classList.add('sidebar-resizing');
|
||||
}
|
||||
function resize(e) {
|
||||
document.documentElement.style.setProperty('--sidebar-width', (e.clientX - sidebar.offsetLeft) + 'px');
|
||||
}
|
||||
//on mouseup remove windows functions mousemove & mouseup
|
||||
function stopResize(e) {
|
||||
html.classList.remove('sidebar-resizing');
|
||||
window.removeEventListener('mousemove', resize, false);
|
||||
window.removeEventListener('mouseup', stopResize, false);
|
||||
}
|
||||
|
||||
document.addEventListener('touchstart', function (e) {
|
||||
firstContact = {
|
||||
x: e.touches[0].clientX,
|
||||
@@ -543,7 +563,7 @@ function playpen_text(playpen) {
|
||||
elem.className = 'fa fa-copy tooltipped';
|
||||
}
|
||||
|
||||
var clipboardSnippets = new Clipboard('.clip-button', {
|
||||
var clipboardSnippets = new ClipboardJS('.clip-button', {
|
||||
text: function (trigger) {
|
||||
hideTooltip(trigger);
|
||||
let playpen = trigger.closest("pre");
|
||||
|
||||
6
src/theme/clipboard.min.js
vendored
6
src/theme/clipboard.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -63,9 +63,12 @@ a > .hljs {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#print-button {
|
||||
.right-buttons {
|
||||
margin: 0 15px;
|
||||
}
|
||||
.right-buttons a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
|
||||
transform: translateY(-60px);
|
||||
@@ -75,7 +78,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||
display: flex;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.no-js .left-buttons {
|
||||
.no-js .left-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -142,7 +145,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-nav-chapters {
|
||||
.mobile-nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
@@ -177,7 +180,11 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:not(pre):not(a) > .hljs {
|
||||
color: var(--inline-code-color);
|
||||
overflow-x: initial;
|
||||
}
|
||||
|
||||
a:hover > .hljs {
|
||||
@@ -298,8 +305,6 @@ ul#searchresults span.teaser em {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--sidebar-width);
|
||||
overflow-y: auto;
|
||||
padding: 10px 10px;
|
||||
font-size: 0.875em;
|
||||
box-sizing: border-box;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -307,12 +312,39 @@ ul#searchresults span.teaser em {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.js .sidebar {
|
||||
.sidebar-resizing {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.js:not(.sidebar-resizing) .sidebar {
|
||||
transition: transform 0.3s; /* Animation: slide away */
|
||||
}
|
||||
.sidebar code {
|
||||
line-height: 2em;
|
||||
}
|
||||
.sidebar .sidebar-scrollbox {
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
.sidebar .sidebar-resize-handle {
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
width: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.js .sidebar .sidebar-resize-handle {
|
||||
cursor: col-resize;
|
||||
width: 5px;
|
||||
}
|
||||
.sidebar-hidden .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width)));
|
||||
}
|
||||
@@ -342,15 +374,17 @@ ul#searchresults span.teaser em {
|
||||
color: var(--sidebar-non-existant);
|
||||
}
|
||||
.chapter li a {
|
||||
color: var(--sidebar-fg);
|
||||
display: block;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.chapter li a:hover { text-decoration: none }
|
||||
.chapter li .active,
|
||||
a:hover {
|
||||
/* Animate color change */
|
||||
|
||||
.chapter li a:hover {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.chapter li .active {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
@@ -363,7 +397,7 @@ a:hover {
|
||||
background-color: var(--sidebar-spacer);
|
||||
}
|
||||
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
.chapter li a { padding: 5px 0; }
|
||||
.spacer { margin: 10px 0; }
|
||||
}
|
||||
|
||||
@@ -30,14 +30,14 @@ h4, h5 { margin-top: 2em; }
|
||||
|
||||
.header + .header h3,
|
||||
.header + .header h4,
|
||||
.header + .header h5 {
|
||||
.header + .header h5 {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
a.header:target h1:before,
|
||||
a.header:target h2:before,
|
||||
a.header:target h3:before,
|
||||
a.header:target h4:before {
|
||||
h1 a.header:target::before,
|
||||
h2 a.header:target::before,
|
||||
h3 a.header:target::before,
|
||||
h4 a.header:target::before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-left: -30px;
|
||||
@@ -51,7 +51,7 @@ a.header:target h4:before {
|
||||
.page-wrapper {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.js .page-wrapper {
|
||||
.js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
|
||||
@@ -141,4 +141,3 @@ blockquote {
|
||||
.tooltipped .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@@ -67,3 +67,13 @@
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #22863a;
|
||||
background-color: #f0fff4;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #b31d28;
|
||||
background-color: #ffeef0;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,6 +4,10 @@
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
{{#if is_print }}
|
||||
<meta name="robots" content="noindex" />
|
||||
{{/if}}
|
||||
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="description" content="{{ description }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -27,7 +31,7 @@
|
||||
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ this }}">
|
||||
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
|
||||
{{/each}}
|
||||
|
||||
{{#if mathjax_support}}
|
||||
@@ -35,9 +39,12 @@
|
||||
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{{/if}}
|
||||
</head>
|
||||
<body class="light">
|
||||
<body class="{{ default_theme }}">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script type="text/javascript">var path_to_root = "{{ path_to_root }}";</script>
|
||||
<script type="text/javascript">
|
||||
var path_to_root = "{{ path_to_root }}";
|
||||
var default_theme = "{{ default_theme }}";
|
||||
</script>
|
||||
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script type="text/javascript">
|
||||
@@ -58,8 +65,8 @@
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
<script type="text/javascript">
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||
document.body.className = theme;
|
||||
document.querySelector('html').className = theme + ' js';
|
||||
</script>
|
||||
@@ -77,7 +84,10 @@
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
{{#toc}}{{/toc}}
|
||||
<div class="sidebar-scrollbox">
|
||||
{{#toc}}{{/toc}}
|
||||
</div>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||
</nav>
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
@@ -94,11 +104,11 @@
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
</button>
|
||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="light">Light <span class="default">(default)</span></button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="light">{{ theme_option "Light" }}</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="rust">{{ theme_option "Rust" }}</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="coal">{{ theme_option "Coal" }}</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="navy">{{ theme_option "Navy" }}</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="ayu">{{ theme_option "Ayu" }}</button></li>
|
||||
</ul>
|
||||
{{#if search_enabled}}
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||
@@ -107,12 +117,17 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
|
||||
<div class="right-buttons">
|
||||
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
|
||||
<i id="print-button" class="fa fa-print"></i>
|
||||
</a>
|
||||
{{#if git_repository_url}}
|
||||
<a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
|
||||
<i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,7 +250,7 @@
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
{{#each additional_js}}
|
||||
<script type="text/javascript" src="{{ path_to_root }}{{this}}"></script>
|
||||
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
{{#if is_print}}
|
||||
|
||||
@@ -9,33 +9,29 @@ use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
||||
use errors::*;
|
||||
use crate::errors::*;
|
||||
|
||||
pub static INDEX: &'static [u8] = include_bytes!("index.hbs");
|
||||
pub static HEADER: &'static [u8] = include_bytes!("header.hbs");
|
||||
pub static CHROME_CSS: &'static [u8] = include_bytes!("css/chrome.css");
|
||||
pub static GENERAL_CSS: &'static [u8] = include_bytes!("css/general.css");
|
||||
pub static PRINT_CSS: &'static [u8] = include_bytes!("css/print.css");
|
||||
pub static VARIABLES_CSS: &'static [u8] = include_bytes!("css/variables.css");
|
||||
pub static FAVICON: &'static [u8] = include_bytes!("favicon.png");
|
||||
pub static JS: &'static [u8] = include_bytes!("book.js");
|
||||
pub static HIGHLIGHT_JS: &'static [u8] = include_bytes!("highlight.js");
|
||||
pub static TOMORROW_NIGHT_CSS: &'static [u8] = include_bytes!("tomorrow-night.css");
|
||||
pub static HIGHLIGHT_CSS: &'static [u8] = include_bytes!("highlight.css");
|
||||
pub static AYU_HIGHLIGHT_CSS: &'static [u8] = include_bytes!("ayu-highlight.css");
|
||||
pub static CLIPBOARD_JS: &'static [u8] = include_bytes!("clipboard.min.js");
|
||||
pub static FONT_AWESOME: &'static [u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
|
||||
pub static FONT_AWESOME_EOT: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
|
||||
pub static FONT_AWESOME_SVG: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
|
||||
pub static FONT_AWESOME_WOFF2: &'static [u8] =
|
||||
pub static INDEX: &[u8] = include_bytes!("index.hbs");
|
||||
pub static HEADER: &[u8] = include_bytes!("header.hbs");
|
||||
pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
|
||||
pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
|
||||
pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
|
||||
pub static VARIABLES_CSS: &[u8] = include_bytes!("css/variables.css");
|
||||
pub static FAVICON: &[u8] = include_bytes!("favicon.png");
|
||||
pub static JS: &[u8] = include_bytes!("book.js");
|
||||
pub static HIGHLIGHT_JS: &[u8] = include_bytes!("highlight.js");
|
||||
pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("tomorrow-night.css");
|
||||
pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("highlight.css");
|
||||
pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("ayu-highlight.css");
|
||||
pub static CLIPBOARD_JS: &[u8] = include_bytes!("clipboard.min.js");
|
||||
pub static FONT_AWESOME: &[u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
|
||||
pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
|
||||
pub static FONT_AWESOME_SVG: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
|
||||
pub static FONT_AWESOME_WOFF2: &[u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff2");
|
||||
pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
|
||||
pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
|
||||
|
||||
/// The `Theme` struct should be used instead of the static variables because
|
||||
/// the `new()` method will look if the user has a theme directory in their
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -13,7 +13,7 @@ window.editors = [];
|
||||
showLineNumbers: false,
|
||||
showGutter: false,
|
||||
maxLines: Infinity,
|
||||
fontSize: "0.875em" // please adjust the font size of the code in general.styl
|
||||
fontSize: "0.875em" // please adjust the font size of the code in general.css
|
||||
});
|
||||
|
||||
editor.$blockScrolling = Infinity;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Theme dependencies for the playpen editor.
|
||||
|
||||
pub static JS: &'static [u8] = include_bytes!("editor.js");
|
||||
pub static ACE_JS: &'static [u8] = include_bytes!("ace.js");
|
||||
pub static MODE_RUST_JS: &'static [u8] = include_bytes!("mode-rust.js");
|
||||
pub static THEME_DAWN_JS: &'static [u8] = include_bytes!("theme-dawn.js");
|
||||
pub static THEME_TOMORROW_NIGHT_JS: &'static [u8] = include_bytes!("theme-tomorrow_night.js");
|
||||
pub static JS: &[u8] = include_bytes!("editor.js");
|
||||
pub static ACE_JS: &[u8] = include_bytes!("ace.js");
|
||||
pub static MODE_RUST_JS: &[u8] = include_bytes!("mode-rust.js");
|
||||
pub static THEME_DAWN_JS: &[u8] = include_bytes!("theme-dawn.js");
|
||||
pub static THEME_TOMORROW_NIGHT_JS: &[u8] = include_bytes!("theme-tomorrow_night.js");
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1,7 @@
|
||||
ace.define("ace/theme/dawn",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-dawn",t.cssText=".ace-dawn .ace_gutter {background: #ebebeb;color: #333}.ace-dawn .ace_print-margin {width: 1px;background: #e8e8e8}.ace-dawn {background-color: #F9F9F9;color: #080808}.ace-dawn .ace_cursor {color: #000000}.ace-dawn .ace_marker-layer .ace_selection {background: rgba(39, 95, 255, 0.30)}.ace-dawn.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #F9F9F9;}.ace-dawn .ace_marker-layer .ace_step {background: rgb(255, 255, 0)}.ace-dawn .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(75, 75, 126, 0.50)}.ace-dawn .ace_marker-layer .ace_active-line {background: rgba(36, 99, 180, 0.12)}.ace-dawn .ace_gutter-active-line {background-color : #dcdcdc}.ace-dawn .ace_marker-layer .ace_selected-word {border: 1px solid rgba(39, 95, 255, 0.30)}.ace-dawn .ace_invisible {color: rgba(75, 75, 126, 0.50)}.ace-dawn .ace_keyword,.ace-dawn .ace_meta {color: #794938}.ace-dawn .ace_constant,.ace-dawn .ace_constant.ace_character,.ace-dawn .ace_constant.ace_character.ace_escape,.ace-dawn .ace_constant.ace_other {color: #811F24}.ace-dawn .ace_invalid.ace_illegal {text-decoration: underline;font-style: italic;color: #F8F8F8;background-color: #B52A1D}.ace-dawn .ace_invalid.ace_deprecated {text-decoration: underline;font-style: italic;color: #B52A1D}.ace-dawn .ace_support {color: #691C97}.ace-dawn .ace_support.ace_constant {color: #B4371F}.ace-dawn .ace_fold {background-color: #794938;border-color: #080808}.ace-dawn .ace_list,.ace-dawn .ace_markup.ace_list,.ace-dawn .ace_support.ace_function {color: #693A17}.ace-dawn .ace_storage {font-style: italic;color: #A71D5D}.ace-dawn .ace_string {color: #0B6125}.ace-dawn .ace_string.ace_regexp {color: #CF5628}.ace-dawn .ace_comment {font-style: italic;color: #5A525F}.ace-dawn .ace_heading,.ace-dawn .ace_markup.ace_heading {color: #19356D}.ace-dawn .ace_variable {color: #234A97}.ace-dawn .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYLh/5+x/AAizA4hxNNsZAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)})
|
||||
ace.define("ace/theme/dawn",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-dawn",t.cssText=".ace-dawn .ace_gutter {background: #ebebeb;color: #333}.ace-dawn .ace_print-margin {width: 1px;background: #e8e8e8}.ace-dawn {background-color: #F9F9F9;color: #080808}.ace-dawn .ace_cursor {color: #000000}.ace-dawn .ace_marker-layer .ace_selection {background: rgba(39, 95, 255, 0.30)}.ace-dawn.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #F9F9F9;}.ace-dawn .ace_marker-layer .ace_step {background: rgb(255, 255, 0)}.ace-dawn .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(75, 75, 126, 0.50)}.ace-dawn .ace_marker-layer .ace_active-line {background: rgba(36, 99, 180, 0.12)}.ace-dawn .ace_gutter-active-line {background-color : #dcdcdc}.ace-dawn .ace_marker-layer .ace_selected-word {border: 1px solid rgba(39, 95, 255, 0.30)}.ace-dawn .ace_invisible {color: rgba(75, 75, 126, 0.50)}.ace-dawn .ace_keyword,.ace-dawn .ace_meta {color: #794938}.ace-dawn .ace_constant,.ace-dawn .ace_constant.ace_character,.ace-dawn .ace_constant.ace_character.ace_escape,.ace-dawn .ace_constant.ace_other {color: #811F24}.ace-dawn .ace_invalid.ace_illegal {text-decoration: underline;font-style: italic;color: #F8F8F8;background-color: #B52A1D}.ace-dawn .ace_invalid.ace_deprecated {text-decoration: underline;font-style: italic;color: #B52A1D}.ace-dawn .ace_support {color: #691C97}.ace-dawn .ace_support.ace_constant {color: #B4371F}.ace-dawn .ace_fold {background-color: #794938;border-color: #080808}.ace-dawn .ace_list,.ace-dawn .ace_markup.ace_list,.ace-dawn .ace_support.ace_function {color: #693A17}.ace-dawn .ace_storage {font-style: italic;color: #A71D5D}.ace-dawn .ace_string {color: #0B6125}.ace-dawn .ace_string.ace_regexp {color: #CF5628}.ace-dawn .ace_comment {font-style: italic;color: #5A525F}.ace-dawn .ace_heading,.ace-dawn .ace_markup.ace_heading {color: #19356D}.ace-dawn .ace_variable {color: #234A97}.ace-dawn .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYLh/5+x/AAizA4hxNNsZAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}); (function() {
|
||||
ace.require(["ace/theme/dawn"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
ace.define("ace/theme/tomorrow_night",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-tomorrow-night",t.cssText=".ace-tomorrow-night .ace_gutter {background: #25282c;color: #C5C8C6}.ace-tomorrow-night .ace_print-margin {width: 1px;background: #25282c}.ace-tomorrow-night {background-color: #1D1F21;color: #C5C8C6}.ace-tomorrow-night .ace_cursor {color: #AEAFAD}.ace-tomorrow-night .ace_marker-layer .ace_selection {background: #373B41}.ace-tomorrow-night.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #1D1F21;}.ace-tomorrow-night .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-tomorrow-night .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #4B4E55}.ace-tomorrow-night .ace_marker-layer .ace_active-line {background: #282A2E}.ace-tomorrow-night .ace_gutter-active-line {background-color: #282A2E}.ace-tomorrow-night .ace_marker-layer .ace_selected-word {border: 1px solid #373B41}.ace-tomorrow-night .ace_invisible {color: #4B4E55}.ace-tomorrow-night .ace_keyword,.ace-tomorrow-night .ace_meta,.ace-tomorrow-night .ace_storage,.ace-tomorrow-night .ace_storage.ace_type,.ace-tomorrow-night .ace_support.ace_type {color: #B294BB}.ace-tomorrow-night .ace_keyword.ace_operator {color: #8ABEB7}.ace-tomorrow-night .ace_constant.ace_character,.ace-tomorrow-night .ace_constant.ace_language,.ace-tomorrow-night .ace_constant.ace_numeric,.ace-tomorrow-night .ace_keyword.ace_other.ace_unit,.ace-tomorrow-night .ace_support.ace_constant,.ace-tomorrow-night .ace_variable.ace_parameter {color: #DE935F}.ace-tomorrow-night .ace_constant.ace_other {color: #CED1CF}.ace-tomorrow-night .ace_invalid {color: #CED2CF;background-color: #DF5F5F}.ace-tomorrow-night .ace_invalid.ace_deprecated {color: #CED2CF;background-color: #B798BF}.ace-tomorrow-night .ace_fold {background-color: #81A2BE;border-color: #C5C8C6}.ace-tomorrow-night .ace_entity.ace_name.ace_function,.ace-tomorrow-night .ace_support.ace_function,.ace-tomorrow-night .ace_variable {color: #81A2BE}.ace-tomorrow-night .ace_support.ace_class,.ace-tomorrow-night .ace_support.ace_type {color: #F0C674}.ace-tomorrow-night .ace_heading,.ace-tomorrow-night .ace_markup.ace_heading,.ace-tomorrow-night .ace_string {color: #B5BD68}.ace-tomorrow-night .ace_entity.ace_name.ace_tag,.ace-tomorrow-night .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow-night .ace_meta.ace_tag,.ace-tomorrow-night .ace_string.ace_regexp,.ace-tomorrow-night .ace_variable {color: #CC6666}.ace-tomorrow-night .ace_comment {color: #969896}.ace-tomorrow-night .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHB3d/8PAAOIAdULw8qMAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)})
|
||||
ace.define("ace/theme/tomorrow_night",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-tomorrow-night",t.cssText=".ace-tomorrow-night .ace_gutter {background: #25282c;color: #C5C8C6}.ace-tomorrow-night .ace_print-margin {width: 1px;background: #25282c}.ace-tomorrow-night {background-color: #1D1F21;color: #C5C8C6}.ace-tomorrow-night .ace_cursor {color: #AEAFAD}.ace-tomorrow-night .ace_marker-layer .ace_selection {background: #373B41}.ace-tomorrow-night.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #1D1F21;}.ace-tomorrow-night .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-tomorrow-night .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #4B4E55}.ace-tomorrow-night .ace_marker-layer .ace_active-line {background: #282A2E}.ace-tomorrow-night .ace_gutter-active-line {background-color: #282A2E}.ace-tomorrow-night .ace_marker-layer .ace_selected-word {border: 1px solid #373B41}.ace-tomorrow-night .ace_invisible {color: #4B4E55}.ace-tomorrow-night .ace_keyword,.ace-tomorrow-night .ace_meta,.ace-tomorrow-night .ace_storage,.ace-tomorrow-night .ace_storage.ace_type,.ace-tomorrow-night .ace_support.ace_type {color: #B294BB}.ace-tomorrow-night .ace_keyword.ace_operator {color: #8ABEB7}.ace-tomorrow-night .ace_constant.ace_character,.ace-tomorrow-night .ace_constant.ace_language,.ace-tomorrow-night .ace_constant.ace_numeric,.ace-tomorrow-night .ace_keyword.ace_other.ace_unit,.ace-tomorrow-night .ace_support.ace_constant,.ace-tomorrow-night .ace_variable.ace_parameter {color: #DE935F}.ace-tomorrow-night .ace_constant.ace_other {color: #CED1CF}.ace-tomorrow-night .ace_invalid {color: #CED2CF;background-color: #DF5F5F}.ace-tomorrow-night .ace_invalid.ace_deprecated {color: #CED2CF;background-color: #B798BF}.ace-tomorrow-night .ace_fold {background-color: #81A2BE;border-color: #C5C8C6}.ace-tomorrow-night .ace_entity.ace_name.ace_function,.ace-tomorrow-night .ace_support.ace_function,.ace-tomorrow-night .ace_variable {color: #81A2BE}.ace-tomorrow-night .ace_support.ace_class,.ace-tomorrow-night .ace_support.ace_type {color: #F0C674}.ace-tomorrow-night .ace_heading,.ace-tomorrow-night .ace_markup.ace_heading,.ace-tomorrow-night .ace_string {color: #B5BD68}.ace-tomorrow-night .ace_entity.ace_name.ace_tag,.ace-tomorrow-night .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow-night .ace_meta.ace_tag,.ace-tomorrow-night .ace_string.ace_regexp,.ace-tomorrow-night .ace_variable {color: #CC6666}.ace-tomorrow-night .ace_comment {color: #969896}.ace-tomorrow-night .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHB3d/8PAAOIAdULw8qMAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}); (function() {
|
||||
ace.require(["ace/theme/tomorrow_night"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Theme dependencies for in-browser search. Not included in mdbook when
|
||||
//! the "search" cargo feature is disabled.
|
||||
|
||||
pub static JS: &'static [u8] = include_bytes!("searcher.js");
|
||||
pub static MARK_JS: &'static [u8] = include_bytes!("mark.min.js");
|
||||
pub static ELASTICLUNR_JS: &'static [u8] = include_bytes!("elasticlunr.min.js");
|
||||
pub static JS: &[u8] = include_bytes!("searcher.js");
|
||||
pub static MARK_JS: &[u8] = include_bytes!("mark.min.js");
|
||||
pub static ELASTICLUNR_JS: &[u8] = include_bytes!("elasticlunr.min.js");
|
||||
|
||||
@@ -94,3 +94,11 @@
|
||||
.xml .hljs-cdata {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #718c00;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #c82829;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
use errors::*;
|
||||
use crate::errors::*;
|
||||
use std::convert::Into;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::io::Write;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
/// Takes a path to a file and try to read the file into a String
|
||||
pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let mut content = String::new();
|
||||
File::open(path)
|
||||
.chain_err(|| "Unable to open the file")?
|
||||
.read_to_string(&mut content)
|
||||
.chain_err(|| "Unable to read the file")?;
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Naively replaces any path seperator with a forward-slash '/'
|
||||
pub fn normalize_path(path: &str) -> String {
|
||||
use std::path::is_separator;
|
||||
@@ -28,7 +16,7 @@ pub fn normalize_path(path: &str) -> String {
|
||||
pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8]) -> Result<()> {
|
||||
let path = build_dir.join(filename);
|
||||
|
||||
create_file(&path)?.write_all(content).map_err(|e| e.into())
|
||||
create_file(&path)?.write_all(content).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Takes a path and returns a path containing just enough `../` to point to
|
||||
@@ -38,8 +26,6 @@ pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8])
|
||||
/// directory from where the path starts.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate mdbook;
|
||||
/// #
|
||||
/// # use std::path::Path;
|
||||
/// # use mdbook::utils::fs::path_to_root;
|
||||
/// #
|
||||
@@ -85,7 +71,7 @@ pub fn create_file(path: &Path) -> Result<File> {
|
||||
fs::create_dir_all(p)?;
|
||||
}
|
||||
|
||||
File::create(path).map_err(|e| e.into())
|
||||
File::create(path).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Removes all the content of a directory but not the directory itself
|
||||
@@ -187,8 +173,6 @@ pub fn copy_files_except_ext(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate tempfile;
|
||||
|
||||
use super::copy_files_except_ext;
|
||||
use std::fs;
|
||||
|
||||
@@ -196,43 +180,44 @@ mod tests {
|
||||
fn copy_files_except_ext_test() {
|
||||
let tmp = match tempfile::TempDir::new() {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic!("Could not create a temp dir"),
|
||||
Err(e) => panic!("Could not create a temp dir: {}", e),
|
||||
};
|
||||
|
||||
// Create a couple of files
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.txt")) {
|
||||
panic!("Could not create file.txt")
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("file.txt")) {
|
||||
panic!("Could not create file.txt: {}", err);
|
||||
}
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.md")) {
|
||||
panic!("Could not create file.md")
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("file.md")) {
|
||||
panic!("Could not create file.md: {}", err);
|
||||
}
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.png")) {
|
||||
panic!("Could not create file.png")
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("file.png")) {
|
||||
panic!("Could not create file.png: {}", err);
|
||||
}
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir")) {
|
||||
panic!("Could not create sub_dir")
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir")) {
|
||||
panic!("Could not create sub_dir: {}", err);
|
||||
}
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir/file.png")) {
|
||||
panic!("Could not create sub_dir/file.png")
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("sub_dir/file.png")) {
|
||||
panic!("Could not create sub_dir/file.png: {}", err);
|
||||
}
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir_exists")) {
|
||||
panic!("Could not create sub_dir_exists")
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir_exists")) {
|
||||
panic!("Could not create sub_dir_exists: {}", err);
|
||||
}
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) {
|
||||
panic!("Could not create sub_dir_exists/file.txt")
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) {
|
||||
panic!("Could not create sub_dir_exists/file.txt: {}", err);
|
||||
}
|
||||
|
||||
// Create output dir
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("output")) {
|
||||
panic!("Could not create output")
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("output")) {
|
||||
panic!("Could not create output: {}", err);
|
||||
}
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) {
|
||||
panic!("Could not create output/sub_dir_exists")
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) {
|
||||
panic!("Could not create output/sub_dir_exists: {}", err);
|
||||
}
|
||||
|
||||
match copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"]) {
|
||||
Err(e) => panic!("Error while executing the function:\n{:?}", e),
|
||||
Ok(_) => {}
|
||||
if let Err(e) =
|
||||
copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"])
|
||||
{
|
||||
panic!("Error while executing the function:\n{:?}", e);
|
||||
}
|
||||
|
||||
// Check if the correct files where created
|
||||
|
||||
195
src/utils/mod.rs
195
src/utils/mod.rs
@@ -2,28 +2,29 @@
|
||||
|
||||
pub mod fs;
|
||||
mod string;
|
||||
use errors::Error;
|
||||
use crate::errors::Error;
|
||||
use regex::Regex;
|
||||
|
||||
use pulldown_cmark::{
|
||||
html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES, OPTION_ENABLE_TABLES,
|
||||
};
|
||||
use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
|
||||
pub use self::string::{take_lines, RangeArgument};
|
||||
pub use self::string::{take_anchored_lines, take_lines};
|
||||
|
||||
/// Replaces multiple consecutive whitespace characters with a single space character.
|
||||
pub fn collapse_whitespace<'a>(text: &'a str) -> Cow<'a, str> {
|
||||
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"\s\s+").unwrap();
|
||||
}
|
||||
RE.replace_all(text, " ")
|
||||
}
|
||||
|
||||
/// Convert the given string to a valid HTML element ID
|
||||
/// Convert the given string to a valid HTML element ID.
|
||||
/// The only restriction is that the ID must not contain any ASCII whitespace.
|
||||
pub fn normalize_id(content: &str) -> String {
|
||||
let mut ret = content
|
||||
content
|
||||
.chars()
|
||||
.filter_map(|ch| {
|
||||
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
|
||||
@@ -33,16 +34,8 @@ pub fn normalize_id(content: &str) -> String {
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect::<String>();
|
||||
// Ensure that the first character is [A-Za-z]
|
||||
if ret
|
||||
.chars()
|
||||
.next()
|
||||
.map_or(false, |c| !c.is_ascii_alphabetic())
|
||||
{
|
||||
ret.insert(0, 'a');
|
||||
}
|
||||
ret
|
||||
})
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
/// Generate an ID for use with anchors which is derived from a "normalised"
|
||||
@@ -69,50 +62,125 @@ pub fn id_from_content(content: &str) -> String {
|
||||
}
|
||||
|
||||
// Remove spaces and hashes indicating a header
|
||||
let trimmed = content.trim().trim_left_matches('#').trim();
|
||||
let trimmed = content.trim().trim_start_matches('#').trim();
|
||||
|
||||
normalize_id(trimmed)
|
||||
}
|
||||
|
||||
fn adjust_links(event: Event) -> Event {
|
||||
/// Fix links to the correct location.
|
||||
///
|
||||
/// This adjusts links, such as turning `.md` extensions to `.html`.
|
||||
///
|
||||
/// `path` is the path to the page being rendered relative to the root of the
|
||||
/// book. This is used for the `print.html` page so that links on the print
|
||||
/// page go to the original location. Normal page rendering sets `path` to
|
||||
/// None. Ideally, print page links would link to anchors on the print page,
|
||||
/// but that is very difficult.
|
||||
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
lazy_static! {
|
||||
static ref HTTP_LINK: Regex = Regex::new("^https?://").unwrap();
|
||||
static ref MD_LINK: Regex = Regex::new("(?P<link>.*).md(?P<anchor>#.*)?").unwrap();
|
||||
static ref SCHEME_LINK: Regex = Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap();
|
||||
static ref MD_LINK: Regex = Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap();
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Start(Tag::Link(dest, title)) => {
|
||||
if !HTTP_LINK.is_match(&dest) {
|
||||
if let Some(caps) = MD_LINK.captures(&dest) {
|
||||
let mut html_link = [&caps["link"], ".html"].concat();
|
||||
|
||||
if let Some(anchor) = caps.name("anchor") {
|
||||
html_link.push_str(anchor.as_str());
|
||||
}
|
||||
|
||||
return Event::Start(Tag::Link(Cow::from(html_link), title));
|
||||
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
|
||||
if dest.starts_with('#') {
|
||||
// Fragment-only link.
|
||||
if let Some(path) = path {
|
||||
let mut base = path.display().to_string();
|
||||
if base.ends_with(".md") {
|
||||
base.replace_range(base.len() - 3.., ".html");
|
||||
}
|
||||
return format!("{}{}", base, dest).into();
|
||||
} else {
|
||||
return dest;
|
||||
}
|
||||
}
|
||||
// Don't modify links with schemes like `https`.
|
||||
if !SCHEME_LINK.is_match(&dest) {
|
||||
// This is a relative link, adjust it as necessary.
|
||||
let mut fixed_link = String::new();
|
||||
if let Some(path) = path {
|
||||
let base = path
|
||||
.parent()
|
||||
.expect("path can't be empty")
|
||||
.to_str()
|
||||
.expect("utf-8 paths only");
|
||||
if !base.is_empty() {
|
||||
write!(fixed_link, "{}/", base).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Event::Start(Tag::Link(dest, title))
|
||||
if let Some(caps) = MD_LINK.captures(&dest) {
|
||||
fixed_link.push_str(&caps["link"]);
|
||||
fixed_link.push_str(".html");
|
||||
if let Some(anchor) = caps.name("anchor") {
|
||||
fixed_link.push_str(anchor.as_str());
|
||||
}
|
||||
} else {
|
||||
fixed_link.push_str(&dest);
|
||||
};
|
||||
return CowStr::from(fixed_link);
|
||||
}
|
||||
dest
|
||||
}
|
||||
|
||||
fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
|
||||
// This is a terrible hack, but should be reasonably reliable. Nobody
|
||||
// should ever parse a tag with a regex. However, there isn't anything
|
||||
// in Rust that I know of that is suitable for handling partial html
|
||||
// fragments like those generated by pulldown_cmark.
|
||||
//
|
||||
// There are dozens of HTML tags/attributes that contain paths, so
|
||||
// feel free to add more tags if desired; these are the only ones I
|
||||
// care about right now.
|
||||
lazy_static! {
|
||||
static ref HTML_LINK: Regex =
|
||||
Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap();
|
||||
}
|
||||
|
||||
HTML_LINK
|
||||
.replace_all(&html, |caps: ®ex::Captures<'_>| {
|
||||
let fixed = fix(caps[2].into(), path);
|
||||
format!("{}{}\"", &caps[1], fixed)
|
||||
})
|
||||
.into_owned()
|
||||
.into()
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Start(Tag::Link(link_type, dest, title)) => {
|
||||
Event::Start(Tag::Link(link_type, fix(dest, path), title))
|
||||
}
|
||||
Event::Start(Tag::Image(link_type, dest, title)) => {
|
||||
Event::Start(Tag::Image(link_type, fix(dest, path), title))
|
||||
}
|
||||
Event::Html(html) => Event::Html(fix_html(html, path)),
|
||||
Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)),
|
||||
_ => event,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
|
||||
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
||||
let mut s = String::with_capacity(text.len() * 3 / 2);
|
||||
render_markdown_with_path(text, curly_quotes, None)
|
||||
}
|
||||
|
||||
pub fn new_cmark_parser(text: &str) -> Parser<'_> {
|
||||
let mut opts = Options::empty();
|
||||
opts.insert(OPTION_ENABLE_TABLES);
|
||||
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
||||
opts.insert(Options::ENABLE_TABLES);
|
||||
opts.insert(Options::ENABLE_FOOTNOTES);
|
||||
opts.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
opts.insert(Options::ENABLE_TASKLISTS);
|
||||
Parser::new_ext(text, opts)
|
||||
}
|
||||
|
||||
let p = Parser::new_ext(text, opts);
|
||||
pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&Path>) -> String {
|
||||
let mut s = String::with_capacity(text.len() * 3 / 2);
|
||||
let p = new_cmark_parser(text);
|
||||
let mut converter = EventQuoteConverter::new(curly_quotes);
|
||||
let events = p
|
||||
.map(clean_codeblock_headers)
|
||||
.map(adjust_links)
|
||||
.map(|event| adjust_links(event, path))
|
||||
.map(|event| converter.convert(event));
|
||||
|
||||
html::push_html(&mut s, events);
|
||||
@@ -127,7 +195,7 @@ struct EventQuoteConverter {
|
||||
impl EventQuoteConverter {
|
||||
fn new(enabled: bool) -> Self {
|
||||
EventQuoteConverter {
|
||||
enabled: enabled,
|
||||
enabled,
|
||||
convert_text: true,
|
||||
}
|
||||
}
|
||||
@@ -138,28 +206,28 @@ impl EventQuoteConverter {
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
|
||||
Event::Start(Tag::CodeBlock(_)) => {
|
||||
self.convert_text = false;
|
||||
event
|
||||
}
|
||||
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
|
||||
Event::End(Tag::CodeBlock(_)) => {
|
||||
self.convert_text = true;
|
||||
event
|
||||
}
|
||||
Event::Text(ref text) if self.convert_text => {
|
||||
Event::Text(Cow::from(convert_quotes_to_curly(text)))
|
||||
Event::Text(CowStr::from(convert_quotes_to_curly(text)))
|
||||
}
|
||||
_ => event,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clean_codeblock_headers(event: Event) -> Event {
|
||||
fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
|
||||
match event {
|
||||
Event::Start(Tag::CodeBlock(ref info)) => {
|
||||
let info: String = info.chars().filter(|ch| !ch.is_whitespace()).collect();
|
||||
|
||||
Event::Start(Tag::CodeBlock(Cow::from(info)))
|
||||
Event::Start(Tag::CodeBlock(CowStr::from(info)))
|
||||
}
|
||||
_ => event,
|
||||
}
|
||||
@@ -193,7 +261,8 @@ fn convert_quotes_to_curly(original_text: &str) -> String {
|
||||
preceded_by_whitespace = original_char.is_whitespace();
|
||||
|
||||
converted_char
|
||||
}).collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Prints a "backtrace" of some `Error`.
|
||||
@@ -228,6 +297,12 @@ mod tests {
|
||||
render_markdown("[example_anchor](example.md#anchor)", false),
|
||||
"<p><a href=\"example.html#anchor\">example_anchor</a></p>\n"
|
||||
);
|
||||
|
||||
// this anchor contains 'md' inside of it
|
||||
assert_eq!(
|
||||
render_markdown("[phantom data](foo.html#phantomdata)", false),
|
||||
"<p><a href=\"foo.html#phantomdata\">phantom data</a></p>\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -328,28 +403,42 @@ more text with spaces
|
||||
|
||||
#[test]
|
||||
fn it_generates_anchors() {
|
||||
assert_eq!(
|
||||
id_from_content("## `--passes`: add more rustdoc passes"),
|
||||
"a--passes-add-more-rustdoc-passes"
|
||||
);
|
||||
assert_eq!(
|
||||
id_from_content("## Method-call expressions"),
|
||||
"method-call-expressions"
|
||||
);
|
||||
assert_eq!(id_from_content("## **Bold** title"), "bold-title");
|
||||
assert_eq!(id_from_content("## `Code` title"), "code-title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_generates_anchors_from_non_ascii_initial() {
|
||||
assert_eq!(
|
||||
id_from_content("## `--passes`: add more rustdoc passes"),
|
||||
"--passes-add-more-rustdoc-passes"
|
||||
);
|
||||
assert_eq!(
|
||||
id_from_content("## 中文標題 CJK title"),
|
||||
"中文標題-cjk-title"
|
||||
);
|
||||
assert_eq!(id_from_content("## Über"), "Über");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_normalizes_ids() {
|
||||
assert_eq!(
|
||||
normalize_id("`--passes`: add more rustdoc passes"),
|
||||
"a--passes-add-more-rustdoc-passes"
|
||||
"--passes-add-more-rustdoc-passes"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_id("Method-call 🐙 expressions \u{1f47c}"),
|
||||
"method-call--expressions-"
|
||||
);
|
||||
assert_eq!(normalize_id("_-_12345"), "a_-_12345");
|
||||
assert_eq!(normalize_id("12345"), "a12345");
|
||||
assert_eq!(normalize_id("_-_12345"), "_-_12345");
|
||||
assert_eq!(normalize_id("12345"), "12345");
|
||||
assert_eq!(normalize_id("中文"), "中文");
|
||||
assert_eq!(normalize_id("にほんご"), "にほんご");
|
||||
assert_eq!(normalize_id("한국어"), "한국어");
|
||||
assert_eq!(normalize_id(""), "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,63 @@
|
||||
use itertools::Itertools;
|
||||
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
|
||||
|
||||
// This trait is already contained in the standard lib, however it is unstable.
|
||||
// TODO: Remove when the `collections_range` feature stabilises
|
||||
// (https://github.com/rust-lang/rust/issues/30877)
|
||||
pub trait RangeArgument<T: ?Sized> {
|
||||
fn start(&self) -> Option<&T>;
|
||||
fn end(&self) -> Option<&T>;
|
||||
}
|
||||
|
||||
impl<T: ?Sized> RangeArgument<T> for RangeFull {
|
||||
fn start(&self) -> Option<&T> {
|
||||
None
|
||||
}
|
||||
fn end(&self) -> Option<&T> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> RangeArgument<T> for RangeFrom<T> {
|
||||
fn start(&self) -> Option<&T> {
|
||||
Some(&self.start)
|
||||
}
|
||||
fn end(&self) -> Option<&T> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> RangeArgument<T> for RangeTo<T> {
|
||||
fn start(&self) -> Option<&T> {
|
||||
None
|
||||
}
|
||||
fn end(&self) -> Option<&T> {
|
||||
Some(&self.end)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> RangeArgument<T> for Range<T> {
|
||||
fn start(&self) -> Option<&T> {
|
||||
Some(&self.start)
|
||||
}
|
||||
fn end(&self) -> Option<&T> {
|
||||
Some(&self.end)
|
||||
}
|
||||
}
|
||||
use regex::Regex;
|
||||
use std::ops::Bound::{Excluded, Included, Unbounded};
|
||||
use std::ops::RangeBounds;
|
||||
|
||||
/// Take a range of lines from a string.
|
||||
pub fn take_lines<R: RangeArgument<usize>>(s: &str, range: R) -> String {
|
||||
let start = *range.start().unwrap_or(&0);
|
||||
pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
let start = match range.start_bound() {
|
||||
Excluded(&n) => n + 1,
|
||||
Included(&n) => n,
|
||||
Unbounded => 0,
|
||||
};
|
||||
let mut lines = s.lines().skip(start);
|
||||
match range.end() {
|
||||
Some(&end) => lines.take(end.saturating_sub(start)).join("\n"),
|
||||
None => lines.join("\n"),
|
||||
match range.end_bound() {
|
||||
Excluded(end) => lines.take(end.saturating_sub(start)).join("\n"),
|
||||
Included(end) => lines.take((end + 1).saturating_sub(start)).join("\n"),
|
||||
Unbounded => lines.join("\n"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Take anchored lines from a string.
|
||||
/// Lines containing anchor are ignored.
|
||||
pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
lazy_static! {
|
||||
static ref RE_START: Regex = Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap();
|
||||
static ref RE_END: Regex = Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap();
|
||||
}
|
||||
|
||||
let mut retained = Vec::<&str>::new();
|
||||
let mut anchor_found = false;
|
||||
|
||||
for l in s.lines() {
|
||||
if anchor_found {
|
||||
match RE_END.captures(l) {
|
||||
Some(cap) => {
|
||||
if &cap["anchor_name"] == anchor {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !RE_START.is_match(l) {
|
||||
retained.push(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(cap) = RE_START.captures(l) {
|
||||
if &cap["anchor_name"] == anchor {
|
||||
anchor_found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
retained.join("\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::take_lines;
|
||||
use super::{take_anchored_lines, take_lines};
|
||||
|
||||
#[test]
|
||||
fn take_lines_test() {
|
||||
@@ -70,4 +70,33 @@ mod tests {
|
||||
assert_eq!(take_lines(s, 4..3), "");
|
||||
assert_eq!(take_lines(s, ..100), s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_anchored_lines_test() {
|
||||
let s = "Lorem\nipsum\ndolor\nsit\namet";
|
||||
assert_eq!(take_anchored_lines(s, "test"), "");
|
||||
|
||||
let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet";
|
||||
assert_eq!(take_anchored_lines(s, "test"), "");
|
||||
|
||||
let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet";
|
||||
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
|
||||
assert_eq!(take_anchored_lines(s, "something"), "");
|
||||
|
||||
let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
|
||||
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
|
||||
assert_eq!(take_anchored_lines(s, "something"), "");
|
||||
|
||||
let s = "Lorem\nANCHOR: test\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
|
||||
assert_eq!(take_anchored_lines(s, "test"), "ipsum\ndolor\nsit\namet");
|
||||
assert_eq!(take_anchored_lines(s, "something"), "");
|
||||
|
||||
let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum";
|
||||
assert_eq!(
|
||||
take_anchored_lines(s, "test2"),
|
||||
"ipsum\ndolor\nsit\namet\nlorem"
|
||||
);
|
||||
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
|
||||
assert_eq!(take_anchored_lines(s, "something"), "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
//! Integration tests to make sure alternate backends work.
|
||||
|
||||
extern crate mdbook;
|
||||
extern crate tempfile;
|
||||
//! Integration tests to make sure alternative backends work.
|
||||
|
||||
use mdbook::config::Config;
|
||||
use mdbook::MDBook;
|
||||
@@ -11,14 +8,14 @@ use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
#[test]
|
||||
fn passing_alternate_backend() {
|
||||
let (md, _temp) = dummy_book_with_backend("passing", "true");
|
||||
let (md, _temp) = dummy_book_with_backend("passing", success_cmd());
|
||||
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failing_alternate_backend() {
|
||||
let (md, _temp) = dummy_book_with_backend("failing", "false");
|
||||
let (md, _temp) = dummy_book_with_backend("failing", fail_cmd());
|
||||
|
||||
md.build().unwrap_err();
|
||||
}
|
||||
@@ -84,3 +81,19 @@ fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) {
|
||||
|
||||
(md, temp)
|
||||
}
|
||||
|
||||
fn fail_cmd() -> &'static str {
|
||||
if cfg!(windows) {
|
||||
r#"cmd.exe /c "exit 1""#
|
||||
} else {
|
||||
"false"
|
||||
}
|
||||
}
|
||||
|
||||
fn success_cmd() -> &'static str {
|
||||
if cfg!(windows) {
|
||||
r#"cmd.exe /c "exit 0""#
|
||||
} else {
|
||||
"true"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
extern crate mdbook;
|
||||
|
||||
mod dummy_book;
|
||||
|
||||
use dummy_book::DummyBook;
|
||||
use crate::dummy_book::DummyBook;
|
||||
use mdbook::book::Book;
|
||||
use mdbook::config::Config;
|
||||
use mdbook::errors::*;
|
||||
@@ -52,7 +50,7 @@ fn mdbook_runs_preprocessors() {
|
||||
let cfg = Config::default();
|
||||
|
||||
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
|
||||
book.with_preprecessor(Spy(Arc::clone(&spy)));
|
||||
book.with_preprocessor(Spy(Arc::clone(&spy)));
|
||||
book.build().unwrap();
|
||||
|
||||
let inner = spy.lock().unwrap();
|
||||
|
||||
56
tests/custom_preprocessors.rs
Normal file
56
tests/custom_preprocessors.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
mod dummy_book;
|
||||
|
||||
use crate::dummy_book::DummyBook;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
|
||||
use mdbook::MDBook;
|
||||
|
||||
fn example() -> CmdPreprocessor {
|
||||
CmdPreprocessor::new(
|
||||
"nop-preprocessor".to_string(),
|
||||
"cargo run --example nop-preprocessor --".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn example_supports_whatever() {
|
||||
let cmd = example();
|
||||
|
||||
let got = cmd.supports_renderer("whatever");
|
||||
|
||||
assert_eq!(got, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn example_doesnt_support_not_supported() {
|
||||
let cmd = example();
|
||||
|
||||
let got = cmd.supports_renderer("not-supported");
|
||||
|
||||
assert_eq!(got, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ask_the_preprocessor_to_blow_up() {
|
||||
let dummy_book = DummyBook::new();
|
||||
let temp = dummy_book.build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
md.with_preprocessor(example());
|
||||
|
||||
md.config
|
||||
.set("preprocessor.nop-preprocessor.blow-up", true)
|
||||
.unwrap();
|
||||
|
||||
let got = md.build();
|
||||
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_the_dummy_book() {
|
||||
let dummy_book = DummyBook::new();
|
||||
let temp = dummy_book.build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
md.with_preprocessor(example());
|
||||
|
||||
md.build().unwrap();
|
||||
}
|
||||
@@ -3,21 +3,15 @@
|
||||
|
||||
// Not all features are used in all test crates, so...
|
||||
#![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
|
||||
extern crate mdbook;
|
||||
extern crate tempfile;
|
||||
extern crate walkdir;
|
||||
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils::fs::file_to_string;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
// The funny `self::` here is because we've got an `extern crate ...` and are
|
||||
// in a submodule
|
||||
use self::mdbook::MDBook;
|
||||
use self::tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
use self::walkdir::WalkDir;
|
||||
use mdbook::MDBook;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Create a dummy book in a temporary directory, using the contents of
|
||||
/// `SUMMARY_MD` as a guide.
|
||||
@@ -58,15 +52,18 @@ impl DummyBook {
|
||||
})?;
|
||||
|
||||
let sub_pattern = if self.passing_test { "true" } else { "false" };
|
||||
let file_containing_test = temp.path().join("src/first/nested.md");
|
||||
replace_pattern_in_file(&file_containing_test, "$TEST_STATUS", sub_pattern)?;
|
||||
let files_containing_tests = ["src/first/nested.md", "src/first/nested-test.rs"];
|
||||
for file in &files_containing_tests {
|
||||
let path_containing_tests = temp.path().join(file);
|
||||
replace_pattern_in_file(&path_containing_tests, "$TEST_STATUS", sub_pattern)?;
|
||||
}
|
||||
|
||||
Ok(temp)
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()> {
|
||||
let contents = file_to_string(filename)?;
|
||||
let contents = fs::read_to_string(filename)?;
|
||||
File::create(filename)?.write_all(contents.replace(from, to).as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
@@ -76,7 +73,7 @@ fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()>
|
||||
/// the list of strings asserting that the file contains all of them.
|
||||
pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
|
||||
let filename = filename.as_ref();
|
||||
let content = file_to_string(filename).expect("Couldn't read the file's contents");
|
||||
let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
|
||||
|
||||
for s in strings {
|
||||
assert!(
|
||||
@@ -91,7 +88,7 @@ pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
|
||||
|
||||
pub fn assert_doesnt_contain_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
|
||||
let filename = filename.as_ref();
|
||||
let content = file_to_string(filename).expect("Couldn't read the file's contents");
|
||||
let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
|
||||
|
||||
for s in strings {
|
||||
assert!(
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
- [Nested Chapter](first/nested.md)
|
||||
- [Includes](first/includes.md)
|
||||
- [Recursive](first/recursive.md)
|
||||
- [Markdown](first/markdown.md)
|
||||
- [Unicode](first/unicode.md)
|
||||
- [Second Chapter](second.md)
|
||||
- [Nested Chapter](second/nested.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
29
tests/dummy_book/src/first/markdown.md
Normal file
29
tests/dummy_book/src/first/markdown.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Markdown tests
|
||||
|
||||
Tests for some markdown output.
|
||||
|
||||
## Tables
|
||||
|
||||
| foo | bar |
|
||||
| --- | --- |
|
||||
| baz | bim |
|
||||
|
||||
## Footnotes
|
||||
|
||||
Footnote example[^1], or with a word[^word].
|
||||
|
||||
[^1]: This is a footnote.
|
||||
|
||||
[^word]: A longer footnote.
|
||||
With multiple lines.
|
||||
Third line.
|
||||
|
||||
## Strikethrough
|
||||
|
||||
~~strikethrough example~~
|
||||
|
||||
## Tasklisks
|
||||
|
||||
- [X] Apples
|
||||
- [X] Broccoli
|
||||
- [ ] Carrots
|
||||
1
tests/dummy_book/src/first/nested-test.rs
Normal file
1
tests/dummy_book/src/first/nested-test.rs
Normal file
@@ -0,0 +1 @@
|
||||
assert!($TEST_STATUS);
|
||||
@@ -7,3 +7,7 @@ assert!($TEST_STATUS);
|
||||
```
|
||||
|
||||
## Some Section
|
||||
|
||||
```rust
|
||||
{{#include nested-test.rs}}
|
||||
```
|
||||
|
||||
21
tests/dummy_book/src/first/unicode.md
Normal file
21
tests/dummy_book/src/first/unicode.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Unicode stress tests
|
||||
|
||||
Please be careful editing, this contains carefully crafted characters.
|
||||
|
||||
Two byte character: spatiëring
|
||||
|
||||
Combining character: spatiëring
|
||||
|
||||
Three byte character: 书こんにちは
|
||||
|
||||
Four byte character: 𐌀𐌁𐌂𐌃𐌄𐌅𐌆𐌇𐌈
|
||||
|
||||
Right-to-left: مرحبا
|
||||
|
||||
Emoticons: 🔊 😍 💜 1️⃣
|
||||
|
||||
right-to-left mark: hello באמת!
|
||||
|
||||
|
||||
Zalgo: ǫ̛̖̱̗̝͈̋͒͋̏ͥͫ̒̆ͩ̏͌̾͊͐ͪ̾̚
|
||||
|
||||
16
tests/dummy_book/src/second/nested.md
Normal file
16
tests/dummy_book/src/second/nested.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Testing relative links for the print page
|
||||
|
||||
When we link to [the first section](../first/nested.md), it should work on
|
||||
both the print page and the non-print page.
|
||||
|
||||
A [fragment link](#some-section) should work.
|
||||
|
||||
Link [outside](../../std/foo/bar.html).
|
||||
|
||||

|
||||
|
||||
<a href="../first/markdown.md">HTML Link</a>
|
||||
|
||||
<img src="../images/picture.png" alt="raw html">
|
||||
|
||||
## Some section
|
||||
@@ -1,9 +1,8 @@
|
||||
extern crate mdbook;
|
||||
extern crate tempfile;
|
||||
|
||||
use mdbook::config::Config;
|
||||
use mdbook::MDBook;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
|
||||
@@ -27,6 +26,36 @@ fn base_mdbook_init_should_create_default_content() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `mdbook init` in a directory containing a SUMMARY.md should create the
|
||||
/// files listed in the summary.
|
||||
#[test]
|
||||
fn run_mdbook_init_should_create_content_from_summary() {
|
||||
let created_files = vec!["intro.md", "first.md", "outro.md"];
|
||||
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
let src_dir = temp.path().join("src");
|
||||
fs::create_dir_all(src_dir.clone()).unwrap();
|
||||
static SUMMARY: &str = r#"# Summary
|
||||
|
||||
[intro](intro.md)
|
||||
|
||||
- [First chapter](first.md)
|
||||
|
||||
[outro](outro.md)
|
||||
|
||||
"#;
|
||||
|
||||
let mut summary = File::create(src_dir.join("SUMMARY.md")).unwrap();
|
||||
summary.write_all(SUMMARY.as_bytes()).unwrap();
|
||||
MDBook::init(temp.path()).build().unwrap();
|
||||
|
||||
for file in &created_files {
|
||||
let target = src_dir.join(file);
|
||||
println!("{}", target.display());
|
||||
assert!(target.exists(), "{} doesn't exist", file);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set some custom arguments for where to place the source and destination
|
||||
/// files, then call `mdbook init`.
|
||||
#[test]
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
//! Some integration tests to make sure the `SUMMARY.md` parser can deal with
|
||||
//! some real-life examples.
|
||||
|
||||
extern crate env_logger;
|
||||
extern crate error_chain;
|
||||
extern crate mdbook;
|
||||
|
||||
use mdbook::book;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
extern crate mdbook;
|
||||
#[macro_use]
|
||||
extern crate pretty_assertions;
|
||||
extern crate select;
|
||||
extern crate tempfile;
|
||||
extern crate walkdir;
|
||||
|
||||
mod dummy_book;
|
||||
|
||||
use dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, DummyBook};
|
||||
use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, DummyBook};
|
||||
|
||||
use mdbook::config::Config;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils::fs::{file_to_string, write_file};
|
||||
use mdbook::utils::fs::write_file;
|
||||
use mdbook::MDBook;
|
||||
use select::document::Document;
|
||||
use select::predicate::{Class, Name, Predicate};
|
||||
@@ -22,16 +18,22 @@ use std::path::Path;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
const BOOK_ROOT: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/dummy_book");
|
||||
const TOC_TOP_LEVEL: &[&'static str] = &[
|
||||
const BOOK_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/dummy_book");
|
||||
const TOC_TOP_LEVEL: &[&str] = &[
|
||||
"1. First Chapter",
|
||||
"2. Second Chapter",
|
||||
"Conclusion",
|
||||
"Dummy Book",
|
||||
"Introduction",
|
||||
];
|
||||
const TOC_SECOND_LEVEL: &[&'static str] =
|
||||
&["1.1. Nested Chapter", "1.2. Includes", "1.3. Recursive"];
|
||||
const TOC_SECOND_LEVEL: &[&str] = &[
|
||||
"1.1. Nested Chapter",
|
||||
"1.2. Includes",
|
||||
"1.3. Recursive",
|
||||
"1.4. Markdown",
|
||||
"1.5. Unicode",
|
||||
"2.1. Nested Chapter",
|
||||
];
|
||||
|
||||
/// Make sure you can load the dummy book and build it without panicking.
|
||||
#[test]
|
||||
@@ -109,6 +111,27 @@ fn check_correct_cross_links_in_nested_dir() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_correct_relative_links_in_print_page() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let first = temp.path().join("book");
|
||||
|
||||
assert_contains_strings(
|
||||
first.join("print.html"),
|
||||
&[
|
||||
r##"<a href="second/../first/nested.html">the first section</a>,"##,
|
||||
r##"<a href="second/../../std/foo/bar.html">outside</a>"##,
|
||||
r##"<img src="second/../images/picture.png" alt="Some image" />"##,
|
||||
r##"<a href="second/nested.html#some-section">fragment link</a>"##,
|
||||
r##"<a href="second/../first/markdown.html">HTML Link</a>"##,
|
||||
r##"<img src="second/../images/picture.png" alt="raw html">"##,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rendered_code_has_playpen_stuff() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
@@ -169,7 +192,7 @@ fn chapter_files_were_rendered_to_html() {
|
||||
let chapter_files = WalkDir::new(&src)
|
||||
.into_iter()
|
||||
.filter_entry(|entry| entry_ends_with(entry, ".md"))
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter_map(std::result::Result::ok)
|
||||
.map(|entry| entry.path().to_path_buf())
|
||||
.filter(|path| path.file_name().and_then(OsStr::to_str) != Some("SUMMARY.md"));
|
||||
|
||||
@@ -201,7 +224,7 @@ fn root_index_html() -> Result<Document> {
|
||||
.chain_err(|| "Book building failed")?;
|
||||
|
||||
let index_page = temp.path().join("book").join("index.html");
|
||||
let html = file_to_string(&index_page).chain_err(|| "Unable to read index.html")?;
|
||||
let html = fs::read_to_string(&index_page).chain_err(|| "Unable to read index.html")?;
|
||||
|
||||
Ok(Document::from(html.as_str()))
|
||||
}
|
||||
@@ -309,7 +332,10 @@ fn able_to_include_files_in_chapters() {
|
||||
|
||||
let includes = temp.path().join("book/first/includes.html");
|
||||
|
||||
let summary_strings = &["<h1>Summary</h1>", ">First Chapter</a>"];
|
||||
let summary_strings = &[
|
||||
r##"<h1><a class="header" href="#summary" id="summary">Summary</a></h1>"##,
|
||||
">First Chapter</a>",
|
||||
];
|
||||
assert_contains_strings(&includes, summary_strings);
|
||||
|
||||
assert_doesnt_contain_strings(&includes, &["{{#include ../SUMMARY.md::}}"]);
|
||||
@@ -372,7 +398,7 @@ fn by_default_mdbook_use_index_preprocessor_to_convert_readme_to_index() {
|
||||
"First README",
|
||||
];
|
||||
assert_contains_strings(&first_index, &expected_strings);
|
||||
assert_doesnt_contain_strings(&first_index, &vec!["README.html"]);
|
||||
assert_doesnt_contain_strings(&first_index, &["README.html"]);
|
||||
|
||||
let second_index = temp.path().join("book").join("second").join("index.html");
|
||||
let unexpected_strings = vec!["Second README"];
|
||||
@@ -385,7 +411,7 @@ fn theme_dir_overrides_work_correctly() {
|
||||
let book_dir = book_dir.path();
|
||||
let theme_dir = book_dir.join("theme");
|
||||
|
||||
let mut index = ::mdbook::theme::INDEX.to_vec();
|
||||
let mut index = mdbook::theme::INDEX.to_vec();
|
||||
index.extend_from_slice(b"\n<!-- This is a modified index.hbs! -->");
|
||||
|
||||
write_file(&theme_dir, "index.hbs", &index).unwrap();
|
||||
@@ -397,24 +423,69 @@ fn theme_dir_overrides_work_correctly() {
|
||||
dummy_book::assert_contains_strings(built_index, &["This is a modified index.hbs!"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_index_for_print_html() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let print_html = temp.path().join("book/print.html");
|
||||
assert_contains_strings(print_html, &[r##"noindex"##]);
|
||||
|
||||
let index_html = temp.path().join("book/index.html");
|
||||
assert_doesnt_contain_strings(index_html, &[r##"noindex"##]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_options() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let path = temp.path().join("book/first/markdown.html");
|
||||
assert_contains_strings(
|
||||
&path,
|
||||
&[
|
||||
"<th>foo</th>",
|
||||
"<th>bar</th>",
|
||||
"<td>baz</td>",
|
||||
"<td>bim</td>",
|
||||
],
|
||||
);
|
||||
assert_contains_strings(&path, &[
|
||||
r##"<sup class="footnote-reference"><a href="#1">1</a></sup>"##,
|
||||
r##"<sup class="footnote-reference"><a href="#word">2</a></sup>"##,
|
||||
r##"<div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>"##,
|
||||
r##"<div class="footnote-definition" id="word"><sup class="footnote-definition-label">2</sup>"##,
|
||||
]);
|
||||
assert_contains_strings(&path, &["<del>strikethrough example</del>"]);
|
||||
assert_contains_strings(
|
||||
&path,
|
||||
&[
|
||||
"<li><input disabled=\"\" type=\"checkbox\" checked=\"\"/>\nApples",
|
||||
"<li><input disabled=\"\" type=\"checkbox\" checked=\"\"/>\nBroccoli",
|
||||
"<li><input disabled=\"\" type=\"checkbox\"/>\nCarrots",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "search")]
|
||||
mod search {
|
||||
extern crate serde_json;
|
||||
use dummy_book::DummyBook;
|
||||
use mdbook::utils::fs::file_to_string;
|
||||
use crate::dummy_book::DummyBook;
|
||||
use mdbook::MDBook;
|
||||
use std::fs::File;
|
||||
use std::fs::{self, File};
|
||||
use std::path::Path;
|
||||
|
||||
fn read_book_index(root: &Path) -> serde_json::Value {
|
||||
let index = root.join("book/searchindex.js");
|
||||
let index = file_to_string(index).unwrap();
|
||||
let index = index.trim_left_matches("window.search = ");
|
||||
let index = index.trim_right_matches(";");
|
||||
let index = fs::read_to_string(index).unwrap();
|
||||
let index = index.trim_start_matches("Object.assign(window.search, ");
|
||||
let index = index.trim_end_matches(");");
|
||||
serde_json::from_str(&index).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::float_cmp)]
|
||||
fn book_creates_reasonable_search_index() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
@@ -443,7 +514,7 @@ mod search {
|
||||
assert_eq!(docs[&some_section]["body"], "");
|
||||
assert_eq!(
|
||||
docs[&summary]["body"],
|
||||
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Second Chapter Conclusion"
|
||||
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode Second Chapter Nested Chapter Conclusion"
|
||||
);
|
||||
assert_eq!(docs[&summary]["breadcrumbs"], "First Chapter » Summary");
|
||||
assert_eq!(docs[&conclusion]["body"], "I put <HTML> in here!");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,6 @@
|
||||
extern crate mdbook;
|
||||
|
||||
mod dummy_book;
|
||||
|
||||
use dummy_book::DummyBook;
|
||||
use crate::dummy_book::DummyBook;
|
||||
|
||||
use mdbook::MDBook;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user