mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 15:01:45 -05:00
Compare commits
273 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffa8284743 | ||
|
|
3e91f9cd5d | ||
|
|
f55028b61a | ||
|
|
0d887505af | ||
|
|
6c20736a55 | ||
|
|
e4a46c9477 | ||
|
|
6ae5c686d9 | ||
|
|
b862080006 | ||
|
|
6b790b83ec | ||
|
|
d8ad68c947 | ||
|
|
6b784be616 | ||
|
|
f5598b2eee | ||
|
|
ff4b8e7a8d | ||
|
|
9c34e602bd | ||
|
|
a306da3ad7 | ||
|
|
9bede85efa | ||
|
|
11b1e86187 | ||
|
|
10d30a2dc0 | ||
|
|
601ebc5499 | ||
|
|
4251d7a838 | ||
|
|
93008cf20b | ||
|
|
3f9f681b9e | ||
|
|
63680d0786 | ||
|
|
656a1825cc | ||
|
|
1a2fa29209 | ||
|
|
d22299d998 | ||
|
|
0af417085f | ||
|
|
9634798eb7 | ||
|
|
2a8af1c21d | ||
|
|
981f8695ff | ||
|
|
48b5e52f62 | ||
|
|
c4fec94c4c | ||
|
|
ab0c338c08 | ||
|
|
8a82f6336a | ||
|
|
1700783594 | ||
|
|
e6629cd75b | ||
|
|
5a077b9ff4 | ||
|
|
8b4e488de1 | ||
|
|
68d8ceec47 | ||
|
|
db337d4a6f | ||
|
|
5e277140be | ||
|
|
14add9c290 | ||
|
|
87877a9dae | ||
|
|
2cf00d0880 | ||
|
|
8c7af3c767 | ||
|
|
6dd785ea6c | ||
|
|
8d131b4310 | ||
|
|
97b38063b1 | ||
|
|
d23734f82e | ||
|
|
2ccfaadd1d | ||
|
|
3d04e5c7ff | ||
|
|
49ef7b6f02 | ||
|
|
da7026190c | ||
|
|
92377013cc | ||
|
|
34b586ab32 | ||
|
|
a79065b0d3 | ||
|
|
b3ab93a4b3 | ||
|
|
49b75810fa | ||
|
|
b85d5eb455 | ||
|
|
27faa54ae8 | ||
|
|
fae0759626 | ||
|
|
cc74ca2e6e | ||
|
|
7cae3a058d | ||
|
|
8fb6ac7987 | ||
|
|
82d32ee761 | ||
|
|
fe9b534ad7 | ||
|
|
56652e8fa6 | ||
|
|
c3a1e41ed7 | ||
|
|
3976c9d8f0 | ||
|
|
96b6f02834 | ||
|
|
b571511737 | ||
|
|
ebdab38a32 | ||
|
|
c06f450e7d | ||
|
|
b87c231fc3 | ||
|
|
8024b08f93 | ||
|
|
8ec0bf6e30 | ||
|
|
a8926d5392 | ||
|
|
00473d8420 | ||
|
|
86d390032b | ||
|
|
b3c0b01350 | ||
|
|
e33192753d | ||
|
|
7932e13512 | ||
|
|
9fd2509c0d | ||
|
|
5dec8508c7 | ||
|
|
4d2dc6f482 | ||
|
|
efb13d7bc1 | ||
|
|
27b1e05c87 | ||
|
|
e440094b37 | ||
|
|
15cae10ca8 | ||
|
|
dc2062ab36 | ||
|
|
d9ce98d710 | ||
|
|
b59aab56f2 | ||
|
|
b899c48019 | ||
|
|
515a253e97 | ||
|
|
7ddc3df945 | ||
|
|
2f7293af5c | ||
|
|
fa3ae53d46 | ||
|
|
6425c29893 | ||
|
|
d0bb830491 | ||
|
|
d325c601bb | ||
|
|
e9e889f523 | ||
|
|
e5e10c681a | ||
|
|
05edc4421b | ||
|
|
22ea5fe335 | ||
|
|
714c5fb81e | ||
|
|
56ceb627b8 | ||
|
|
c1b2bec7d7 | ||
|
|
8201b411ab | ||
|
|
836546cf0d | ||
|
|
9813802b3e | ||
|
|
fcf8f938d2 | ||
|
|
60aaa7ae31 | ||
|
|
1b584d1746 | ||
|
|
aa4cb9465f | ||
|
|
89a2e39b80 | ||
|
|
3c2b8cd10f | ||
|
|
6b0b42ebcc | ||
|
|
7a3513200f | ||
|
|
3db0c0b9a1 | ||
|
|
2c7aac6d7a | ||
|
|
3ee22fb430 | ||
|
|
16c5ec4d74 | ||
|
|
7e7e779ef7 | ||
|
|
b364e8ea2c | ||
|
|
78325aaccb | ||
|
|
1411ea967a | ||
|
|
d147a85006 | ||
|
|
0f0dce8d6c | ||
|
|
379574dc61 | ||
|
|
6a7de13c6f | ||
|
|
331aad1597 | ||
|
|
7e01cf9e18 | ||
|
|
c922b8aae6 | ||
|
|
b21446898a | ||
|
|
f4b4a331d7 | ||
|
|
aa349e0b7c | ||
|
|
b592b10633 | ||
|
|
d62cf8e883 | ||
|
|
c6844dd771 | ||
|
|
009247be01 | ||
|
|
84b3b7218e | ||
|
|
71ba6c9eb8 | ||
|
|
9d4ee689db | ||
|
|
ffe88d7e29 | ||
|
|
9f930706bb | ||
|
|
24fa615149 | ||
|
|
a72d6002b7 | ||
|
|
5b7abf4714 | ||
|
|
d0ef70e574 | ||
|
|
7525b35383 | ||
|
|
b54e73e3b6 | ||
|
|
59c76fa665 | ||
|
|
c1d982d92b | ||
|
|
3db275d68a | ||
|
|
94e797fba0 | ||
|
|
c3beecc96a | ||
|
|
7aff98a859 | ||
|
|
bbf54d7459 | ||
|
|
dcc642e66d | ||
|
|
2b738d4425 | ||
|
|
b3670ece0e | ||
|
|
30ce7e79ac | ||
|
|
94f7578576 | ||
|
|
e6568a70eb | ||
|
|
0eb23efd44 | ||
|
|
e78a8471c7 | ||
|
|
dcccd3289d | ||
|
|
5637a66459 | ||
|
|
536873ca26 | ||
|
|
d6ea4e3f7a | ||
|
|
fcceee4761 | ||
|
|
3f39ba82f9 | ||
|
|
7da38715c1 | ||
|
|
c83bbd6319 | ||
|
|
fad3c663f4 | ||
|
|
f8b9054265 | ||
|
|
f26116a491 | ||
|
|
7f59fdd9bd | ||
|
|
45d41eac5f | ||
|
|
2b5890e2ed | ||
|
|
0b9570b160 | ||
|
|
90396c5b76 | ||
|
|
24b76dd879 | ||
|
|
9a9eb0124a | ||
|
|
257374d76b | ||
|
|
1a0c296532 | ||
|
|
9b4ab72a80 | ||
|
|
b1c2e466e7 | ||
|
|
cdea0f6b61 | ||
|
|
e9b0be7090 | ||
|
|
d402a12e88 | ||
|
|
218e200117 | ||
|
|
3d55375f61 | ||
|
|
77e7cfd22b | ||
|
|
76cd39e5e2 | ||
|
|
09e7bb76dc | ||
|
|
28387130c0 | ||
|
|
33d3d9c3ec | ||
|
|
beec17e55d | ||
|
|
e651f4d734 | ||
|
|
87d2cd9845 | ||
|
|
32abeef088 | ||
|
|
5de9b6841e | ||
|
|
95e0743bc0 | ||
|
|
3c97525743 | ||
|
|
9a65c8ab92 | ||
|
|
a64a7b7470 | ||
|
|
fd4137a9ea | ||
|
|
a3d4febe3e | ||
|
|
7af4b1dfe8 | ||
|
|
ba6bffac5a | ||
|
|
6201e577fe | ||
|
|
cf2459f730 | ||
|
|
45a481049e | ||
|
|
6bcabcbb6b | ||
|
|
ef993e8cc2 | ||
|
|
a3a5386da0 | ||
|
|
3ab911afa1 | ||
|
|
4615ce2f8c | ||
|
|
7cb8087469 | ||
|
|
d1721667b6 | ||
|
|
1038f0b7f5 | ||
|
|
942cc12a74 | ||
|
|
59f2a9bf4e | ||
|
|
75d0f1efd4 | ||
|
|
552e3378cf | ||
|
|
7c0ddff96a | ||
|
|
07e72757d3 | ||
|
|
58f66a146d | ||
|
|
643d5ecc5c | ||
|
|
c712ba7aab | ||
|
|
1450070f73 | ||
|
|
e310dfc605 | ||
|
|
cbfd75a821 | ||
|
|
eaa6914205 | ||
|
|
a76557a678 | ||
|
|
01836ba5d4 | ||
|
|
46ce077de6 | ||
|
|
f7c9180d80 | ||
|
|
9e9cf49c50 | ||
|
|
4c951d530d | ||
|
|
780fb979a0 | ||
|
|
b77942d3c8 | ||
|
|
d0deee90b0 | ||
|
|
e6ac8ecdd9 | ||
|
|
d1f5ecc103 | ||
|
|
e0b247e9d6 | ||
|
|
db8a2821ea | ||
|
|
39d7130019 | ||
|
|
2eccb457d2 | ||
|
|
d1682d27fb | ||
|
|
a94a940ff7 | ||
|
|
daf402e1dc | ||
|
|
5ebd2c0527 | ||
|
|
b349e8abc9 | ||
|
|
e225586953 | ||
|
|
cf7663f800 | ||
|
|
3155c63e88 | ||
|
|
4df9ec90af | ||
|
|
73cabeb904 | ||
|
|
4b773024ae | ||
|
|
33ea661350 | ||
|
|
1b18740b56 | ||
|
|
6fed9e52f9 | ||
|
|
fd59dc73e5 | ||
|
|
146bea48c6 | ||
|
|
efb5bc285d | ||
|
|
5ea8e55aea | ||
|
|
1acf23ff73 | ||
|
|
69cc1fa005 | ||
|
|
2fb489137b | ||
|
|
4d9eb9b4b4 | ||
|
|
65d9eb6f7e |
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@@ -31,12 +31,12 @@ jobs:
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Build book
|
||||
run: cargo run -- build book-example
|
||||
run: cargo run -- build guide
|
||||
- name: Deploy to GitHub
|
||||
env:
|
||||
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
|
||||
run: |
|
||||
touch book-example/book/.nojekyll
|
||||
touch guide/book/.nojekyll
|
||||
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
|
||||
cd book-example/book
|
||||
cd guide/book
|
||||
/tmp/deploy
|
||||
|
||||
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
rust: stable
|
||||
- build: msrv
|
||||
os: ubuntu-latest
|
||||
rust: 1.39.0
|
||||
rust: 1.46.0
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -4,10 +4,14 @@ target
|
||||
.DS_Store
|
||||
|
||||
book-test
|
||||
book-example/book
|
||||
guide/book
|
||||
|
||||
.vscode
|
||||
tests/dummy_book/book/
|
||||
|
||||
# Ignore Jetbrains specific files.
|
||||
.idea/
|
||||
|
||||
# Ignore Vim temporary and swap files.
|
||||
*.sw?
|
||||
*~
|
||||
|
||||
200
CHANGELOG.md
200
CHANGELOG.md
@@ -1,5 +1,205 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.4.13
|
||||
[e6629cd...f55028b](https://github.com/rust-lang/mdBook/compare/e6629cd...f55028b)
|
||||
|
||||
### Added
|
||||
|
||||
- Added the ability to specify the preprocessor order.
|
||||
[#1607](https://github.com/rust-lang/mdBook/pull/1607)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Include chapters with no headers in the search index
|
||||
[#1637](https://github.com/rust-lang/mdBook/pull/1637)
|
||||
- Switched to the `opener` crate for opening a web browser, which should fix
|
||||
some issues with blocking.
|
||||
[#1656](https://github.com/rust-lang/mdBook/pull/1656)
|
||||
- Fixed clicking the border of the theme switcher breaking the theme selection.
|
||||
[#1651](https://github.com/rust-lang/mdBook/pull/1651)
|
||||
|
||||
## mdBook 0.4.12
|
||||
[14add9c...8b4e488](https://github.com/rust-lang/mdBook/compare/14add9c...8b4e488)
|
||||
|
||||
### Changed
|
||||
- Reverted the change to update to highlight.js 11, as it broke hidden code lines.
|
||||
[#1597](https://github.com/rust-lang/mdBook/pull/1621)
|
||||
|
||||
## mdBook 0.4.11
|
||||
[e440094...2cf00d0](https://github.com/rust-lang/mdBook/compare/e440094...2cf00d0)
|
||||
|
||||
### Added
|
||||
- Added support for Rust 2021 edition.
|
||||
[#1596](https://github.com/rust-lang/mdBook/pull/1596)
|
||||
- Added `mdbook completions` subcommand which provides shell completions.
|
||||
[#1425](https://github.com/rust-lang/mdBook/pull/1425)
|
||||
- Added `--title` and `--ignore` flags to `mdbook init` to avoid the
|
||||
interactive input.
|
||||
[#1559](https://github.com/rust-lang/mdBook/pull/1559)
|
||||
|
||||
### Changed
|
||||
- If running a Rust example does not have any output, it now displays the text
|
||||
"No output" instead of not showing anything.
|
||||
[#1599](https://github.com/rust-lang/mdBook/pull/1599)
|
||||
- Code block language tags can now be separated by space or tab (along with
|
||||
commas) to match the behavior of other sites like GitHub and rustdoc.
|
||||
[#1469](https://github.com/rust-lang/mdBook/pull/1469)
|
||||
- Updated `warp` (the web server) to the latest version.
|
||||
This also updates the minimum supported Rust version to 1.46.
|
||||
[#1612](https://github.com/rust-lang/mdBook/pull/1612)
|
||||
- Updated to highlight.js 11. This has various highlighting improvements.
|
||||
[#1597](https://github.com/rust-lang/mdBook/pull/1597)
|
||||
|
||||
### Fixed
|
||||
- Inline code blocks inside a header are no longer highlighted when
|
||||
`output.html.playground.editable` is `true`.
|
||||
[#1613](https://github.com/rust-lang/mdBook/pull/1613)
|
||||
|
||||
## mdBook 0.4.10
|
||||
[2f7293a...dc2062a](https://github.com/rust-lang/mdBook/compare/2f7293a...dc2062a)
|
||||
|
||||
### Changed
|
||||
- Reverted breaking change in 0.4.9 that removed the `__non_exhaustive` marker
|
||||
on the `Book` struct.
|
||||
[#1572](https://github.com/rust-lang/mdBook/pull/1572)
|
||||
- Updated handlebars to 4.0.
|
||||
[#1550](https://github.com/rust-lang/mdBook/pull/1550)
|
||||
- Removed the `chapter_begin` id on the print page's chapter separators.
|
||||
[#1541](https://github.com/rust-lang/mdBook/pull/1541)
|
||||
|
||||
## mdBook 0.4.9
|
||||
[7e01cf9...d325c60](https://github.com/rust-lang/mdBook/compare/7e01cf9...d325c60)
|
||||
|
||||
### Changed
|
||||
- Updated all dependencies and raised the minimum Rust version to 1.42.
|
||||
[#1528](https://github.com/rust-lang/mdBook/pull/1528)
|
||||
- Added more detail to error message when a preprocessor fails.
|
||||
[#1526](https://github.com/rust-lang/mdBook/pull/1526)
|
||||
- Set max-width of HTML video tags to 100% to match img tags.
|
||||
[#1542](https://github.com/rust-lang/mdBook/pull/1542)
|
||||
|
||||
### Fixed
|
||||
- Type errors when parsing `book.toml` are no longer ignored.
|
||||
[#1539](https://github.com/rust-lang/mdBook/pull/1539)
|
||||
- Better handling if `mdbook serve` fails to start the http server.
|
||||
[#1555](https://github.com/rust-lang/mdBook/pull/1555)
|
||||
- Fixed the path for `edit-url-template` if the book used a source directory
|
||||
other than `src`.
|
||||
[#1554](https://github.com/rust-lang/mdBook/pull/1554)
|
||||
|
||||
## mdBook 0.4.8
|
||||
[fcceee4...b592b10](https://github.com/rust-lang/mdBook/compare/fcceee4...b592b10)
|
||||
|
||||
### Added
|
||||
- Added the option `output.html.edit-url-template` which can be a URL which is
|
||||
linked on each page to direct the user to a site (such as GitHub) where the
|
||||
user can directly suggest an edit for the page they are currently reading.
|
||||
[#1506](https://github.com/rust-lang/mdBook/pull/1506)
|
||||
|
||||
### Changed
|
||||
- Printed output now includes a page break between chapters.
|
||||
[#1485](https://github.com/rust-lang/mdBook/pull/1485)
|
||||
|
||||
### Fixed
|
||||
- HTML, such as HTML comments, is now ignored if it appears above the title line
|
||||
in `SUMMARY.md`.
|
||||
[#1437](https://github.com/rust-lang/mdBook/pull/1437)
|
||||
|
||||
## mdBook 0.4.7
|
||||
[9a9eb01...c83bbd6](https://github.com/rust-lang/mdBook/compare/9a9eb01...c83bbd6)
|
||||
|
||||
### Changed
|
||||
- Updated shlex parser to fix a minor parsing issue (used by the
|
||||
preprocessor/backend custom command config).
|
||||
[#1471](https://github.com/rust-lang/mdBook/pull/1471)
|
||||
- Enhanced text contrast of `light` theme to improve accessibility.
|
||||
[#1470](https://github.com/rust-lang/mdBook/pull/1470)
|
||||
|
||||
### Fixed
|
||||
- Fixed some issues with fragment scrolling and linking.
|
||||
[#1463](https://github.com/rust-lang/mdBook/pull/1463)
|
||||
|
||||
## mdBook 0.4.6
|
||||
[eaa6914...1a0c296](https://github.com/rust-lang/mdBook/compare/eaa6914...1a0c296)
|
||||
|
||||
### Changed
|
||||
- The chapter name is now included in the search breadcrumbs.
|
||||
[#1389](https://github.com/rust-lang/mdBook/pull/1389)
|
||||
- Pressing Escape will remove the `?highlight` argument from the URL.
|
||||
[#1427](https://github.com/rust-lang/mdBook/pull/1427)
|
||||
- `mdbook init --theme` will now place the theme in the root of the book
|
||||
directory instead of in the `src` directory.
|
||||
[#1432](https://github.com/rust-lang/mdBook/pull/1432)
|
||||
- A custom renderer that sets the `command` to a relative path now interprets
|
||||
the relative path relative to the book root. Previously it was inconsistent
|
||||
based on the platform (either relative to the current directory, or relative
|
||||
to the renderer output directory). Paths relative to the output directory
|
||||
are still supported with a deprecation warning.
|
||||
[#1418](https://github.com/rust-lang/mdBook/pull/1418)
|
||||
- The `theme` directory in the config is now interpreted as relative to the
|
||||
book root, instead of the current directory.
|
||||
[#1405](https://github.com/rust-lang/mdBook/pull/1405)
|
||||
- Handle UTF-8 BOM for chapter sources.
|
||||
[#1285](https://github.com/rust-lang/mdBook/pull/1285)
|
||||
- Removed extra whitespace added to `{{#playground}}` snippets.
|
||||
[#1375](https://github.com/rust-lang/mdBook/pull/1375)
|
||||
|
||||
### Fixed
|
||||
- Clicking on a search result with multiple search words will now correctly
|
||||
highlight all of the words.
|
||||
[#1426](https://github.com/rust-lang/mdBook/pull/1426)
|
||||
- Properly handle `<` and `>` characters in the table of contents.
|
||||
[#1376](https://github.com/rust-lang/mdBook/pull/1376)
|
||||
- Fixed to properly serialize the `build` table in the config, which prevented
|
||||
setting it in the API.
|
||||
[#1378](https://github.com/rust-lang/mdBook/pull/1378)
|
||||
|
||||
## mdBook 0.4.5
|
||||
[eaa6914...f66df09](https://github.com/rust-lang/mdBook/compare/eaa6914...f66df09)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed XSS in the search page.
|
||||
[CVE-2020-26297](https://groups.google.com/g/rustlang-security-announcements/c/3-sO6of29O0)
|
||||
[648c9ae](https://github.com/rust-lang/mdBook/commit/648c9ae772bec83f0a5954d17b4287d5bb1d6606)
|
||||
|
||||
## mdBook 0.4.4
|
||||
[4df9ec9...01836ba](https://github.com/rust-lang/mdBook/compare/4df9ec9...01836ba)
|
||||
|
||||
### Added
|
||||
- Added the `output.html.print.enable` configuration value to disable the
|
||||
"print" page.
|
||||
[#1169](https://github.com/rust-lang/mdBook/pull/1169)
|
||||
- Added a list of supported languages for syntax-highlighting to the
|
||||
documentation.
|
||||
[#1345](https://github.com/rust-lang/mdBook/pull/1345)
|
||||
|
||||
### Fixed
|
||||
- Now supports symbolic links for files in the `src` directory.
|
||||
[#1323](https://github.com/rust-lang/mdBook/pull/1323)
|
||||
|
||||
## mdBook 0.4.3
|
||||
[9278b83...4df9ec9](https://github.com/rust-lang/mdBook/compare/9278b83...4df9ec9)
|
||||
|
||||
### Added
|
||||
- Added `output.html.cname` option to emit a `CNAME` file which is used by
|
||||
GitHub Pages to know which domain is being used.
|
||||
[#1311](https://github.com/rust-lang/mdBook/pull/1311)
|
||||
|
||||
### Changed
|
||||
- `mdbook test` no longer stops on the first test failure, but instead will
|
||||
run all the tests.
|
||||
[#1313](https://github.com/rust-lang/mdBook/pull/1313)
|
||||
- Removed the `local` font source for Source Code Pro, as the locally
|
||||
installed font may not render properly on FireFox on macOS.
|
||||
[#1307](https://github.com/rust-lang/mdBook/pull/1307)
|
||||
|
||||
### Fixed
|
||||
- Added newline to end of `.nojekyll` file.
|
||||
[#1310](https://github.com/rust-lang/mdBook/pull/1310)
|
||||
- Fixed missing space before draft chapter titles.
|
||||
[#1309](https://github.com/rust-lang/mdBook/pull/1309)
|
||||
|
||||
## mdBook 0.4.2
|
||||
[649f355...9278b83](https://github.com/rust-lang/mdBook/compare/649f355...9278b83)
|
||||
|
||||
|
||||
3
CODE_OF_CONDUCT.md
Normal file
3
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# The Rust Code of Conduct
|
||||
|
||||
The Code of Conduct for this repository [can be found online](https://www.rust-lang.org/conduct.html).
|
||||
@@ -6,7 +6,6 @@ If you have come here to learn how to contribute to mdBook, we have some tips fo
|
||||
|
||||
First of all, don't hesitate to ask questions!
|
||||
Use the [issue tracker](https://github.com/rust-lang/mdBook/issues), no question is too simple.
|
||||
If we don't respond in a couple of days, ping us @Michael-F-Bryan, @budziq, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
|
||||
|
||||
### Issues to work on
|
||||
|
||||
@@ -46,7 +45,7 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
|
||||
0. Navigate into the newly created `mdBook` directory
|
||||
0. Run `cargo build`
|
||||
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdbook` or `mdbook.exe`.
|
||||
|
||||
### Code Quality
|
||||
|
||||
@@ -79,7 +78,7 @@ For more information, such as running it from your favourite editor, please see
|
||||
|
||||
#### Finding Issues with Clippy
|
||||
|
||||
Clippy is a code analyser/linter detecting mistakes, and therfore helps to improve your code.
|
||||
Clippy is a code analyser/linter detecting mistakes, and therefore 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.
|
||||
|
||||
@@ -106,3 +105,26 @@ and [rustfmt](https://github.com/rust-lang/rustfmt) on the code first.
|
||||
This is not a requirement though and will never block a pull-request from being merged.
|
||||
|
||||
That's it, happy contributions! :tada: :tada: :tada:
|
||||
|
||||
## Browser compatibility and testing
|
||||
|
||||
Currently we don't have a strict browser compatibility matrix due to our limited resources.
|
||||
We generally strive to keep mdBook compatible with a relatively recent browser on all of the most major platforms.
|
||||
That is, supporting Chrome, Safari, Firefox, Edge on Windows, macOS, Linux, iOS, and Android.
|
||||
If possible, do your best to avoid breaking older browser releases.
|
||||
|
||||
Any change to the HTML or styling is encouraged to manually check on as many browsers and platforms that you can.
|
||||
Unfortunately at this time we don't have any automated UI or browser testing, so your assistance in testing is appreciated.
|
||||
|
||||
## Updating higlight.js
|
||||
|
||||
The following are instructions for updating [highlight.js](https://highlightjs.org/).
|
||||
|
||||
1. Clone the repository at <https://github.com/highlightjs/highlight.js>
|
||||
1. Check out a tagged release (like `10.1.1`).
|
||||
1. Run `npm install`
|
||||
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx properties r scala x86asm yaml`
|
||||
1. Compare the language list that it spits out to the one in [`syntax-highlighting.md`](https://github.com/camelid/mdBook/blob/master/guide/src/format/theme/syntax-highlighting.md). If any are missing, add them to the list and rebuild (and update these docs). If any are added to the common set, add them to `syntax-highlighting.md`.
|
||||
1. Copy `build/highlight.min.js` to mdbook's directory [`highlight.js`](https://github.com/rust-lang/mdBook/blob/master/src/theme/highlight.js).
|
||||
1. Be sure to check the highlight.js [CHANGES](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md) for any breaking changes. Breaking changes that would affect users will need to wait until the next major release.
|
||||
1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. (TODO: It would be nice to have a demo file in the repo to help with this.)
|
||||
|
||||
1026
Cargo.lock
generated
1026
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
18
Cargo.toml
18
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.4.2"
|
||||
version = "0.4.13"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
]
|
||||
documentation = "http://rust-lang.github.io/mdBook/index.html"
|
||||
edition = "2018"
|
||||
exclude = ["/book-example/*"]
|
||||
exclude = ["/guide/*"]
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
@@ -20,19 +20,20 @@ anyhow = "1.0.28"
|
||||
chrono = "0.4"
|
||||
clap = "2.24"
|
||||
env_logger = "0.7.1"
|
||||
handlebars = "3.0"
|
||||
handlebars = "4.0"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4"
|
||||
memchr = "2.0"
|
||||
open = "1.1"
|
||||
opener = "0.5"
|
||||
pulldown-cmark = "0.7.0"
|
||||
regex = "1.0.0"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
shlex = "0.1"
|
||||
shlex = "1"
|
||||
tempfile = "3.0"
|
||||
toml = "0.5.1"
|
||||
topological-sort = "0.1.0"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "4.0", optional = true }
|
||||
@@ -40,15 +41,18 @@ gitignore = { version = "1.0", optional = true }
|
||||
|
||||
# Serve feature
|
||||
futures-util = { version = "0.3.4", optional = true }
|
||||
tokio = { version = "0.2.18", features = ["macros"], optional = true }
|
||||
warp = { version = "0.2.2", default-features = false, features = ["websocket"], optional = true }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
warp = { version = "0.3.1", default-features = false, features = ["websocket"], optional = true }
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
||||
ammonia = { version = "3", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "1"
|
||||
predicates = "2"
|
||||
select = "0.5"
|
||||
semver = "0.11.0"
|
||||
pretty_assertions = "0.6"
|
||||
walkdir = "2.0"
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -85,16 +85,17 @@ There are multiple ways to install mdBook.
|
||||
|
||||
## Usage
|
||||
|
||||
mdBook will primarily be used as a command line tool, even though it exposes
|
||||
mdBook is primarily used as a command line tool, even though it exposes
|
||||
all its functionality as a Rust crate for integration in other projects.
|
||||
|
||||
Here are the main commands you will want to run. For a more exhaustive
|
||||
explanation, check out the [User Guide].
|
||||
|
||||
- `mdbook init`
|
||||
- `mdbook init <directory>`
|
||||
|
||||
The init command will create a directory with the minimal boilerplate to
|
||||
start with.
|
||||
start with. If the `<directory>` parameter is omitted, the current
|
||||
directory will be used.
|
||||
|
||||
```
|
||||
book-test/
|
||||
@@ -149,6 +150,7 @@ preprocessors are:
|
||||
the url `foo/` when published to a browser
|
||||
- `links` - a built-in preprocessor (enabled by default) for expanding the
|
||||
`{{# playground}}` and `{{# include}}` helpers in a chapter.
|
||||
- [`katex`](https://github.com/lzanini/mdbook-katex) - a preprocessor rendering LaTex equations to HTML.
|
||||
|
||||
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
|
||||
@@ -161,6 +163,7 @@ of a book in order to validate links or run tests. Some existing renderers are:
|
||||
preprocessors.
|
||||
- [`linkcheck`] - a backend which will check that all links are valid
|
||||
- [`epub`] - an experimental EPUB generator
|
||||
- [`man`] - a backend that generates manual pages from the book
|
||||
|
||||
> **Note for Developers:** Feel free to send us a PR if you've developed your
|
||||
> own plugin and want it mentioned here.
|
||||
@@ -209,7 +212,7 @@ tagged [E-Easy] and **we will gladly mentor you** so that you can successfully
|
||||
go through the process of fixing a bug or adding a new feature! Let us know if
|
||||
you need any help.
|
||||
|
||||
For more info about contributing, check out our [contribution guide] who helps
|
||||
For more info about contributing, check out our [contribution guide] which helps
|
||||
you go through the build and contribution process!
|
||||
|
||||
There is also a [rendered version][master-docs] of the latest API docs
|
||||
@@ -232,3 +235,4 @@ All the code in this repository is released under the ***Mozilla Public License
|
||||
[master-docs]: http://rust-lang.github.io/mdBook/
|
||||
[`linkcheck`]: https://crates.io/crates/mdbook-linkcheck
|
||||
[`epub`]: https://crates.io/crates/mdbook-epub
|
||||
[`man`]: https://crates.io/crates/mdbook-man
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# mdBook
|
||||
|
||||
**mdBook** is a command line tool and Rust crate to create books using Markdown
|
||||
(as by the [CommonMark](https://commonmark.org/) specification) files. It's very
|
||||
similar to Gitbook but written in [Rust](http://www.rust-lang.org).
|
||||
|
||||
What you are reading serves as an example of the output of mdBook and at the
|
||||
same time as a high-level documentation.
|
||||
|
||||
mdBook is free and open source, you can find the source code on
|
||||
[GitHub](https://github.com/rust-lang/mdBook). Issues and feature
|
||||
requests can be posted on the [GitHub issue
|
||||
tracker](https://github.com/rust-lang/mdBook/issues).
|
||||
|
||||
## API docs
|
||||
|
||||
Alongside this book you can also read the [API
|
||||
docs](https://docs.rs/mdbook/*/mdbook/) generated by Rustdoc if you would like
|
||||
to use mdBook as a crate or write a new renderer and need a more low-level
|
||||
overview.
|
||||
|
||||
## License
|
||||
|
||||
mdBook, all the source code, is released under the [Mozilla Public License
|
||||
v2.0](https://www.mozilla.org/MPL/2.0/).
|
||||
@@ -1,6 +0,0 @@
|
||||
fn main() {
|
||||
println!("Hello World!");
|
||||
#
|
||||
# // You can even hide lines! :D
|
||||
# println!("I am hidden! Expand the code snippet to see me");
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
# SUMMARY.md
|
||||
|
||||
The summary file is used by mdBook to know what chapters to include, in what
|
||||
order they should appear, what their hierarchy is and where the source files
|
||||
are. Without this file, there is no book.
|
||||
|
||||
Even though `SUMMARY.md` is a markdown file, the formatting is very strict to
|
||||
allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
|
||||
|
||||
#### Structure
|
||||
|
||||
1. ***Title*** It's common practice to begin with a title, generally <code
|
||||
class="language-markdown"># Summary</code>. But it is not mandatory, the
|
||||
parser just ignores it. So you can too if you feel like it.
|
||||
|
||||
2. ***Prefix Chapter*** Before the main numbered chapters you can add a couple
|
||||
of elements that will not be numbered. This is useful for forewords,
|
||||
introductions, etc. There are however some constraints. You can not nest
|
||||
prefix chapters, they should all be on the root level. And you can not add
|
||||
prefix chapters once you have added numbered chapters.
|
||||
```markdown
|
||||
[Title of prefix element](relative/path/to/markdown.md)
|
||||
```
|
||||
|
||||
3. ***Part Title:*** Headers can be used as a title for the following numbered
|
||||
chapters. This can be used to logically separate different sections
|
||||
of book. The title is rendered as unclickable text.
|
||||
Titles are optional, and the numbered chapters can be broken into as many
|
||||
parts as desired.
|
||||
|
||||
4. ***Numbered Chapter*** Numbered chapters are the main content of the book,
|
||||
they will be numbered and can be nested, resulting in a nice hierarchy
|
||||
(chapters, sub-chapters, etc.)
|
||||
```markdown
|
||||
# Title of Part
|
||||
|
||||
- [Title of the Chapter](relative/path/to/markdown.md)
|
||||
|
||||
# Title of Another Part
|
||||
|
||||
- [More Chapters](relative/path/to/markdown2.md)
|
||||
```
|
||||
You can either use `-` or `*` to indicate a numbered chapter.
|
||||
|
||||
5. ***Suffix Chapter*** After the numbered chapters you can add a couple of
|
||||
non-numbered chapters. They are the same as prefix chapters but come after
|
||||
the numbered chapters instead of before.
|
||||
|
||||
All other elements are unsupported and will be ignored at best or result in an
|
||||
error.
|
||||
|
||||
#### Other elements
|
||||
|
||||
- ***Separators*** In between chapters you can add a separator. In the HTML renderer
|
||||
this will result in a line being rendered in the table of contents. A separator is
|
||||
a line containing exclusively dashes and at least three of them: `---`.
|
||||
- ***Draft chapters*** Draft chapters are chapters without a file and thus content.
|
||||
The purpose of a draft chapter is to signal future chapters still to be written.
|
||||
Or when still laying out the structure of the book to avoid creating the files
|
||||
while you are still changing the structure of the book a lot.
|
||||
Draft chapters will be rendered in the HTML renderer as disabled links in the table
|
||||
of contents, as you can see for the next chapter in the table of contents on the left.
|
||||
Draft chapters are written like normal chapters but without writing the path to the file
|
||||
```markdown
|
||||
- [Draft chapter]()
|
||||
```
|
||||
@@ -21,4 +21,4 @@ case $1 in
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "##[add-path]$PWD/hub/bin"
|
||||
echo "$PWD/hub/bin" >> $GITHUB_PATH
|
||||
|
||||
@@ -15,10 +15,20 @@ export CARGO_PROFILE_RELEASE_LTO=true
|
||||
cargo build --bin mdbook --release
|
||||
cd target/release
|
||||
case $1 in
|
||||
ubuntu* | macos*)
|
||||
ubuntu*)
|
||||
asset="mdbook-$TAG-$host.tar.gz"
|
||||
tar czf ../../$asset mdbook
|
||||
;;
|
||||
macos*)
|
||||
asset="mdbook-$TAG-$host.tar.gz"
|
||||
# There is a bug with BSD tar on macOS where the first 8MB of the file are
|
||||
# sometimes all NUL bytes. See https://github.com/actions/cache/issues/403
|
||||
# and https://github.com/rust-lang/cargo/issues/8603 for some more
|
||||
# information. An alternative solution here is to install GNU tar, but
|
||||
# flushing the disk cache seems to work, too.
|
||||
sudo /usr/sbin/purge
|
||||
tar czf ../../$asset mdbook
|
||||
;;
|
||||
windows*)
|
||||
asset="mdbook-$TAG-$host.zip"
|
||||
7z a ../../$asset mdbook.exe
|
||||
|
||||
@@ -3,6 +3,7 @@ use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use mdbook::book::Book;
|
||||
use mdbook::errors::Error;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use semver::{Version, VersionReq};
|
||||
use std::io;
|
||||
use std::process;
|
||||
|
||||
@@ -33,9 +34,10 @@ fn main() {
|
||||
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...
|
||||
let book_version = Version::parse(&ctx.mdbook_version)?;
|
||||
let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
|
||||
|
||||
if !version_req.matches(&book_version) {
|
||||
eprintln!(
|
||||
"Warning: The {} plugin was built against version {} of mdbook, \
|
||||
but we're being called from version {}",
|
||||
@@ -53,7 +55,7 @@ fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
|
||||
|
||||
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);
|
||||
let supported = pre.supports_renderer(renderer);
|
||||
|
||||
// Signal whether the renderer is supported by exiting with 1 or 0.
|
||||
if supported {
|
||||
|
||||
@@ -10,6 +10,8 @@ edition = "2018"
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
site-url = "/mdBook/"
|
||||
git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide"
|
||||
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
@@ -23,3 +25,6 @@ boost-hierarchy = 2
|
||||
boost-paragraph = 1
|
||||
expand = true
|
||||
heading-split-level = 2
|
||||
|
||||
[output.html.redirect]
|
||||
"/format/config.html" = "configuration/index.html"
|
||||
40
guide/src/README.md
Normal file
40
guide/src/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Introduction
|
||||
|
||||
**mdBook** is a command line tool and Rust crate to create books with Markdown. The output resembles tools like Gitbook,
|
||||
and is ideal for creating product or API documentation, tutorials, course materials or anything that requires a clean,
|
||||
easily navigable and customizable presentation. mdBook is written in [Rust](https://www.rust-lang.org); its performance
|
||||
and simplicity made it ideal for use as a tool to publish directly to hosted websites such
|
||||
as [GitHub Pages](https://pages.github.com) via automation. This guide, in fact, serves as both the mdBook documentation
|
||||
and a fine example of what mdBook produces.
|
||||
|
||||
mdBook includes built in support for both preprocessing your Markdown and alternative renderers for producing formats
|
||||
other than HTML. These facilities also enable other functionality such as
|
||||
validation. [Searching](https://crates.io/search?q=mdbook&sort=relevance) Rust's [crates.io](https://crates.io) is a
|
||||
great way to discover more extensions.
|
||||
|
||||
## API Documentation
|
||||
|
||||
In addition to the above features, mdBook also has a Rust [API](https://docs.rs/mdbook/*/mdbook/). This allows you to
|
||||
write your own preprocessor or renderer, as well as incorporate mdBook features into other applications.
|
||||
The [For Developers](for_developers) section of this guide contains more information and some examples.
|
||||
|
||||
## Markdown
|
||||
|
||||
mdBook's [parser](https://github.com/raphlinus/pulldown-cmark) adheres to the [CommonMark](https://commonmark.org/)
|
||||
specification. You can take a quick [tutorial](https://commonmark.org/help/tutorial/),
|
||||
or [try out](https://spec.commonmark.org/dingus/) CommonMark in real time. For a more in-depth experience, check out the
|
||||
[Markdown Guide](https://www.markdownguide.org).
|
||||
|
||||
## Contributing
|
||||
|
||||
mdBook is free and open source. You can find the source code on
|
||||
[GitHub](https://github.com/rust-lang/mdBook) and issues and feature requests can be posted on
|
||||
the [GitHub issue tracker](https://github.com/rust-lang/mdBook/issues). mdBook relies on the community to fix bugs and
|
||||
add features: if you'd like to contribute, please read
|
||||
the [CONTRIBUTING](https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md) guide and consider opening
|
||||
a [pull request](https://github.com/rust-lang/mdBook/pulls).
|
||||
|
||||
## License
|
||||
|
||||
The mdBook source and documentation are released under
|
||||
the [Mozilla Public License v2.0](https://www.mozilla.org/MPL/2.0/).
|
||||
@@ -1,6 +1,6 @@
|
||||
# Summary
|
||||
|
||||
- [mdBook](README.md)
|
||||
- [Introduction](README.md)
|
||||
- [Command Line Tool](cli/README.md)
|
||||
- [init](cli/init.md)
|
||||
- [build](cli/build.md)
|
||||
@@ -11,13 +11,17 @@
|
||||
- [Format](format/README.md)
|
||||
- [SUMMARY.md](format/summary.md)
|
||||
- [Draft chapter]()
|
||||
- [Configuration](format/config.md)
|
||||
- [Configuration](format/configuration/README.md)
|
||||
- [General](format/configuration/general.md)
|
||||
- [Preprocessors](format/configuration/preprocessors.md)
|
||||
- [Renderers](format/configuration/renderers.md)
|
||||
- [Environment Variables](format/configuration/environment-variables.md)
|
||||
- [Theme](format/theme/README.md)
|
||||
- [index.hbs](format/theme/index-hbs.md)
|
||||
- [Syntax highlighting](format/theme/syntax-highlighting.md)
|
||||
- [Editor](format/theme/editor.md)
|
||||
- [MathJax Support](format/mathjax.md)
|
||||
- [mdBook specific features](format/mdbook.md)
|
||||
- [mdBook-specific features](format/mdbook.md)
|
||||
- [Continuous Integration](continuous-integration.md)
|
||||
- [For Developers](for_developers/README.md)
|
||||
- [Preprocessors](for_developers/preprocessors.md)
|
||||
@@ -12,7 +12,7 @@ to download the appropriate version for your platform.
|
||||
|
||||
## Install From Source
|
||||
|
||||
mdBook can also be installed from source
|
||||
mdBook can also be installed by compiling the source code on your local machine.
|
||||
|
||||
### Pre-requisite
|
||||
|
||||
@@ -7,7 +7,8 @@ mdbook build
|
||||
```
|
||||
|
||||
It will try to parse your `SUMMARY.md` file to understand the structure of your
|
||||
book and fetch the corresponding files.
|
||||
book and fetch the corresponding files. Note that files mentioned in `SUMMARY.md`
|
||||
but not present will be created.
|
||||
|
||||
The rendered output will maintain the same directory structure as the source for
|
||||
convenience. Large books will therefore remain structured when rendered.
|
||||
@@ -25,9 +25,9 @@ book-test/
|
||||
- The `book` directory is where your book is rendered. All the output is ready
|
||||
to be uploaded to a server to be seen by your audience.
|
||||
|
||||
- The `SUMMARY.md` file is the most important file, it's the skeleton of your
|
||||
book and is discussed in more detail [in another
|
||||
chapter](../format/summary.md)
|
||||
- The `SUMMARY.md` is the skeleton of your
|
||||
book, and is discussed in more detail [in another
|
||||
chapter](../format/summary.md).
|
||||
|
||||
#### Tip: Generate chapters from SUMMARY.md
|
||||
|
||||
@@ -52,3 +52,19 @@ directory called `theme` in your source directory so that you can modify it.
|
||||
|
||||
The theme is selectively overwritten, this means that if you don't want to
|
||||
overwrite a specific file, just delete it and the default file will be used.
|
||||
|
||||
#### --title
|
||||
|
||||
Specify a title for the book. If not supplied, an interactive prompt will ask for
|
||||
a title.
|
||||
|
||||
```bash
|
||||
mdbook init --title="my amazing book"
|
||||
```
|
||||
|
||||
#### --ignore
|
||||
|
||||
Create a `.gitignore` file configured to ignore the `book` directory created when [building] a book.
|
||||
If not supplied, an interactive prompt will ask whether it should be created.
|
||||
|
||||
[building]: build.md
|
||||
@@ -1,8 +1,15 @@
|
||||
# The serve command
|
||||
|
||||
The serve command is used to preview a book by serving it over HTTP at
|
||||
`localhost:3000` by default. Additionally it watches the book's directory for
|
||||
changes, rebuilding the book and refreshing clients for each change. A websocket
|
||||
The serve command is used to preview a book by serving it via HTTP at
|
||||
`localhost:3000` by default:
|
||||
|
||||
```bash
|
||||
mdbook serve
|
||||
```
|
||||
|
||||
The `serve` command watches the book's `src` directory for
|
||||
changes, rebuilding the book and refreshing clients for each change; this includes
|
||||
re-creating deleted files still mentioned in `SUMMARY.md`! 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
|
||||
@@ -17,24 +24,14 @@ root instead of the current working directory.
|
||||
mdbook serve path/to/book
|
||||
```
|
||||
|
||||
#### Server options
|
||||
### Server options
|
||||
|
||||
`serve` has four options: the HTTP port, the WebSocket port, the HTTP hostname
|
||||
to listen on, and the hostname for the browser to connect to for WebSockets.
|
||||
|
||||
For example: suppose you have an nginx server for SSL termination which has a
|
||||
public address of 192.168.1.100 on port 80 and proxied that to 127.0.0.1 on port
|
||||
8000\. To run use the nginx proxy do:
|
||||
The `serve` hostname defaults to `localhost`, and the port defaults to `3000`. Either option can be specified on the command line:
|
||||
|
||||
```bash
|
||||
mdbook serve path/to/book -p 8000 -n 127.0.0.1 --websocket-hostname 192.168.1.100
|
||||
mdbook serve path/to/book -p 8000 -n 127.0.0.1
|
||||
```
|
||||
|
||||
If you were to want live reloading for this you would need to proxy the
|
||||
websocket calls through nginx as well from `192.168.1.100:<WS_PORT>` to
|
||||
`127.0.0.1:<WS_PORT>`. The `-w` flag allows for the websocket port to be
|
||||
configured.
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the book in your
|
||||
@@ -55,5 +52,5 @@ contain file patterns described in the [gitignore
|
||||
documentation](https://git-scm.com/docs/gitignore). This can be useful for
|
||||
ignoring temporary files created by some editors.
|
||||
|
||||
_Note: Only `.gitignore` from book root directory is used. Global
|
||||
`$HOME/.gitignore` or `.gitignore` files in parent directories are not used._
|
||||
***Note:*** *Only the `.gitignore` from the book root directory is used. Global
|
||||
`$HOME/.gitignore` or `.gitignore` files in parent directories are not used.*
|
||||
@@ -43,7 +43,17 @@ mdbook test path/to/book
|
||||
The `--library-path` (`-L`) option allows you to add directories to the library
|
||||
search path used by `rustdoc` when it builds and tests the examples. Multiple
|
||||
directories can be specified with multiple options (`-L foo -L bar`) or with a
|
||||
comma-delimited list (`-L foo,bar`).
|
||||
comma-delimited list (`-L foo,bar`). The path should point to the Cargo
|
||||
[build cache](https://doc.rust-lang.org/cargo/guide/build-cache.html) `deps` directory that
|
||||
contains the build output of your project. For example, if your Rust project's book is in a directory
|
||||
named `my-book`, the following command would include the crate's dependencies when running `test`:
|
||||
|
||||
```shell
|
||||
mdbook test my-book -L target/debug/deps/
|
||||
```
|
||||
|
||||
See the `rustdoc` command-line [documentation](https://doc.rust-lang.org/rustdoc/command-line-arguments.html#-l--library-path-where-to-look-for-dependencies)
|
||||
for more information.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
The `watch` command is useful when you want your book to be rendered on every
|
||||
file change. You could repeatedly issue `mdbook build` every time a file is
|
||||
changed. But using `mdbook watch` once will watch your files and will trigger a
|
||||
build automatically whenever you modify a file.
|
||||
build automatically whenever you modify a file; this includes re-creating
|
||||
deleted files still mentioned in `SUMMARY.md`!
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
@@ -26,7 +26,7 @@ before_script:
|
||||
- cargo install-update -a
|
||||
|
||||
script:
|
||||
- mdbook build path/to/mybook && mdbook test path/to/mybook
|
||||
- mdbook build && mdbook test # In case of custom book path: mdbook build path/to/mybook && mdbook test path/to/mybook
|
||||
```
|
||||
|
||||
## Deploying Your Book to GitHub Pages
|
||||
@@ -39,6 +39,9 @@ permissions (or "repo" for private repositories). Go to your repository's Travis
|
||||
CI settings page and add an environment variable named `GITHUB_TOKEN` that is
|
||||
marked secure and *not* shown in the logs.
|
||||
|
||||
Whilst still in your repository's settings page, navigate to Options and change the
|
||||
Source on GitHub pages to `gh-pages`.
|
||||
|
||||
Then, append this snippet to your `.travis.yml` and update the path to the
|
||||
`book` directory:
|
||||
|
||||
@@ -47,14 +50,48 @@ deploy:
|
||||
provider: pages
|
||||
skip-cleanup: true
|
||||
github-token: $GITHUB_TOKEN
|
||||
local-dir: path/to/mybook/book
|
||||
local-dir: book # In case of custom book path: path/to/mybook/book
|
||||
keep-history: false
|
||||
on:
|
||||
branch: master
|
||||
branch: main
|
||||
```
|
||||
|
||||
That's it!
|
||||
|
||||
Note: Travis has a new [dplv2](https://blog.travis-ci.com/2019-08-27-deployment-tooling-dpl-v2-preview-release) configuration that is currently in beta. To use this new format, update your `.travis.yml` file to:
|
||||
|
||||
```yaml
|
||||
language: rust
|
||||
os: linux
|
||||
dist: xenial
|
||||
|
||||
cache:
|
||||
- cargo
|
||||
|
||||
rust:
|
||||
- stable
|
||||
|
||||
before_script:
|
||||
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
|
||||
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.3" mdbook)
|
||||
- cargo install-update -a
|
||||
|
||||
script:
|
||||
- mdbook build && mdbook test # In case of custom book path: mdbook build path/to/mybook && mdbook test path/to/mybook
|
||||
|
||||
deploy:
|
||||
provider: pages
|
||||
strategy: git
|
||||
edge: true
|
||||
cleanup: false
|
||||
github-token: $GITHUB_TOKEN
|
||||
local-dir: book # In case of custom book path: path/to/mybook/book
|
||||
keep-history: false
|
||||
on:
|
||||
branch: main
|
||||
target_branch: gh-pages
|
||||
```
|
||||
|
||||
### Deploying to GitHub Pages manually
|
||||
|
||||
If your CI doesn't support GitHub pages, or you're deploying somewhere else
|
||||
@@ -87,3 +124,31 @@ deploy: book
|
||||
git commit -m "deployed on $(shell date) by ${USER}" && \
|
||||
git push origin gh-pages
|
||||
```
|
||||
|
||||
## Deploying Your Book to GitLab Pages
|
||||
Inside your repository's project root, create a file named `.gitlab-ci.yml` with the following contents:
|
||||
```yml
|
||||
stages:
|
||||
- deploy
|
||||
|
||||
pages:
|
||||
stage: deploy
|
||||
image: rust
|
||||
variables:
|
||||
CARGO_HOME: $CI_PROJECT_DIR/cargo
|
||||
before_script:
|
||||
- export PATH="$PATH:$CARGO_HOME/bin"
|
||||
- mdbook --version || cargo install mdbook
|
||||
script:
|
||||
- mdbook build -d public
|
||||
rules:
|
||||
- if: '$CI_COMMIT_REF_NAME == "master"'
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
cache:
|
||||
paths:
|
||||
- $CARGO_HOME/bin
|
||||
```
|
||||
|
||||
After you commit and push this new file, GitLab CI will run and your book will be available!
|
||||
@@ -13,6 +13,7 @@ rough example of how this is accomplished in practice.
|
||||
- [mdbook-epub] - an EPUB renderer
|
||||
- [mdbook-test] - a program to run the book's contents through [rust-skeptic] to
|
||||
verify everything compiles and runs correctly (similar to `rustdoc --test`)
|
||||
- [mdbook-man] - generate manual pages from the book
|
||||
|
||||
This page will step you through creating your own alternative backend in the form
|
||||
of a simple word counting program. Although it will be written in Rust, there's
|
||||
@@ -377,6 +378,7 @@ the source code or ask questions.
|
||||
[mdbook-linkcheck]: https://github.com/Michael-F-Bryan/mdbook-linkcheck
|
||||
[mdbook-epub]: https://github.com/Michael-F-Bryan/mdbook-epub
|
||||
[mdbook-test]: https://github.com/Michael-F-Bryan/mdbook-test
|
||||
[mdbook-man]: https://github.com/vv9k/mdbook-man
|
||||
[rust-skeptic]: https://github.com/budziq/rust-skeptic
|
||||
[`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
|
||||
@@ -18,9 +18,9 @@ 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.
|
||||
|
||||
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.
|
||||
A preprocessor can be hard-coded to specify which backend(s) it should be run
|
||||
for with the `preprocessor.foo.renderer` key. For example, it doesn't make sense for
|
||||
[MathJax](../format/mathjax.md) to be used for non-HTML renderers.
|
||||
|
||||
```toml
|
||||
[book]
|
||||
@@ -34,8 +34,15 @@ command = "python3 /path/to/foo.py"
|
||||
renderer = ["html", "epub"]
|
||||
```
|
||||
|
||||
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.
|
||||
Once the preprocessor has been defined and the build process starts, mdBook executes the command defined in the `preprocessor.foo.command` key twice.
|
||||
The first time it runs the preprocessor to determine if it supports the given renderer.
|
||||
mdBook passes two arguments to the process: the first argument is the string `supports` and the second argument is the renderer name.
|
||||
The preprocessor should exit with a status code 0 if it supports the given renderer, or return a non-zero exit code if it does not.
|
||||
|
||||
If the preprocessor supports the renderer, then mdbook runs it a second time, passing JSON data into stdin.
|
||||
The JSON consists of an array of `[context, book]` where `context` is the serialized object [`PreprocessorContext`] and `book` is a [`Book`] object containing the content of the book.
|
||||
|
||||
The preprocessor should return the JSON format of the [`Book`] object to stdout, with any modifications it wishes to perform.
|
||||
|
||||
The easiest way to get started is by creating your own implementation of the
|
||||
`Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
|
||||
@@ -106,6 +113,33 @@ fn remove_emphasis(
|
||||
|
||||
For everything else, have a look [at the complete example][example].
|
||||
|
||||
## Implementing a preprocessor with a different language
|
||||
|
||||
The fact that mdBook utilizes stdin and stdout to communicate with the preprocessors makes it easy to implement them in a language other than Rust.
|
||||
The following code shows how to implement a simple preprocessor in Python, which will modify the content of the first chapter.
|
||||
The example below follows the configuration shown above with `preprocessor.foo.command` actually pointing to a Python script.
|
||||
|
||||
```python
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1: # we check if we received any argument
|
||||
if sys.argv[1] == "supports":
|
||||
# then we are good to return an exit status code of 0, since the other argument will just be the renderer's name
|
||||
sys.exit(0)
|
||||
|
||||
# load both the context and the book representations from stdin
|
||||
context, book = json.load(sys.stdin)
|
||||
# and now, we can just modify the content of the first chapter
|
||||
book['sections'][0]['Chapter']['content'] = '# Hello'
|
||||
# we are done with the book's modification, we can just print it to stdout,
|
||||
print(json.dumps(book))
|
||||
```
|
||||
|
||||
|
||||
|
||||
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
|
||||
[pc]: https://crates.io/crates/pulldown-cmark
|
||||
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
|
||||
@@ -113,3 +147,5 @@ For everything else, have a look [at the complete example][example].
|
||||
[an example no-op preprocessor]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||
[`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
|
||||
[`PreprocessorContext`]: https://docs.rs/mdbook/latest/mdbook/preprocess/struct.PreprocessorContext.html
|
||||
[`Book`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html
|
||||
12
guide/src/format/configuration/README.md
Normal file
12
guide/src/format/configuration/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Configuration
|
||||
|
||||
This section details the configuration options available in the ***book.toml***:
|
||||
- **[General]** configuration including the `book`, `rust`, `build` sections
|
||||
- **[Preprocessor]** configuration for default and custom book preprocessors
|
||||
- **[Renderer]** configuration for the HTML, Markdown and custom renderers
|
||||
- **[Environment Variable]** configuration for overriding configuration options in your environment
|
||||
|
||||
[General]: general.md
|
||||
[Preprocessor]: preprocessors.md
|
||||
[Renderer]: renderers.md
|
||||
[Environment Variable]: environment-variables.md
|
||||
38
guide/src/format/configuration/environment-variables.md
Normal file
38
guide/src/format/configuration/environment-variables.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Environment Variables
|
||||
|
||||
All configuration values can be overridden from the command line by setting the
|
||||
corresponding environment variable. Because many operating systems restrict
|
||||
environment variables to be alphanumeric characters or `_`, the configuration
|
||||
key needs to be formatted slightly differently to the normal `foo.bar.baz` form.
|
||||
|
||||
Variables starting with `MDBOOK_` are used for configuration. The key is created
|
||||
by removing the `MDBOOK_` prefix and turning the resulting string into
|
||||
`kebab-case`. Double underscores (`__`) separate nested keys, while a single
|
||||
underscore (`_`) is replaced with a dash (`-`).
|
||||
|
||||
For example:
|
||||
|
||||
- `MDBOOK_foo` -> `foo`
|
||||
- `MDBOOK_FOO` -> `foo`
|
||||
- `MDBOOK_FOO__BAR` -> `foo.bar`
|
||||
- `MDBOOK_FOO_BAR` -> `foo-bar`
|
||||
- `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
|
||||
|
||||
So by setting the `MDBOOK_BOOK__TITLE` environment variable you can override the
|
||||
book's title without needing to touch your `book.toml`.
|
||||
|
||||
> **Note:** To facilitate setting more complex config items, the value of an
|
||||
> environment variable is first parsed as JSON, falling back to a string if the
|
||||
> parse fails.
|
||||
>
|
||||
> This means, if you so desired, you could override all book metadata when
|
||||
> building the book with something like
|
||||
>
|
||||
> ```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.
|
||||
97
guide/src/format/configuration/general.md
Normal file
97
guide/src/format/configuration/general.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# General Configuration
|
||||
|
||||
You can configure the parameters for your book in the ***book.toml*** file.
|
||||
|
||||
Here is an example of what a ***book.toml*** file might look like:
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
author = "John Doe"
|
||||
description = "The example book covers examples."
|
||||
|
||||
[rust]
|
||||
edition = "2018"
|
||||
|
||||
[build]
|
||||
build-dir = "my-example-book"
|
||||
create-missing = false
|
||||
|
||||
[preprocessor.index]
|
||||
|
||||
[preprocessor.links]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["custom.css"]
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 15
|
||||
```
|
||||
|
||||
## Supported configuration options
|
||||
|
||||
It is important to note that **any** relative path specified in the
|
||||
configuration will always be taken relative from the root of the book where the
|
||||
configuration file is located.
|
||||
|
||||
### General metadata
|
||||
|
||||
This is general information about your book.
|
||||
|
||||
- **title:** The title of the book
|
||||
- **authors:** The author(s) of the book
|
||||
- **description:** A description for the book, which is added as meta
|
||||
information in the html `<head>` of each page
|
||||
- **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
|
||||
[book]
|
||||
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"
|
||||
```
|
||||
|
||||
### Rust options
|
||||
|
||||
Options for the Rust language, relevant to running tests and playground
|
||||
integration.
|
||||
|
||||
- **edition**: Rust edition to use by default for the code snippets. Default
|
||||
is "2015". Individual code blocks can be controlled with the `edition2015`,
|
||||
`edition2018` or `edition2021` annotations, such as:
|
||||
|
||||
~~~text
|
||||
```rust,edition2015
|
||||
// This only works in 2015.
|
||||
let try = true;
|
||||
```
|
||||
~~~
|
||||
|
||||
### Build options
|
||||
|
||||
This controls the build process of your book.
|
||||
|
||||
- **build-dir:** The directory to put the rendered book in. By default this is
|
||||
`book/` in the book's root directory.
|
||||
- **create-missing:** By default, any missing files specified in `SUMMARY.md`
|
||||
will be created when the book is built (i.e. `create-missing = true`). If this
|
||||
is `false` then the build process will instead exit with an error if any files
|
||||
do not exist.
|
||||
- **use-default-preprocessors:** Disable the default preprocessors of (`links` &
|
||||
`index`) by setting this option to `false`.
|
||||
|
||||
If you have the same, and/or other preprocessors declared via their table
|
||||
of configuration, they will run instead.
|
||||
|
||||
- For clarity, with no preprocessor configuration, the default `links` and
|
||||
`index` will run.
|
||||
- Setting `use-default-preprocessors = false` will disable these
|
||||
default preprocessors from running.
|
||||
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
|
||||
`use-default-preprocessors` that `links` it will run.
|
||||
80
guide/src/format/configuration/preprocessors.md
Normal file
80
guide/src/format/configuration/preprocessors.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Configuring Preprocessors
|
||||
|
||||
The following preprocessors are available and included by default:
|
||||
|
||||
- `links`: Expand the `{{ #playground }}`, `{{ #include }}`, and `{{ #rustdoc_include }}` handlebars
|
||||
helpers in a chapter to include the contents of a file.
|
||||
- `index`: Convert all chapter files named `README.md` into `index.md`. That is
|
||||
to say, all `README.md` would be rendered to an index file `index.html` in the
|
||||
rendered book.
|
||||
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
[build]
|
||||
build-dir = "build"
|
||||
create-missing = false
|
||||
|
||||
[preprocessor.links]
|
||||
|
||||
[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.
|
||||
|
||||
For example
|
||||
|
||||
```toml
|
||||
[preprocessor.links]
|
||||
# set the renderers this preprocessor will run for
|
||||
renderers = ["html"]
|
||||
some_extra_feature = true
|
||||
```
|
||||
|
||||
#### Locking a Preprocessor dependency to a renderer
|
||||
|
||||
You can explicitly specify that a preprocessor should run for a renderer by
|
||||
binding the two together.
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
### Require A Certain Order
|
||||
|
||||
The order in which preprocessors are run can be controlled with the `before` and `after` fields.
|
||||
For example, suppose you want your `linenos` preprocessor to process lines that may have been `{{#include}}`d; then you want it to run after the built-in `links` preprocessor, which you can require using either the `before` or `after` field:
|
||||
|
||||
```toml
|
||||
[preprocessor.linenos]
|
||||
after = [ "links" ]
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```toml
|
||||
[preprocessor.links]
|
||||
before = [ "linenos" ]
|
||||
```
|
||||
|
||||
It would also be possible, though redundant, to specify both of the above in the same config file.
|
||||
|
||||
Preprocessors having the same priority specified through `before` and `after` are sorted by name.
|
||||
Any infinite loops will be detected and produce an error.
|
||||
@@ -1,161 +1,4 @@
|
||||
# Configuration
|
||||
|
||||
You can configure the parameters for your book in the ***book.toml*** file.
|
||||
|
||||
Here is an example of what a ***book.toml*** file might look like:
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
author = "John Doe"
|
||||
description = "The example book covers examples."
|
||||
|
||||
[rust]
|
||||
edition = "2018"
|
||||
|
||||
[build]
|
||||
build-dir = "my-example-book"
|
||||
create-missing = false
|
||||
|
||||
[preprocessor.index]
|
||||
|
||||
[preprocessor.links]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["custom.css"]
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 15
|
||||
```
|
||||
|
||||
## Supported configuration options
|
||||
|
||||
It is important to note that **any** relative path specified in the
|
||||
configuration will always be taken relative from the root of the book where the
|
||||
configuration file is located.
|
||||
|
||||
### General metadata
|
||||
|
||||
This is general information about your book.
|
||||
|
||||
- **title:** The title of the book
|
||||
- **authors:** The author(s) of the book
|
||||
- **description:** A description for the book, which is added as meta
|
||||
information in the html `<head>` of each page
|
||||
- **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
|
||||
[book]
|
||||
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"
|
||||
```
|
||||
|
||||
### Rust options
|
||||
|
||||
Options for the Rust language, relevant to running tests and playground
|
||||
integration.
|
||||
|
||||
- **edition**: Rust edition to use by default for the code snippets. Default
|
||||
is "2015". Individual code blocks can be controlled with the `edition2015`
|
||||
or `edition2018` annotations, such as:
|
||||
|
||||
~~~text
|
||||
```rust,edition2015
|
||||
// This only works in 2015.
|
||||
let try = true;
|
||||
```
|
||||
~~~
|
||||
|
||||
### Build options
|
||||
|
||||
This controls the build process of your book.
|
||||
|
||||
- **build-dir:** The directory to put the rendered book in. By default this is
|
||||
`book/` in the book's root directory.
|
||||
- **create-missing:** By default, any missing files specified in `SUMMARY.md`
|
||||
will be created when the book is built (i.e. `create-missing = true`). If this
|
||||
is `false` then the build process will instead exit with an error if any files
|
||||
do not exist.
|
||||
- **use-default-preprocessors:** Disable the default preprocessors of (`links` &
|
||||
`index`) by setting this option to `false`.
|
||||
|
||||
If you have the same, and/or other preprocessors declared via their table
|
||||
of configuration, they will run instead.
|
||||
|
||||
- For clarity, with no preprocessor configuration, the default `links` and
|
||||
`index` will run.
|
||||
- Setting `use-default-preprocessors = false` will disable these
|
||||
default preprocessors from running.
|
||||
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
|
||||
`use-default-preprocessors` that `links` it will run.
|
||||
|
||||
## Configuring Preprocessors
|
||||
|
||||
The following preprocessors are available and included by default:
|
||||
|
||||
- `links`: Expand the `{{ #playground }}`, `{{ #include }}`, and `{{ #rustdoc_include }}` handlebars
|
||||
helpers in a chapter to include the contents of a file.
|
||||
- `index`: Convert all chapter files named `README.md` into `index.md`. That is
|
||||
to say, all `README.md` would be rendered to an index file `index.html` in the
|
||||
rendered book.
|
||||
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
[build]
|
||||
build-dir = "build"
|
||||
create-missing = false
|
||||
|
||||
[preprocessor.links]
|
||||
|
||||
[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.
|
||||
|
||||
For example
|
||||
|
||||
```toml
|
||||
[preprocessor.links]
|
||||
# set the renderers this preprocessor will run for
|
||||
renderers = ["html"]
|
||||
some_extra_feature = true
|
||||
```
|
||||
|
||||
#### Locking a Preprocessor dependency to a renderer
|
||||
|
||||
You can explicitly specify that a preprocessor should run for a renderer by
|
||||
binding the two together.
|
||||
|
||||
```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
|
||||
# Configuring Renderers
|
||||
|
||||
### HTML renderer options
|
||||
|
||||
@@ -175,7 +18,7 @@ The following configuration options are available:
|
||||
CSS media query. Defaults to `navy`.
|
||||
- **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
|
||||
- **mathjax-support:** Adds support for [MathJax](../mathjax.md). Defaults to
|
||||
`false`.
|
||||
- **copy-fonts:** Copies fonts.css and respective font files to the output directory and use them in the default theme. Defaults to `true`.
|
||||
- **google-analytics:** If you use Google Analytics, this option lets you enable
|
||||
@@ -187,6 +30,9 @@ The following configuration options are available:
|
||||
- **additional-js:** If you need to add some behaviour to your book without
|
||||
removing the current behaviour, you can specify a set of JavaScript files that
|
||||
will be loaded alongside the default one.
|
||||
- **print:** A subtable for configuration print settings. mdBook by default adds
|
||||
support for printing out the book as a single page. This is accessed using the
|
||||
print icon on the top right of the book.
|
||||
- **no-section-label:** mdBook by defaults adds section label in table of
|
||||
contents column. For example, "1.", "2.1". Set this option to true to disable
|
||||
those labels. Defaults to `false`.
|
||||
@@ -198,18 +44,37 @@ The following configuration options are available:
|
||||
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`.
|
||||
- **edit-url-template:** Edit url template, when provided shows a
|
||||
"Suggest an edit" button for directly jumping to editing the currently
|
||||
viewed page. For e.g. GitHub projects set this to
|
||||
`https://github.com/<owner>/<repo>/edit/master/{path}` or for
|
||||
Bitbucket projects set it to
|
||||
`https://bitbucket.org/<owner>/<repo>/src/master/{path}?mode=edit`
|
||||
where {path} will be replaced with the full path of the file in the
|
||||
repository.
|
||||
- **redirect:** A subtable used for generating redirects when a page is moved.
|
||||
The table contains key-value pairs where the key is where the redirect file
|
||||
needs to be created, as an absolute path from the build directory, (e.g.
|
||||
`/appendices/bibliography.html`). The value can be any valid URI the
|
||||
browser should navigate to (e.g. `https://rust-lang.org/`,
|
||||
`/overview.html`, or `../bibliography.html`).
|
||||
- **input-404:** The name of the markdown file used for misssing files.
|
||||
- **input-404:** The name of the markdown file used for missing files.
|
||||
The corresponding output file will be the same, with the extension replaced with `html`.
|
||||
Defaults to `404.md`.
|
||||
- **site-url:** The url where the book will be hosted. This is required to ensure
|
||||
navigation links and script/css imports in the 404 file work correctly, even when accessing
|
||||
urls in subdirectories. Defaults to `/`.
|
||||
- **cname:** The DNS subdomain or apex domain at which your book will be hosted.
|
||||
This string will be written to a file named CNAME in the root of your site, as
|
||||
required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages
|
||||
site*][custom domain]).
|
||||
|
||||
[custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
||||
|
||||
Available configuration options for the `[output.html.print]` table:
|
||||
|
||||
- **enable:** Enable print support. When `false`, all print support will not be
|
||||
rendered. Defaults to `true`.
|
||||
|
||||
Available configuration options for the `[output.html.fold]` table:
|
||||
|
||||
@@ -272,9 +137,14 @@ additional-js = ["custom.js"]
|
||||
no-section-label = false
|
||||
git-repository-url = "https://github.com/rust-lang/mdBook"
|
||||
git-repository-icon = "fa-github"
|
||||
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||
site-url = "/example-book/"
|
||||
cname = "myproject.rs"
|
||||
input-404 = "not-found.md"
|
||||
|
||||
[output.html.print]
|
||||
enable = true
|
||||
|
||||
[output.html.fold]
|
||||
enable = false
|
||||
level = 0
|
||||
@@ -318,13 +188,13 @@ Enable it by adding an empty table to your `book.toml` as follows:
|
||||
There are no configuration options for the Markdown renderer at this time;
|
||||
only whether it is enabled or disabled.
|
||||
|
||||
See [the preprocessors documentation](#configuring-preprocessors) for how to
|
||||
See [the preprocessors documentation](preprocessors.md) for how to
|
||||
specify which preprocessors should run before the Markdown renderer.
|
||||
|
||||
### 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
|
||||
`book.toml`. Similar to [preprocessors](preprocessors.md) this will
|
||||
instruct `mdbook` to pass a representation of the book to `mdbook-foo` for
|
||||
rendering. See the [alternative backends] chapter for more detail.
|
||||
|
||||
@@ -336,43 +206,4 @@ anything under `[output.foo]`). mdBook checks for two common fields:
|
||||
- **optional:** If `true`, then the command will be ignored if it is not
|
||||
installed, otherwise mdBook will fail with an error. Defaults to `false`.
|
||||
|
||||
[alternative backends]: ../for_developers/backends.md
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All configuration values can be overridden from the command line by setting the
|
||||
corresponding environment variable. Because many operating systems restrict
|
||||
environment variables to be alphanumeric characters or `_`, the configuration
|
||||
key needs to be formatted slightly differently to the normal `foo.bar.baz` form.
|
||||
|
||||
Variables starting with `MDBOOK_` are used for configuration. The key is created
|
||||
by removing the `MDBOOK_` prefix and turning the resulting string into
|
||||
`kebab-case`. Double underscores (`__`) separate nested keys, while a single
|
||||
underscore (`_`) is replaced with a dash (`-`).
|
||||
|
||||
For example:
|
||||
|
||||
- `MDBOOK_foo` -> `foo`
|
||||
- `MDBOOK_FOO` -> `foo`
|
||||
- `MDBOOK_FOO__BAR` -> `foo.bar`
|
||||
- `MDBOOK_FOO_BAR` -> `foo-bar`
|
||||
- `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
|
||||
|
||||
So by setting the `MDBOOK_BOOK__TITLE` environment variable you can override the
|
||||
book's title without needing to touch your `book.toml`.
|
||||
|
||||
> **Note:** To facilitate setting more complex config items, the value of an
|
||||
> environment variable is first parsed as JSON, falling back to a string if the
|
||||
> parse fails.
|
||||
>
|
||||
> This means, if you so desired, you could override all book metadata when
|
||||
> building the book with something like
|
||||
>
|
||||
> ```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.
|
||||
[alternative backends]: ../../for_developers/backends.md
|
||||
6
guide/src/format/example.rs
Normal file
6
guide/src/format/example.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
fn main() {
|
||||
println!("Hello World!");
|
||||
#
|
||||
# // You can even hide lines! :D
|
||||
# println!("I am hidden! Expand the code snippet to see me");
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
# mdBook-specific markdown
|
||||
# mdBook-specific features
|
||||
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them
|
||||
with a `#` [in the same way that Rustdoc does][rustdoc-hide].
|
||||
with a `#` [like you would with Rustdoc][rustdoc-hide].
|
||||
|
||||
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example
|
||||
|
||||
@@ -21,7 +21,7 @@ Will render as
|
||||
```rust
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 7;
|
||||
let y = 6;
|
||||
|
||||
println!("{}", x + y);
|
||||
# }
|
||||
@@ -37,10 +37,10 @@ With the following syntax, you can include files into your book:
|
||||
|
||||
The path to the file has to be relative from the current source file.
|
||||
|
||||
mdBook will interpret included files as markdown. Since the include command
|
||||
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.
|
||||
interpreting them.
|
||||
|
||||
````hbs
|
||||
```
|
||||
@@ -49,7 +49,7 @@ interpretting them.
|
||||
````
|
||||
|
||||
## Including portions of a file
|
||||
Often you only need a specific part of the file e.g. relevant lines for an
|
||||
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
|
||||
@@ -68,8 +68,8 @@ 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
|
||||
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:
|
||||
@@ -156,7 +156,7 @@ To call the `add_one` function, we pass it an `i32` and bind the returned value
|
||||
#
|
||||
# fn add_one(num: i32) -> i32 {
|
||||
# num + 1
|
||||
#}
|
||||
# }
|
||||
```
|
||||
````
|
||||
|
||||
@@ -170,7 +170,7 @@ That is, it looks like this (click the "expand" icon to see the rest of the file
|
||||
#
|
||||
# fn add_one(num: i32) -> i32 {
|
||||
# num + 1
|
||||
#}
|
||||
# }
|
||||
```
|
||||
|
||||
## Inserting runnable Rust files
|
||||
@@ -192,3 +192,12 @@ Here is what a rendered code snippet looks like:
|
||||
{{#playground example.rs}}
|
||||
|
||||
[Rust Playground]: https://play.rust-lang.org/
|
||||
|
||||
## Controlling page \<title\>
|
||||
|
||||
A chapter can set a \<title\> that is different from its entry in the table of
|
||||
contents (sidebar) by including a `\{{#title ...}}` near the top of the page.
|
||||
|
||||
```hbs
|
||||
\{{#title My Title}}
|
||||
```
|
||||
99
guide/src/format/summary.md
Normal file
99
guide/src/format/summary.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# SUMMARY.md
|
||||
|
||||
The summary file is used by mdBook to know what chapters to include, in what
|
||||
order they should appear, what their hierarchy is and where the source files
|
||||
are. Without this file, there is no book.
|
||||
|
||||
This markdown file must be named `SUMMARY.md`. Its formatting
|
||||
is very strict and must follow the structure outlined below to allow for easy
|
||||
parsing. Any element not specified below, be it formatting or textual, is likely
|
||||
to be ignored at best, or may cause an error when attempting to build the book.
|
||||
|
||||
### Structure
|
||||
|
||||
1. ***Title*** - While optional, it's common practice to begin with a title, generally <code
|
||||
class="language-markdown"># Summary</code>. This is ignored by the parser however, and
|
||||
can be omitted.
|
||||
```markdown
|
||||
# Summary
|
||||
```
|
||||
|
||||
1. ***Prefix Chapter*** - Before the main numbered chapters, prefix chapters can be added
|
||||
that will not be numbered. This is useful for forewords,
|
||||
introductions, etc. There are, however, some constraints. Prefix chapters cannot be
|
||||
nested; they should all be on the root level. And you can not add
|
||||
prefix chapters once you have added numbered chapters.
|
||||
```markdown
|
||||
[A Prefix Chapter](relative/path/to/markdown.md)
|
||||
|
||||
- [First Chapter](relative/path/to/markdown2.md)
|
||||
```
|
||||
|
||||
1. ***Part Title*** - Headers can be used as a title for the following numbered
|
||||
chapters. This can be used to logically separate different sections
|
||||
of the book. The title is rendered as unclickable text.
|
||||
Titles are optional, and the numbered chapters can be broken into as many
|
||||
parts as desired.
|
||||
```markdown
|
||||
# My Part Tile
|
||||
|
||||
- [First Chapter](relative/path/to/markdown.md)
|
||||
```
|
||||
|
||||
1. ***Numbered Chapter*** - Numbered chapters outline the main content of the book
|
||||
and can be nested, resulting in a nice hierarchy
|
||||
(chapters, sub-chapters, etc.).
|
||||
```markdown
|
||||
# Title of Part
|
||||
|
||||
- [First Chapter](relative/path/to/markdown.md)
|
||||
- [Second Chapter](relative/path/to/markdown2.md)
|
||||
- [Sub Chapter](relative/path/to/markdown3.md)
|
||||
|
||||
# Title of Another Part
|
||||
|
||||
- [Another Chapter](relative/path/to/markdown4.md)
|
||||
```
|
||||
Numbered chapters can be denoted with either `-` or `*` (do not mix delimiters).
|
||||
|
||||
1. ***Suffix Chapter*** - Like prefix chapters, suffix chapters are unnumbered, but they come after
|
||||
numbered chapters.
|
||||
```markdown
|
||||
- [Last Chapter](relative/path/to/markdown.md)
|
||||
|
||||
[Title of Suffix Chapter](relative/path/to/markdown2.md)
|
||||
```
|
||||
|
||||
1. ***Draft chapters*** - Draft chapters are chapters without a file and thus content.
|
||||
The purpose of a draft chapter is to signal future chapters still to be written.
|
||||
Or when still laying out the structure of the book to avoid creating the files
|
||||
while you are still changing the structure of the book a lot.
|
||||
Draft chapters will be rendered in the HTML renderer as disabled links in the table
|
||||
of contents, as you can see for the next chapter in the table of contents on the left.
|
||||
Draft chapters are written like normal chapters but without writing the path to the file.
|
||||
```markdown
|
||||
- [Draft Chapter]()
|
||||
```
|
||||
|
||||
1. ***Separators*** - Separators can be added before, in between, and after any other element. They result
|
||||
in an HTML rendered line in the built table of contents. A separator is
|
||||
a line containing exclusively dashes and at least three of them: `---`.
|
||||
```markdown
|
||||
# My Part Title
|
||||
|
||||
[A Prefix Chapter](relative/path/to/markdown.md)
|
||||
|
||||
---
|
||||
|
||||
- [First Chapter](relative/path/to/markdown2.md)
|
||||
```
|
||||
|
||||
|
||||
### Example
|
||||
|
||||
Below is the markdown source for the `SUMMARY.md` for this guide, with the resulting table
|
||||
of contents as rendered to the left.
|
||||
|
||||
```markdown
|
||||
{{#include ../SUMMARY.md}}
|
||||
```
|
||||
@@ -14,9 +14,11 @@ Here are the files you can override:
|
||||
- **_index.hbs_** is the handlebars template.
|
||||
- **_head.hbs_** is appended to the HTML `<head>` section.
|
||||
- **_header.hbs_** content is appended on top of every book page.
|
||||
- **_book.css_** is the style used in the output. If you want to change the
|
||||
design of your book, this is probably the file you want to modify. Sometimes
|
||||
in conjunction with `index.hbs` when you want to radically change the layout.
|
||||
- **_css/_** contains the CSS files for styling the book.
|
||||
- **_css/chrome.css_** is for UI elements.
|
||||
- **_css/general.css_** is the base styles.
|
||||
- **_css/print.css_** is the style for printer output.
|
||||
- **_css/variables.css_** contains variables used in other CSS files.
|
||||
- **_book.js_** is mostly used to add client side functionality, like hiding /
|
||||
un-hiding the sidebar, changing the theme, ...
|
||||
- **_highlight.js_** is the JavaScript that is used to highlight code snippets,
|
||||
@@ -40,5 +42,5 @@ If you completely replace all built-in themes, be sure to also set
|
||||
[`output.html.preferred-dark-theme`] in the config, which defaults to the
|
||||
built-in `navy` theme.
|
||||
|
||||
[`output.html.preferred-dark-theme`]: ../config.md#html-renderer-options
|
||||
[`output.html.preferred-dark-theme`]: ../configuration/renderers.md#html-renderer-options
|
||||
[newer browsers]: https://caniuse.com/#feat=link-icon-svg
|
||||
@@ -33,7 +33,7 @@ Note the new `Undo Changes` button in the editable playgrounds.
|
||||
## Customizing the Editor
|
||||
|
||||
By default, the editor is the [Ace](https://ace.c9.io/) editor, but, if desired,
|
||||
the functionality may be overriden by providing a different folder:
|
||||
the functionality may be overridden by providing a different folder:
|
||||
|
||||
```toml
|
||||
[output.html.playground]
|
||||
@@ -42,5 +42,5 @@ editor = "/path/to/editor"
|
||||
```
|
||||
|
||||
Note that for the editor changes to function correctly, the `book.js` inside of
|
||||
the `theme` folder will need to be overriden as it has some couplings with the
|
||||
the `theme` folder will need to be overridden as it has some couplings with the
|
||||
default Ace editor.
|
||||
@@ -19,7 +19,7 @@ Here is a list of the properties that are exposed:
|
||||
|
||||
- ***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 used for the current page. This is identical to `{{ book_title }} - {{ chapter_title }}` unless `book_title` is not set in which case it just defaults to the `chapter_title`.
|
||||
- ***title*** Title used for the current page. This is identical to `{{ chapter_title }} - {{ book_title }}` unless `book_title` is not set in which case it just defaults to the `chapter_title`.
|
||||
- ***book_title*** Title of the book, as specified in `book.toml`
|
||||
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
||||
|
||||
@@ -1,16 +1,67 @@
|
||||
# Syntax Highlighting
|
||||
|
||||
For syntax highlighting I use [Highlight.js](https://highlightjs.org) with a
|
||||
custom theme.
|
||||
mdBook uses [Highlight.js](https://highlightjs.org) with a custom theme
|
||||
for syntax highlighting.
|
||||
|
||||
Automatic language detection has been turned off, so you will probably want to
|
||||
specify the programming language you use like this
|
||||
specify the programming language you use like this:
|
||||
|
||||
<pre><code class="language-markdown">```rust
|
||||
~~~markdown
|
||||
```rust
|
||||
fn main() {
|
||||
// Some code
|
||||
}
|
||||
```</code></pre>
|
||||
```
|
||||
~~~
|
||||
|
||||
## Supported languages
|
||||
|
||||
These languages are supported by default, but you can add more by supplying
|
||||
your own `highlight.js` file:
|
||||
|
||||
- apache
|
||||
- armasm
|
||||
- bash
|
||||
- c
|
||||
- coffeescript
|
||||
- cpp
|
||||
- csharp
|
||||
- css
|
||||
- d
|
||||
- diff
|
||||
- go
|
||||
- handlebars
|
||||
- haskell
|
||||
- http
|
||||
- ini
|
||||
- java
|
||||
- javascript
|
||||
- json
|
||||
- julia
|
||||
- kotlin
|
||||
- less
|
||||
- lua
|
||||
- makefile
|
||||
- markdown
|
||||
- nginx
|
||||
- objectivec
|
||||
- perl
|
||||
- php
|
||||
- plaintext
|
||||
- properties
|
||||
- python
|
||||
- r
|
||||
- ruby
|
||||
- rust
|
||||
- scala
|
||||
- scss
|
||||
- shell
|
||||
- sql
|
||||
- swift
|
||||
- typescript
|
||||
- x86asm
|
||||
- xml
|
||||
- yaml
|
||||
|
||||
## Custom theme
|
||||
Like the rest of the theme, the files used for syntax highlighting can be
|
||||
@@ -61,10 +112,10 @@ everyone can benefit from it.**
|
||||
## Improve default theme
|
||||
|
||||
If you think the default theme doesn't look quite right for a specific language,
|
||||
or could be improved. Feel free to [submit a new
|
||||
or could be improved, feel free to [submit a new
|
||||
issue](https://github.com/rust-lang/mdBook/issues) explaining what you
|
||||
have in mind and I will take a look at it.
|
||||
|
||||
You could also create a pull-request with the proposed improvements.
|
||||
|
||||
Overall the theme should be light and sober, without to many flashy colors.
|
||||
Overall the theme should be light and sober, without too many flashy colors.
|
||||
@@ -15,6 +15,10 @@ shout-out to them!
|
||||
- [projektir](https://github.com/projektir)
|
||||
- [Phaiax](https://github.com/Phaiax)
|
||||
- Matt Ickstadt ([mattico](https://github.com/mattico))
|
||||
- Weihang Lo ([@weihanglo](https://github.com/weihanglo))
|
||||
- Weihang Lo ([weihanglo](https://github.com/weihanglo))
|
||||
- Avision Ho ([avisionh](https://github.com/avisionh))
|
||||
- Vivek Akupatni ([apatniv](https://github.com/apatniv))
|
||||
- Eric Huss ([ehuss](https://github.com/ehuss))
|
||||
- Josh Rotenberg ([joshrotenberg](https://github.com/joshrotenberg))
|
||||
|
||||
If you feel you're missing from this list, feel free to add yourself in a PR.
|
||||
If you feel you're missing from this list, feel free to add yourself in a PR.
|
||||
@@ -14,14 +14,15 @@ pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book>
|
||||
let summary_md = src_dir.join("SUMMARY.md");
|
||||
|
||||
let mut summary_content = String::new();
|
||||
File::open(summary_md)
|
||||
.with_context(|| "Couldn't open SUMMARY.md")?
|
||||
File::open(&summary_md)
|
||||
.with_context(|| format!("Couldn't open SUMMARY.md in {:?} directory", src_dir))?
|
||||
.read_to_string(&mut summary_content)?;
|
||||
|
||||
let summary = parse_summary(&summary_content).with_context(|| "Summary parsing failed")?;
|
||||
let summary = parse_summary(&summary_content)
|
||||
.with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
|
||||
|
||||
if cfg.create_missing {
|
||||
create_missing(&src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
|
||||
create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
|
||||
}
|
||||
|
||||
load_book_from_disk(&summary, src_dir)
|
||||
@@ -49,7 +50,9 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||
}
|
||||
debug!("Creating missing file {}", filename.display());
|
||||
|
||||
let mut f = File::create(&filename)?;
|
||||
let mut f = File::create(&filename).with_context(|| {
|
||||
format!("Unable to create missing file: {}", filename.display())
|
||||
})?;
|
||||
writeln!(f, "# {}", link.name)?;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +66,7 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||
|
||||
/// A dumb tree structure representing a book.
|
||||
///
|
||||
/// For the moment a book is just a collection of `BookItems` which are
|
||||
/// For the moment a book is just a collection of [`BookItems`] which are
|
||||
/// accessible by either iterating (immutably) over the book with [`iter()`], or
|
||||
/// recursively applying a closure to each section to mutate the chapters, using
|
||||
/// [`for_each_mut()`].
|
||||
@@ -157,7 +160,9 @@ pub struct Chapter {
|
||||
pub sub_items: Vec<BookItem>,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
pub path: Option<PathBuf>,
|
||||
/// An ordered list of the names of each chapter above this one, in the hierarchy.
|
||||
/// The chapter's source file, relative to the `SUMMARY.md` file.
|
||||
pub source_path: Option<PathBuf>,
|
||||
/// An ordered list of the names of each chapter above this one in the hierarchy.
|
||||
pub parent_names: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -166,36 +171,36 @@ impl Chapter {
|
||||
pub fn new<P: Into<PathBuf>>(
|
||||
name: &str,
|
||||
content: String,
|
||||
path: P,
|
||||
p: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Chapter {
|
||||
let path: PathBuf = p.into();
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content,
|
||||
path: Some(path.into()),
|
||||
path: Some(path.clone()),
|
||||
source_path: Some(path),
|
||||
parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new draft chapter that is not attached to a source markdown file and has
|
||||
/// thus no content.
|
||||
/// Create a new draft chapter that is not attached to a source markdown file (and thus
|
||||
/// has no content).
|
||||
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content: String::new(),
|
||||
path: None,
|
||||
source_path: None,
|
||||
parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file
|
||||
/// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
|
||||
pub fn is_draft_chapter(&self) -> bool {
|
||||
match self.path {
|
||||
Some(_) => false,
|
||||
None => true,
|
||||
}
|
||||
self.path.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +269,10 @@ fn load_chapter<P: AsRef<Path>>(
|
||||
format!("Unable to read \"{}\" ({})", link.name, location.display())
|
||||
})?;
|
||||
|
||||
if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
|
||||
content.replace_range(..3, "");
|
||||
}
|
||||
|
||||
let stripped = location
|
||||
.strip_prefix(&src_dir)
|
||||
.expect("Chapters are always inside a book");
|
||||
@@ -273,7 +282,7 @@ fn load_chapter<P: AsRef<Path>>(
|
||||
Chapter::new_draft(&link.name, parent_names.clone())
|
||||
};
|
||||
|
||||
let mut sub_item_parents = parent_names.clone();
|
||||
let mut sub_item_parents = parent_names;
|
||||
|
||||
ch.number = link.number.clone();
|
||||
|
||||
@@ -295,8 +304,6 @@ fn load_chapter<P: AsRef<Path>>(
|
||||
///
|
||||
/// This struct shouldn't be created directly, instead prefer the
|
||||
/// [`Book::iter()`] method.
|
||||
///
|
||||
/// [`Book::iter()`]: struct.Book.html#method.iter
|
||||
pub struct BookItems<'a> {
|
||||
items: VecDeque<&'a BookItem>,
|
||||
}
|
||||
@@ -374,7 +381,7 @@ And here is some \
|
||||
|
||||
root.nested_items.push(second.clone().into());
|
||||
root.nested_items.push(SummaryItem::Separator);
|
||||
root.nested_items.push(second.clone().into());
|
||||
root.nested_items.push(second.into());
|
||||
|
||||
(root, temp_dir)
|
||||
}
|
||||
@@ -393,6 +400,29 @@ And here is some \
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_a_single_chapter_with_utf8_bom_from_disk() {
|
||||
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
|
||||
|
||||
let chapter_path = temp_dir.path().join("chapter_1.md");
|
||||
File::create(&chapter_path)
|
||||
.unwrap()
|
||||
.write_all(("\u{feff}".to_owned() + DUMMY_SRC).as_bytes())
|
||||
.unwrap();
|
||||
|
||||
let link = Link::new("Chapter 1", chapter_path);
|
||||
|
||||
let should_be = Chapter::new(
|
||||
"Chapter 1",
|
||||
DUMMY_SRC.to_string(),
|
||||
"chapter_1.md",
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_load_a_nonexistent_chapter() {
|
||||
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
|
||||
@@ -410,6 +440,7 @@ And here is some \
|
||||
content: String::from("Hello World!"),
|
||||
number: Some(SectionNumber(vec![1, 2])),
|
||||
path: Some(PathBuf::from("second.md")),
|
||||
source_path: Some(PathBuf::from("second.md")),
|
||||
parent_names: vec![String::from("Chapter 1")],
|
||||
sub_items: Vec::new(),
|
||||
};
|
||||
@@ -418,11 +449,12 @@ And here is some \
|
||||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: Some(PathBuf::from("chapter_1.md")),
|
||||
source_path: Some(PathBuf::from("chapter_1.md")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(nested.clone()),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(nested.clone()),
|
||||
BookItem::Chapter(nested),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -442,6 +474,7 @@ And here is some \
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
path: Some(PathBuf::from("chapter_1.md")),
|
||||
source_path: Some(PathBuf::from("chapter_1.md")),
|
||||
..Default::default()
|
||||
})],
|
||||
..Default::default()
|
||||
@@ -482,6 +515,7 @@ And here is some \
|
||||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
source_path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
@@ -534,6 +568,7 @@ And here is some \
|
||||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
source_path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
|
||||
@@ -28,7 +28,7 @@ impl BookBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the `Config` to be used.
|
||||
/// Set the [`Config`] to be used.
|
||||
pub fn with_config(&mut self, cfg: Config) -> &mut BookBuilder {
|
||||
self.config = cfg;
|
||||
self
|
||||
@@ -109,12 +109,8 @@ impl BookBuilder {
|
||||
fn copy_across_theme(&self) -> Result<()> {
|
||||
debug!("Copying theme");
|
||||
|
||||
let themedir = self
|
||||
.config
|
||||
.html_config()
|
||||
.and_then(|html| html.theme)
|
||||
.unwrap_or_else(|| self.config.book.src.join("theme"));
|
||||
let themedir = self.root.join(themedir);
|
||||
let html_config = self.config.html_config().unwrap_or_default();
|
||||
let themedir = html_config.theme_dir(&self.root);
|
||||
|
||||
if !themedir.exists() {
|
||||
debug!(
|
||||
@@ -128,7 +124,9 @@ impl BookBuilder {
|
||||
index.write_all(theme::INDEX)?;
|
||||
|
||||
let cssdir = themedir.join("css");
|
||||
fs::create_dir(&cssdir)?;
|
||||
if !cssdir.exists() {
|
||||
fs::create_dir(&cssdir)?;
|
||||
}
|
||||
|
||||
let mut general_css = File::create(cssdir.join("general.css"))?;
|
||||
general_css.write_all(theme::GENERAL_CSS)?;
|
||||
@@ -136,8 +134,10 @@ impl BookBuilder {
|
||||
let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
|
||||
chrome_css.write_all(theme::CHROME_CSS)?;
|
||||
|
||||
let mut print_css = File::create(cssdir.join("print.css"))?;
|
||||
print_css.write_all(theme::PRINT_CSS)?;
|
||||
if html_config.print.enable {
|
||||
let mut print_css = File::create(cssdir.join("print.css"))?;
|
||||
print_css.write_all(theme::PRINT_CSS)?;
|
||||
}
|
||||
|
||||
let mut variables_css = File::create(cssdir.join("variables.css"))?;
|
||||
variables_css.write_all(theme::VARIABLES_CSS)?;
|
||||
|
||||
297
src/book/mod.rs
297
src/book/mod.rs
@@ -20,6 +20,7 @@ use std::process::Command;
|
||||
use std::string::ToString;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use toml::Value;
|
||||
use topological_sort::TopologicalSort;
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::preprocess::{
|
||||
@@ -40,7 +41,7 @@ pub struct MDBook {
|
||||
pub book: Book,
|
||||
renderers: Vec<Box<dyn Renderer>>,
|
||||
|
||||
/// List of pre-processors to be run on the book
|
||||
/// List of pre-processors to be run on the book.
|
||||
preprocessors: Vec<Box<dyn Preprocessor>>,
|
||||
}
|
||||
|
||||
@@ -78,7 +79,7 @@ impl MDBook {
|
||||
MDBook::load_with_config(book_root, config)
|
||||
}
|
||||
|
||||
/// Load a book from its root directory using a custom config.
|
||||
/// Load a book from its root directory using a custom `Config`.
|
||||
pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
|
||||
let root = book_root.into();
|
||||
|
||||
@@ -97,7 +98,7 @@ impl MDBook {
|
||||
})
|
||||
}
|
||||
|
||||
/// Load a book from its root directory using a custom config and a custom summary.
|
||||
/// 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,
|
||||
@@ -121,7 +122,7 @@ impl MDBook {
|
||||
}
|
||||
|
||||
/// Returns a flat depth-first iterator over the elements of the book,
|
||||
/// it returns an [BookItem enum](bookitem.html):
|
||||
/// it returns a [`BookItem`] enum:
|
||||
/// `(section: String, bookitem: &BookItem)`
|
||||
///
|
||||
/// ```no_run
|
||||
@@ -180,7 +181,7 @@ impl MDBook {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run the entire build process for a particular `Renderer`.
|
||||
/// Run the entire build process for a particular [`Renderer`].
|
||||
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
||||
let mut preprocessed_book = self.book.clone();
|
||||
let preprocess_ctx = PreprocessorContext::new(
|
||||
@@ -196,37 +197,34 @@ impl MDBook {
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
let mut render_context = RenderContext::new(
|
||||
self.root.clone(),
|
||||
preprocessed_book.clone(),
|
||||
preprocessed_book,
|
||||
self.config.clone(),
|
||||
build_dir,
|
||||
);
|
||||
render_context
|
||||
.chapter_titles
|
||||
.extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
|
||||
|
||||
info!("Running the {} backend", renderer.name());
|
||||
renderer
|
||||
.render(&render_context)
|
||||
.with_context(|| "Rendering failed")
|
||||
}
|
||||
|
||||
/// You can change the default renderer to another one by using this method.
|
||||
/// The only requirement is for your renderer to implement the [`Renderer`
|
||||
/// trait](../renderer/trait.Renderer.html)
|
||||
/// The only requirement is that your renderer implement the [`Renderer`]
|
||||
/// trait.
|
||||
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
|
||||
self.renderers.push(Box::new(renderer));
|
||||
self
|
||||
}
|
||||
|
||||
/// Register a [`Preprocessor`](../preprocess/trait.Preprocessor.html) to be used when rendering the book.
|
||||
/// Register a [`Preprocessor`] to be used when rendering the book.
|
||||
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
||||
self.preprocessors.push(Box::new(preprocessor));
|
||||
self
|
||||
@@ -250,6 +248,7 @@ impl MDBook {
|
||||
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
||||
// actual markdown files.
|
||||
|
||||
let mut failed = false;
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
let chapter_path = match ch.path {
|
||||
@@ -276,13 +275,18 @@ impl MDBook {
|
||||
RustEdition::E2018 => {
|
||||
cmd.args(&["--edition", "2018"]);
|
||||
}
|
||||
RustEdition::E2021 => {
|
||||
cmd.args(&["--edition", "2021"])
|
||||
.args(&["-Z", "unstable-options"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
failed = true;
|
||||
error!(
|
||||
"rustdoc returned an error:\n\
|
||||
\n--- stdout\n{}\n--- stderr\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
@@ -291,6 +295,9 @@ impl MDBook {
|
||||
}
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
bail!("One or more tests failed");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -298,7 +305,7 @@ impl MDBook {
|
||||
/// artefacts.
|
||||
///
|
||||
/// If there is only 1 renderer, put it in the directory pointed to by the
|
||||
/// `build.build_dir` key in `Config`. If there is more than one then the
|
||||
/// `build.build_dir` key in [`Config`]. If there is more than one then the
|
||||
/// renderer gets its own directory within the main build dir.
|
||||
///
|
||||
/// i.e. If there were only one renderer (in this case, the HTML renderer):
|
||||
@@ -366,12 +373,7 @@ fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
|
||||
renderers
|
||||
}
|
||||
|
||||
fn default_preprocessors() -> Vec<Box<dyn Preprocessor>> {
|
||||
vec![
|
||||
Box::new(LinkPreprocessor::new()),
|
||||
Box::new(IndexPreprocessor::new()),
|
||||
]
|
||||
}
|
||||
const DEFAULT_PREPROCESSORS: &[&'static str] = &["links", "index"];
|
||||
|
||||
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
|
||||
let name = pre.name();
|
||||
@@ -380,36 +382,127 @@ fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
|
||||
|
||||
/// Look at the `MDBook` and try to figure out what preprocessors to run.
|
||||
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
|
||||
let mut preprocessors = Vec::new();
|
||||
// Collect the names of all preprocessors intended to be run, and the order
|
||||
// in which they should be run.
|
||||
let mut preprocessor_names = TopologicalSort::<String>::new();
|
||||
|
||||
if config.build.use_default_preprocessors {
|
||||
preprocessors.extend(default_preprocessors());
|
||||
for name in DEFAULT_PREPROCESSORS {
|
||||
preprocessor_names.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
|
||||
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],
|
||||
)),
|
||||
for (name, table) in preprocessor_table.iter() {
|
||||
preprocessor_names.insert(name.to_string());
|
||||
|
||||
let exists = |name| {
|
||||
(config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
|
||||
|| preprocessor_table.contains_key(name)
|
||||
};
|
||||
|
||||
if let Some(before) = table.get("before") {
|
||||
let before = before.as_array().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.before to be an array",
|
||||
name
|
||||
))
|
||||
})?;
|
||||
for after in before {
|
||||
let after = after.as_str().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.before to contain strings",
|
||||
name
|
||||
))
|
||||
})?;
|
||||
|
||||
if !exists(after) {
|
||||
// Only warn so that preprocessors can be toggled on and off (e.g. for
|
||||
// troubleshooting) without having to worry about order too much.
|
||||
warn!(
|
||||
"preprocessor.{}.after contains \"{}\", which was not found",
|
||||
name, after
|
||||
);
|
||||
} else {
|
||||
preprocessor_names.add_dependency(name, after);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(after) = table.get("after") {
|
||||
let after = after.as_array().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.after to be an array",
|
||||
name
|
||||
))
|
||||
})?;
|
||||
for before in after {
|
||||
let before = before.as_str().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.after to contain strings",
|
||||
name
|
||||
))
|
||||
})?;
|
||||
|
||||
if !exists(before) {
|
||||
// See equivalent warning above for rationale
|
||||
warn!(
|
||||
"preprocessor.{}.before contains \"{}\", which was not found",
|
||||
name, before
|
||||
);
|
||||
} else {
|
||||
preprocessor_names.add_dependency(before, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(preprocessors)
|
||||
// Now that all links have been established, queue preprocessors in a suitable order
|
||||
let mut preprocessors = Vec::with_capacity(preprocessor_names.len());
|
||||
// `pop_all()` returns an empty vector when no more items are not being depended upon
|
||||
for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
|
||||
.take_while(|names| !names.is_empty())
|
||||
{
|
||||
// The `topological_sort` crate does not guarantee a stable order for ties, even across
|
||||
// runs of the same program. Thus, we break ties manually by sorting.
|
||||
// Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
|
||||
// values ([1]), which may not be an alphabetical sort.
|
||||
// As mentioned in [1], doing so depends on locale, which is not desirable for deciding
|
||||
// preprocessor execution order.
|
||||
// [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
|
||||
names.sort();
|
||||
for name in names {
|
||||
let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
|
||||
"links" => Box::new(LinkPreprocessor::new()),
|
||||
"index" => Box::new(IndexPreprocessor::new()),
|
||||
_ => {
|
||||
// The only way to request a custom preprocessor is through the `preprocessor`
|
||||
// table, so it must exist, be a table, and contain the key.
|
||||
let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name];
|
||||
let command = get_custom_preprocessor_cmd(&name, table);
|
||||
Box::new(CmdPreprocessor::new(name, command))
|
||||
}
|
||||
};
|
||||
preprocessors.push(preprocessor);
|
||||
}
|
||||
}
|
||||
|
||||
// "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
|
||||
// Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
|
||||
if preprocessor_names.is_empty() {
|
||||
Ok(preprocessors)
|
||||
} else {
|
||||
Err(Error::msg("Cyclic dependency detected in preprocessors"))
|
||||
}
|
||||
}
|
||||
|
||||
fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocessor> {
|
||||
let command = table
|
||||
fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
|
||||
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))
|
||||
.unwrap_or_else(|| format!("mdbook-{}", key))
|
||||
}
|
||||
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
@@ -509,8 +602,8 @@ mod tests {
|
||||
|
||||
assert!(got.is_ok());
|
||||
assert_eq!(got.as_ref().unwrap().len(), 2);
|
||||
assert_eq!(got.as_ref().unwrap()[0].name(), "links");
|
||||
assert_eq!(got.as_ref().unwrap()[1].name(), "index");
|
||||
assert_eq!(got.as_ref().unwrap()[0].name(), "index");
|
||||
assert_eq!(got.as_ref().unwrap()[1].name(), "links");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -557,9 +650,123 @@ mod tests {
|
||||
|
||||
// make sure the `preprocessor.random` table exists
|
||||
let random = cfg.get_preprocessor("random").unwrap();
|
||||
let random = interpret_custom_preprocessor("random", &Value::Table(random.clone()));
|
||||
let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
|
||||
|
||||
assert_eq!(random.cmd(), "python random.py");
|
||||
assert_eq!(random, "python random.py");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_before_must_be_array() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
before = 0
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
assert!(determine_preprocessors(&cfg).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_after_must_be_array() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
after = 0
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
assert!(determine_preprocessors(&cfg).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_order_is_honored() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
before = [ "last" ]
|
||||
after = [ "index" ]
|
||||
|
||||
[preprocessor.last]
|
||||
after = [ "links", "index" ]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let preprocessors = determine_preprocessors(&cfg).unwrap();
|
||||
let index = |name| {
|
||||
preprocessors
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, preprocessor)| preprocessor.name() == name)
|
||||
.unwrap()
|
||||
.0
|
||||
};
|
||||
let assert_before = |before, after| {
|
||||
if index(before) >= index(after) {
|
||||
eprintln!("Preprocessor order:");
|
||||
for preprocessor in &preprocessors {
|
||||
eprintln!(" {}", preprocessor.name());
|
||||
}
|
||||
panic!("{} should come before {}", before, after);
|
||||
}
|
||||
};
|
||||
|
||||
assert_before("index", "random");
|
||||
assert_before("index", "last");
|
||||
assert_before("random", "last");
|
||||
assert_before("links", "last");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cyclic_dependencies_are_detected() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.links]
|
||||
before = [ "index" ]
|
||||
|
||||
[preprocessor.index]
|
||||
before = [ "links" ]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
assert!(determine_preprocessors(&cfg).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependencies_dont_register_undefined_preprocessors() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.links]
|
||||
before = [ "random" ]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let preprocessors = determine_preprocessors(&cfg).unwrap();
|
||||
|
||||
assert!(preprocessors
|
||||
.iter()
|
||||
.find(|preprocessor| preprocessor.name() == "random")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
before = [ "links" ]
|
||||
|
||||
[build]
|
||||
use-default-preprocessors = false
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let preprocessors = determine_preprocessors(&cfg).unwrap();
|
||||
|
||||
assert!(preprocessors
|
||||
.iter()
|
||||
.find(|preprocessor| preprocessor.name() == "links")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -173,7 +173,7 @@ struct SummaryParser<'a> {
|
||||
/// `Event::End` is encountered which matches the `$delimiter` pattern.
|
||||
///
|
||||
/// This is the equivalent of doing
|
||||
/// `$stream.take_while(|e| e != $delimeter).collect()` but it allows you to
|
||||
/// `$stream.take_while(|e| e != $delimiter).collect()` but it allows you to
|
||||
/// use pattern matching and you won't get errors because `take_while()`
|
||||
/// moves `$stream` out of self.
|
||||
macro_rules! collect_events {
|
||||
@@ -382,7 +382,7 @@ impl<'a> SummaryParser<'a> {
|
||||
}
|
||||
Some(ev @ Event::Start(Tag::List(..))) => {
|
||||
self.back(ev);
|
||||
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?;
|
||||
let mut bunch_of_items = self.parse_nested_numbered(root_number)?;
|
||||
|
||||
// if we've resumed after something like a rule the root sections
|
||||
// will be numbered from 1. We need to manually go back and update
|
||||
@@ -525,14 +525,19 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
/// Try to parse the title line.
|
||||
fn parse_title(&mut self) -> Option<String> {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Heading(1))) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Heading(1))) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(1));
|
||||
Some(stringify_events(tags))
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(1));
|
||||
return Some(stringify_events(tags));
|
||||
}
|
||||
// Skip a HTML element such as a comment line.
|
||||
Some(Event::Html(_)) => {}
|
||||
// Otherwise, no title.
|
||||
_ => return None,
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -973,4 +978,103 @@ mod tests {
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_html_comments() {
|
||||
let src = r#"<!--
|
||||
# Title - En
|
||||
-->
|
||||
# Title - Local
|
||||
|
||||
<!--
|
||||
[Prefix 00-01 - En](ch00-01.md)
|
||||
[Prefix 00-02 - En](ch00-02.md)
|
||||
-->
|
||||
[Prefix 00-01 - Local](ch00-01.md)
|
||||
[Prefix 00-02 - Local](ch00-02.md)
|
||||
|
||||
<!--
|
||||
## Section Title - En
|
||||
-->
|
||||
## Section Title - Localized
|
||||
|
||||
<!--
|
||||
- [Ch 01-00 - En](ch01-00.md)
|
||||
- [Ch 01-01 - En](ch01-01.md)
|
||||
- [Ch 01-02 - En](ch01-02.md)
|
||||
-->
|
||||
- [Ch 01-00 - Local](ch01-00.md)
|
||||
- [Ch 01-01 - Local](ch01-01.md)
|
||||
- [Ch 01-02 - Local](ch01-02.md)
|
||||
|
||||
<!--
|
||||
- [Ch 02-00 - En](ch02-00.md)
|
||||
-->
|
||||
- [Ch 02-00 - Local](ch02-00.md)
|
||||
|
||||
<!--
|
||||
[Appendix A - En](appendix-01.md)
|
||||
[Appendix B - En](appendix-02.md)
|
||||
-->`
|
||||
[Appendix A - Local](appendix-01.md)
|
||||
[Appendix B - Local](appendix-02.md)
|
||||
"#;
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
|
||||
// ---- Title ----
|
||||
let title = parser.parse_title();
|
||||
assert_eq!(title, Some(String::from("Title - Local")));
|
||||
|
||||
// ---- Prefix Chapters ----
|
||||
|
||||
let new_affix_item = |name, location| {
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from(name),
|
||||
location: Some(PathBuf::from(location)),
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
|
||||
let should_be = vec![
|
||||
new_affix_item("Prefix 00-01 - Local", "ch00-01.md"),
|
||||
new_affix_item("Prefix 00-02 - Local", "ch00-02.md"),
|
||||
];
|
||||
|
||||
let got = parser.parse_affix(true).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
// ---- Numbered Chapters ----
|
||||
|
||||
let new_numbered_item = |name, location, numbers: &[u32], nested_items| {
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from(name),
|
||||
location: Some(PathBuf::from(location)),
|
||||
number: Some(SectionNumber(numbers.to_vec())),
|
||||
nested_items,
|
||||
})
|
||||
};
|
||||
|
||||
let ch01_nested = vec![
|
||||
new_numbered_item("Ch 01-01 - Local", "ch01-01.md", &[1, 1], vec![]),
|
||||
new_numbered_item("Ch 01-02 - Local", "ch01-02.md", &[1, 2], vec![]),
|
||||
];
|
||||
|
||||
let should_be = vec![
|
||||
new_numbered_item("Ch 01-00 - Local", "ch01-00.md", &[1], ch01_nested),
|
||||
new_numbered_item("Ch 02-00 - Local", "ch02-00.md", &[2], vec![]),
|
||||
];
|
||||
let got = parser.parse_parts().unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
// ---- Suffix Chapters ----
|
||||
|
||||
let should_be = vec![
|
||||
new_affix_item("Appendix A - Local", "appendix-01.md"),
|
||||
new_affix_item("Appendix B - Local", "appendix-02.md"),
|
||||
];
|
||||
|
||||
let got = parser.parse_affix(false).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::get_book_dir;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use mdbook::config;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
@@ -18,6 +18,21 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
)
|
||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
||||
.arg_from_usage("--force 'Skips confirmation prompts'")
|
||||
.arg(
|
||||
Arg::with_name("title")
|
||||
.long("title")
|
||||
.takes_value(true)
|
||||
.help("Sets the book title")
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("ignore")
|
||||
.long("ignore")
|
||||
.takes_value(true)
|
||||
.possible_values(&["none", "git"])
|
||||
.help("Creates a VCS ignore file (i.e. .gitignore)")
|
||||
.required(false),
|
||||
)
|
||||
}
|
||||
|
||||
// Init command implementation
|
||||
@@ -25,18 +40,13 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut builder = MDBook::init(&book_dir);
|
||||
let mut config = config::Config::default();
|
||||
|
||||
// If flag `--theme` is present, copy theme to src
|
||||
if args.is_present("theme") {
|
||||
config.set("output.html.theme", "src/theme")?;
|
||||
let theme_dir = book_dir.join("theme");
|
||||
println!();
|
||||
println!("Copying the default theme to {}", theme_dir.display());
|
||||
// Skip this if `--force` is present
|
||||
if !args.is_present("force") {
|
||||
// Print warning
|
||||
println!();
|
||||
println!(
|
||||
"Copying the default theme to {}",
|
||||
builder.config().book.src.display()
|
||||
);
|
||||
if !args.is_present("force") && theme_dir.exists() {
|
||||
println!("This could potentially overwrite files already present in that directory.");
|
||||
print!("\nAre you sure you want to continue? (y/n) ");
|
||||
|
||||
@@ -49,13 +59,23 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||
|
||||
if confirm() {
|
||||
builder.create_gitignore(true);
|
||||
if let Some(ignore) = args.value_of("ignore") {
|
||||
match ignore {
|
||||
"git" => builder.create_gitignore(true),
|
||||
_ => builder.create_gitignore(false),
|
||||
};
|
||||
} else {
|
||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||
if confirm() {
|
||||
builder.create_gitignore(true);
|
||||
}
|
||||
}
|
||||
|
||||
config.book.title = request_book_title();
|
||||
config.book.title = if args.is_present("title") {
|
||||
args.value_of("title").map(String::from)
|
||||
} else {
|
||||
request_book_title()
|
||||
};
|
||||
|
||||
if let Some(author) = get_author_name() {
|
||||
debug!("Obtained user name from gitconfig: {:?}", author);
|
||||
|
||||
@@ -161,5 +161,12 @@ async fn serve(
|
||||
let fallback_route = warp::fs::file(build_dir.join(file_404))
|
||||
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
|
||||
let routes = livereload.or(book_route).or(fallback_route);
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// exit if serve panics
|
||||
error!("Unable to serve: {}", panic_info);
|
||||
std::process::exit(1);
|
||||
}));
|
||||
|
||||
warp::serve(routes).run(address).await;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_ignored_files(book_root: &PathBuf, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
if paths.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
@@ -81,7 +81,7 @@ fn remove_ignored_files(book_root: &PathBuf, paths: &[PathBuf]) -> Vec<PathBuf>
|
||||
}
|
||||
}
|
||||
|
||||
fn find_gitignore(book_root: &PathBuf) -> Option<PathBuf> {
|
||||
fn find_gitignore(book_root: &Path) -> Option<PathBuf> {
|
||||
book_root
|
||||
.ancestors()
|
||||
.map(|p| p.join(".gitignore"))
|
||||
|
||||
129
src/config.rs
129
src/config.rs
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! 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
|
||||
//! pre-determined tables ([`BookConfig`] and [`BuildConfig`]) as well as support
|
||||
//! for arbitrary data which is exposed to plugins and alternative backends.
|
||||
//!
|
||||
//!
|
||||
@@ -294,6 +294,7 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Config {
|
||||
fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
|
||||
let raw = Value::deserialize(de)?;
|
||||
@@ -310,10 +311,10 @@ impl<'de> Deserialize<'de> for Config {
|
||||
return Ok(Config::from_legacy(raw));
|
||||
}
|
||||
|
||||
use serde::de::Error;
|
||||
let mut table = match raw {
|
||||
Value::Table(t) => t,
|
||||
_ => {
|
||||
use serde::de::Error;
|
||||
return Err(D::Error::custom(
|
||||
"A config file should always be a toml table",
|
||||
));
|
||||
@@ -322,17 +323,20 @@ impl<'de> Deserialize<'de> for Config {
|
||||
|
||||
let book: BookConfig = table
|
||||
.remove("book")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.map(|book| book.try_into().map_err(D::Error::custom))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let build: BuildConfig = table
|
||||
.remove("build")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.map(|build| build.try_into().map_err(D::Error::custom))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let rust: RustConfig = table
|
||||
.remove("rust")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.map(|rust| rust.try_into().map_err(D::Error::custom))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Config {
|
||||
@@ -352,6 +356,11 @@ impl Serialize for Config {
|
||||
let book_config = Value::try_from(&self.book).expect("should always be serializable");
|
||||
table.insert("book", book_config);
|
||||
|
||||
if self.build != BuildConfig::default() {
|
||||
let build_config = Value::try_from(&self.build).expect("should always be serializable");
|
||||
table.insert("build", build_config);
|
||||
}
|
||||
|
||||
if self.rust != RustConfig::default() {
|
||||
let rust_config = Value::try_from(&self.rust).expect("should always be serializable");
|
||||
table.insert("rust", rust_config);
|
||||
@@ -458,6 +467,9 @@ pub struct RustConfig {
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// Rust edition to use for the code.
|
||||
pub enum RustEdition {
|
||||
/// The 2021 edition of Rust
|
||||
#[serde(rename = "2021")]
|
||||
E2021,
|
||||
/// The 2018 edition of Rust
|
||||
#[serde(rename = "2018")]
|
||||
E2018,
|
||||
@@ -495,6 +507,8 @@ pub struct HtmlConfig {
|
||||
/// Playground settings.
|
||||
#[serde(alias = "playpen")]
|
||||
pub playground: Playground,
|
||||
/// Print settings.
|
||||
pub print: Print,
|
||||
/// Don't render section labels.
|
||||
pub no_section_label: bool,
|
||||
/// Search settings. If `None`, the default will be used.
|
||||
@@ -508,6 +522,17 @@ pub struct HtmlConfig {
|
||||
pub input_404: Option<String>,
|
||||
/// Absolute url to site, used to emit correct paths for the 404 page, which might be accessed in a deeply nested directory
|
||||
pub site_url: Option<String>,
|
||||
/// The DNS subdomain or apex domain at which your book will be hosted. This
|
||||
/// string will be written to a file named CNAME in the root of your site,
|
||||
/// as required by GitHub Pages (see [*Managing a custom domain for your
|
||||
/// GitHub Pages site*][custom domain]).
|
||||
///
|
||||
/// [custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
||||
pub cname: Option<String>,
|
||||
/// Edit url template, when set shows a "Suggest an edit" button for
|
||||
/// directly jumping to editing the currently viewed page.
|
||||
/// Contains {path} that is replaced with chapter source file path
|
||||
pub edit_url_template: Option<String>,
|
||||
/// This is used as a bit of a workaround for the `mdbook serve` command.
|
||||
/// Basically, because you set the websocket port from the command line, the
|
||||
/// `mdbook serve` command needs a way to let the HTML renderer know where
|
||||
@@ -535,12 +560,15 @@ impl Default for HtmlConfig {
|
||||
additional_js: Vec::new(),
|
||||
fold: Fold::default(),
|
||||
playground: Playground::default(),
|
||||
print: Print::default(),
|
||||
no_section_label: false,
|
||||
search: None,
|
||||
git_repository_url: None,
|
||||
git_repository_icon: None,
|
||||
edit_url_template: None,
|
||||
input_404: None,
|
||||
site_url: None,
|
||||
cname: None,
|
||||
livereload_url: None,
|
||||
redirect: HashMap::new(),
|
||||
}
|
||||
@@ -550,7 +578,7 @@ impl Default for HtmlConfig {
|
||||
impl HtmlConfig {
|
||||
/// Returns the directory of theme from the provided root directory. If the
|
||||
/// directory is not present it will append the default directory of "theme"
|
||||
pub fn theme_dir(&self, root: &PathBuf) -> PathBuf {
|
||||
pub fn theme_dir(&self, root: &Path) -> PathBuf {
|
||||
match self.theme {
|
||||
Some(ref d) => root.join(d),
|
||||
None => root.join("theme"),
|
||||
@@ -558,6 +586,20 @@ impl HtmlConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for how to render the print icon, print.html, and print.css.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Print {
|
||||
/// Whether print support is enabled.
|
||||
pub enable: bool,
|
||||
}
|
||||
|
||||
impl Default for Print {
|
||||
fn default() -> Self {
|
||||
Self { enable: true }
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for how to fold chapters of sidebar.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
@@ -621,7 +663,7 @@ pub struct Search {
|
||||
pub boost_paragraph: u8,
|
||||
/// True if the searchword `micro` should match `microwave`. Default: `true`.
|
||||
pub expand: bool,
|
||||
/// Documents are split into smaller parts, seperated by headings. This defines, until which
|
||||
/// Documents are split into smaller parts, separated by headings. This defines, until which
|
||||
/// level of heading documents should be split. Default: `3`. (`### This is a level 3 heading`)
|
||||
pub heading_split_level: u8,
|
||||
/// Copy JavaScript files for the search functionality to the output directory?
|
||||
@@ -816,6 +858,26 @@ mod tests {
|
||||
assert_eq!(got.rust, rust_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edition_2021() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
src = "./source"
|
||||
[rust]
|
||||
edition = "2021"
|
||||
"#;
|
||||
|
||||
let rust_should_be = RustConfig {
|
||||
edition: Some(RustEdition::E2021),
|
||||
};
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.rust, rust_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_arbitrary_output_type() {
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
@@ -1035,4 +1097,57 @@ mod tests {
|
||||
assert_eq!(html_config.input_404, Some("missing.md".to_string()));
|
||||
assert_eq!(&get_404_output_file(&html_config.input_404), "missing.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Invalid configuration file")]
|
||||
fn invalid_language_type_error() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
language = ["en", "pt-br"]
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
src = "./source"
|
||||
"#;
|
||||
|
||||
Config::from_str(src).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Invalid configuration file")]
|
||||
fn invalid_title_type() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = 20
|
||||
language = "en"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
src = "./source"
|
||||
"#;
|
||||
|
||||
Config::from_str(src).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Invalid configuration file")]
|
||||
fn invalid_build_dir_type() {
|
||||
let src = r#"
|
||||
[build]
|
||||
build-dir = 99
|
||||
create-missing = false
|
||||
"#;
|
||||
|
||||
Config::from_str(src).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Invalid configuration file")]
|
||||
fn invalid_rust_edition() {
|
||||
let src = r#"
|
||||
[rust]
|
||||
edition = "1999"
|
||||
"#;
|
||||
|
||||
Config::from_str(src).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +76,9 @@
|
||||
//! access to the various methods for working with the [`Config`].
|
||||
//!
|
||||
//! [user guide]: https://rust-lang.github.io/mdBook/
|
||||
//! [`RenderContext`]: renderer/struct.RenderContext.html
|
||||
//! [`RenderContext`]: renderer::RenderContext
|
||||
//! [relevant chapter]: https://rust-lang.github.io/mdBook/for_developers/backends.html
|
||||
//! [`Config`]: config/struct.Config.html
|
||||
//! [`Config`]: config::Config
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(rust_2018_idioms)]
|
||||
|
||||
74
src/main.rs
74
src/main.rs
@@ -3,8 +3,9 @@ extern crate clap;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::Local;
|
||||
use clap::{App, AppSettings, ArgMatches};
|
||||
use clap::{App, AppSettings, Arg, ArgMatches, Shell, SubCommand};
|
||||
use env_logger::Builder;
|
||||
use log::LevelFilter;
|
||||
use mdbook::utils;
|
||||
@@ -20,7 +21,40 @@ const VERSION: &str = concat!("v", crate_version!());
|
||||
fn main() {
|
||||
init_logger();
|
||||
|
||||
// Create a list of valid arguments and sub-commands
|
||||
let app = create_clap_app();
|
||||
|
||||
// Check which subcomamnd the user ran...
|
||||
let res = match app.get_matches().subcommand() {
|
||||
("init", Some(sub_matches)) => cmd::init::execute(sub_matches),
|
||||
("build", Some(sub_matches)) => cmd::build::execute(sub_matches),
|
||||
("clean", Some(sub_matches)) => cmd::clean::execute(sub_matches),
|
||||
#[cfg(feature = "watch")]
|
||||
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches),
|
||||
#[cfg(feature = "serve")]
|
||||
("serve", Some(sub_matches)) => cmd::serve::execute(sub_matches),
|
||||
("test", Some(sub_matches)) => cmd::test::execute(sub_matches),
|
||||
("completions", Some(sub_matches)) => (|| {
|
||||
let shell: Shell = sub_matches
|
||||
.value_of("shell")
|
||||
.ok_or_else(|| anyhow!("Shell name missing."))?
|
||||
.parse()
|
||||
.map_err(|s| anyhow!("Invalid shell: {}", s))?;
|
||||
|
||||
create_clap_app().gen_completions_to("mdbook", shell, &mut std::io::stdout().lock());
|
||||
Ok(())
|
||||
})(),
|
||||
(_, _) => unreachable!(),
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
utils::log_backtrace(&e);
|
||||
|
||||
std::process::exit(101);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a list of valid arguments and sub-commands
|
||||
fn create_clap_app<'a, 'b>() -> App<'a, 'b> {
|
||||
let app = App::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
@@ -35,31 +69,26 @@ fn main() {
|
||||
.subcommand(cmd::init::make_subcommand())
|
||||
.subcommand(cmd::build::make_subcommand())
|
||||
.subcommand(cmd::test::make_subcommand())
|
||||
.subcommand(cmd::clean::make_subcommand());
|
||||
.subcommand(cmd::clean::make_subcommand())
|
||||
.subcommand(
|
||||
SubCommand::with_name("completions")
|
||||
.about("Generate shell completions for your shell to stdout")
|
||||
.arg(
|
||||
Arg::with_name("shell")
|
||||
.takes_value(true)
|
||||
.possible_values(&Shell::variants())
|
||||
.help("the shell to generate completions for")
|
||||
.value_name("SHELL")
|
||||
.required(true),
|
||||
),
|
||||
);
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
let app = app.subcommand(cmd::watch::make_subcommand());
|
||||
#[cfg(feature = "serve")]
|
||||
let app = app.subcommand(cmd::serve::make_subcommand());
|
||||
|
||||
// Check which subcomamnd the user ran...
|
||||
let res = match app.get_matches().subcommand() {
|
||||
("init", Some(sub_matches)) => cmd::init::execute(sub_matches),
|
||||
("build", Some(sub_matches)) => cmd::build::execute(sub_matches),
|
||||
("clean", Some(sub_matches)) => cmd::clean::execute(sub_matches),
|
||||
#[cfg(feature = "watch")]
|
||||
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches),
|
||||
#[cfg(feature = "serve")]
|
||||
("serve", Some(sub_matches)) => cmd::serve::execute(sub_matches),
|
||||
("test", Some(sub_matches)) => cmd::test::execute(sub_matches),
|
||||
(_, _) => unreachable!(),
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
utils::log_backtrace(&e);
|
||||
|
||||
std::process::exit(101);
|
||||
}
|
||||
app
|
||||
}
|
||||
|
||||
fn init_logger() {
|
||||
@@ -103,7 +132,8 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
||||
}
|
||||
|
||||
fn open<P: AsRef<OsStr>>(path: P) {
|
||||
if let Err(e) = open::that(path) {
|
||||
info!("Opening web browser");
|
||||
if let Err(e) = opener::open(path) {
|
||||
error!("Error opening web browser: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ impl CmdPreprocessor {
|
||||
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) {
|
||||
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);
|
||||
@@ -109,18 +109,28 @@ impl Preprocessor for CmdPreprocessor {
|
||||
|
||||
self.write_input_to_child(&mut child, &book, ctx);
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.with_context(|| "Error waiting for the preprocessor to complete")?;
|
||||
let output = child.wait_with_output().with_context(|| {
|
||||
format!(
|
||||
"Error waiting for the \"{}\" preprocessor to complete",
|
||||
self.name
|
||||
)
|
||||
})?;
|
||||
|
||||
trace!("{} exited with output: {:?}", self.cmd, output);
|
||||
ensure!(
|
||||
output.status.success(),
|
||||
"The preprocessor exited unsuccessfully"
|
||||
format!(
|
||||
"The \"{}\" preprocessor exited unsuccessfully with {} status",
|
||||
self.name, output.status
|
||||
)
|
||||
);
|
||||
|
||||
serde_json::from_slice(&output.stdout)
|
||||
.with_context(|| "Unable to parse the preprocessed book")
|
||||
serde_json::from_slice(&output.stdout).with_context(|| {
|
||||
format!(
|
||||
"Unable to parse the preprocessed book from \"{}\" processor",
|
||||
self.name
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
@@ -171,15 +181,15 @@ mod tests {
|
||||
use crate::MDBook;
|
||||
use std::path::Path;
|
||||
|
||||
fn book_example() -> MDBook {
|
||||
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
|
||||
fn guide() -> MDBook {
|
||||
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
|
||||
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 md = guide();
|
||||
let ctx = PreprocessorContext::new(
|
||||
md.root.clone(),
|
||||
md.config.clone(),
|
||||
|
||||
@@ -23,6 +23,7 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
/// This hides the lines from initial display but shows them when the reader expands the code
|
||||
/// block and provides them to Rustdoc for testing.
|
||||
/// - `{{# playground}}` - Insert runnable Rust files
|
||||
/// - `{{# title}}` - Override \<title\> of a webpage.
|
||||
#[derive(Default)]
|
||||
pub struct LinkPreprocessor;
|
||||
|
||||
@@ -51,8 +52,15 @@ impl Preprocessor for LinkPreprocessor {
|
||||
.map(|dir| src_dir.join(dir))
|
||||
.expect("All book items have a parent");
|
||||
|
||||
let content = replace_all(&ch.content, base, chapter_path, 0);
|
||||
let mut chapter_title = ch.name.clone();
|
||||
let content =
|
||||
replace_all(&ch.content, base, chapter_path, 0, &mut chapter_title);
|
||||
ch.content = content;
|
||||
if chapter_title != ch.name {
|
||||
ctx.chapter_titles
|
||||
.borrow_mut()
|
||||
.insert(chapter_path.clone(), chapter_title);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -61,7 +69,13 @@ impl Preprocessor for LinkPreprocessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_all<P1, P2>(s: &str, path: P1, source: P2, depth: usize) -> String
|
||||
fn replace_all<P1, P2>(
|
||||
s: &str,
|
||||
path: P1,
|
||||
source: P2,
|
||||
depth: usize,
|
||||
chapter_title: &mut String,
|
||||
) -> String
|
||||
where
|
||||
P1: AsRef<Path>,
|
||||
P2: AsRef<Path>,
|
||||
@@ -77,11 +91,17 @@ where
|
||||
for link in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..link.start_index]);
|
||||
|
||||
match link.render_with_path(&path) {
|
||||
match link.render_with_path(&path, chapter_title) {
|
||||
Ok(new_content) => {
|
||||
if depth < MAX_LINK_NESTED_DEPTH {
|
||||
if let Some(rel_path) = link.link_type.relative_path(path) {
|
||||
replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1));
|
||||
replaced.push_str(&replace_all(
|
||||
&new_content,
|
||||
rel_path,
|
||||
source,
|
||||
depth + 1,
|
||||
chapter_title,
|
||||
));
|
||||
} else {
|
||||
replaced.push_str(&new_content);
|
||||
}
|
||||
@@ -116,6 +136,7 @@ enum LinkType<'a> {
|
||||
Include(PathBuf, RangeOrAnchor),
|
||||
Playground(PathBuf, Vec<&'a str>),
|
||||
RustdocInclude(PathBuf, RangeOrAnchor),
|
||||
Title(&'a str),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
@@ -185,6 +206,7 @@ impl<'a> LinkType<'a> {
|
||||
LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Playground(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Title(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,6 +277,9 @@ struct Link<'a> {
|
||||
impl<'a> Link<'a> {
|
||||
fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
|
||||
let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
|
||||
(_, Some(typ), Some(title)) if typ.as_str() == "title" => {
|
||||
Some(LinkType::Title(title.as_str()))
|
||||
}
|
||||
(_, Some(typ), Some(rest)) => {
|
||||
let mut path_props = rest.as_str().split_whitespace();
|
||||
let file_arg = path_props.next();
|
||||
@@ -291,7 +316,11 @@ impl<'a> Link<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
|
||||
fn render_with_path<P: AsRef<Path>>(
|
||||
&self,
|
||||
base: P,
|
||||
chapter_title: &mut String,
|
||||
) -> Result<String> {
|
||||
let base = base.as_ref();
|
||||
match self.link_type {
|
||||
// omit the escape char
|
||||
@@ -335,7 +364,7 @@ impl<'a> Link<'a> {
|
||||
LinkType::Playground(ref pat, ref attrs) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
let contents = fs::read_to_string(&target).with_context(|| {
|
||||
let mut contents = fs::read_to_string(&target).with_context(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
@@ -343,13 +372,20 @@ impl<'a> Link<'a> {
|
||||
)
|
||||
})?;
|
||||
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
||||
if !contents.ends_with('\n') {
|
||||
contents.push('\n');
|
||||
}
|
||||
Ok(format!(
|
||||
"```{}{}\n{}\n```\n",
|
||||
"```{}{}\n{}```\n",
|
||||
ftype,
|
||||
attrs.join(","),
|
||||
contents
|
||||
))
|
||||
}
|
||||
LinkType::Title(title) => {
|
||||
*chapter_title = title.to_owned();
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,17 +406,17 @@ impl<'a> Iterator for LinkIter<'a> {
|
||||
|
||||
fn find_links(contents: &str) -> LinkIter<'_> {
|
||||
// lazily compute following regex
|
||||
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
|
||||
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(
|
||||
r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9_]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([a-zA-Z0-9\s_.\-:/\\\+]+) # link target path and space separated properties
|
||||
\s*\}\} # whitespace and link closing parens"
|
||||
r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9_]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([^}]+) # link target path and space separated properties
|
||||
\}\} # link closing parens"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -403,7 +439,21 @@ mod tests {
|
||||
```hbs
|
||||
{{#include file.rs}} << an escaped link!
|
||||
```";
|
||||
assert_eq!(replace_all(start, "", "", 0), end);
|
||||
let mut chapter_title = "test_replace_all_escaped".to_owned();
|
||||
assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_chapter_title() {
|
||||
let start = r"{{#title My Title}}
|
||||
# My Chapter
|
||||
";
|
||||
let end = r"
|
||||
# My Chapter
|
||||
";
|
||||
let mut chapter_title = "test_set_chapter_title".to_owned();
|
||||
assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
|
||||
assert_eq!(chapter_title, "My Title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -12,6 +12,8 @@ use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Extra information for a `Preprocessor` to give them more context when
|
||||
@@ -27,6 +29,8 @@ pub struct PreprocessorContext {
|
||||
/// The calling `mdbook` version.
|
||||
pub mdbook_version: String,
|
||||
#[serde(skip)]
|
||||
pub(crate) chapter_titles: RefCell<HashMap<PathBuf, String>>,
|
||||
#[serde(skip)]
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
@@ -38,6 +42,7 @@ impl PreprocessorContext {
|
||||
config,
|
||||
renderer,
|
||||
mdbook_version: crate::MDBOOK_VERSION.to_string(),
|
||||
chapter_titles: RefCell::new(HashMap::new()),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::config::{Config, HtmlConfig, Playground, RustEdition};
|
||||
use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
@@ -37,14 +37,32 @@ impl HtmlHandlebars {
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
if let Some(ref edit_url_template) = ctx.html_config.edit_url_template {
|
||||
let full_path = ctx.book_config.src.to_str().unwrap_or_default().to_owned()
|
||||
+ "/"
|
||||
+ ch.source_path
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
let edit_url = edit_url_template.replace("{path}", &full_path);
|
||||
ctx.data
|
||||
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
|
||||
}
|
||||
|
||||
let content = ch.content.clone();
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
|
||||
let fixed_content = utils::render_markdown_with_path(
|
||||
&ch.content,
|
||||
ctx.html_config.curly_quotes,
|
||||
Some(&path),
|
||||
);
|
||||
let fixed_content =
|
||||
utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path));
|
||||
if !ctx.is_index {
|
||||
// Add page break between chapters
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
|
||||
// Add both two CSS properties because of the compatibility issue
|
||||
print_content
|
||||
.push_str(r#"<div style="break-before: page; page-break-before: always;"></div>"#);
|
||||
}
|
||||
print_content.push_str(&fixed_content);
|
||||
|
||||
// Update the context with data for this file
|
||||
@@ -64,9 +82,12 @@ impl HtmlHandlebars {
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
|
||||
let title = match book_title {
|
||||
"" => ch.name.clone(),
|
||||
_ => ch.name.clone() + " - " + book_title,
|
||||
let title = if let Some(title) = ctx.chapter_titles.get(path) {
|
||||
title.clone()
|
||||
} else if book_title.is_empty() {
|
||||
ch.name.clone()
|
||||
} else {
|
||||
ch.name.clone() + " - " + book_title
|
||||
};
|
||||
|
||||
ctx.data.insert("path".to_owned(), json!(path));
|
||||
@@ -110,7 +131,7 @@ impl HtmlHandlebars {
|
||||
&self,
|
||||
ctx: &RenderContext,
|
||||
html_config: &HtmlConfig,
|
||||
src_dir: &PathBuf,
|
||||
src_dir: &Path,
|
||||
handlebars: &mut Handlebars<'_>,
|
||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||
) -> Result<()> {
|
||||
@@ -154,7 +175,7 @@ impl HtmlHandlebars {
|
||||
let rendered =
|
||||
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
|
||||
let output_file = get_404_output_file(&html_config.input_404);
|
||||
utils::fs::write_file(&destination, output_file, rendered.as_bytes())?;
|
||||
utils::fs::write_file(destination, output_file, rendered.as_bytes())?;
|
||||
debug!("Creating 404.html ✓");
|
||||
Ok(())
|
||||
}
|
||||
@@ -184,19 +205,25 @@ impl HtmlHandlebars {
|
||||
write_file(
|
||||
destination,
|
||||
".nojekyll",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
write_file(destination, "CNAME", format!("{}\n", cname).as_bytes())?;
|
||||
}
|
||||
|
||||
write_file(destination, "book.js", &theme.js)?;
|
||||
write_file(destination, "css/general.css", &theme.general_css)?;
|
||||
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
|
||||
write_file(destination, "css/print.css", &theme.print_css)?;
|
||||
if html_config.print.enable {
|
||||
write_file(destination, "css/print.css", &theme.print_css)?;
|
||||
}
|
||||
write_file(destination, "css/variables.css", &theme.variables_css)?;
|
||||
if let Some(contents) = &theme.favicon_png {
|
||||
write_file(destination, "favicon.png", &contents)?;
|
||||
write_file(destination, "favicon.png", contents)?;
|
||||
}
|
||||
if let Some(contents) = &theme.favicon_svg {
|
||||
write_file(destination, "favicon.svg", &contents)?;
|
||||
write_file(destination, "favicon.svg", contents)?;
|
||||
}
|
||||
write_file(destination, "highlight.css", &theme.highlight_css)?;
|
||||
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||
@@ -360,7 +387,7 @@ impl HtmlHandlebars {
|
||||
// Note: all paths are relative to the build directory, so the
|
||||
// leading slash in an absolute path means nothing (and would mess
|
||||
// up `root.join(original)`).
|
||||
let original = original.trim_start_matches("/");
|
||||
let original = original.trim_start_matches('/');
|
||||
let filename = root.join(original);
|
||||
self.emit_redirect(handlebars, &filename, new)?;
|
||||
}
|
||||
@@ -431,6 +458,7 @@ impl Renderer for HtmlHandlebars {
|
||||
}
|
||||
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||
let book_config = &ctx.config.book;
|
||||
let html_config = ctx.config.html_config().unwrap_or_default();
|
||||
let src_dir = ctx.root.join(&ctx.config.book.src);
|
||||
let destination = &ctx.destination;
|
||||
@@ -446,7 +474,7 @@ impl Renderer for HtmlHandlebars {
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
let theme_dir = match html_config.theme {
|
||||
Some(ref theme) => theme.to_path_buf(),
|
||||
Some(ref theme) => ctx.root.join(theme),
|
||||
None => ctx.root.join("theme"),
|
||||
};
|
||||
|
||||
@@ -478,7 +506,7 @@ impl Renderer for HtmlHandlebars {
|
||||
debug!("Register handlebars helpers");
|
||||
self.register_hbs_helpers(&mut handlebars, &html_config);
|
||||
|
||||
let mut data = make_data(&ctx.root, &book, &ctx.config, &html_config, &theme)?;
|
||||
let mut data = make_data(&ctx.root, book, &ctx.config, &html_config, &theme)?;
|
||||
|
||||
// Print version
|
||||
let mut print_content = String::new();
|
||||
@@ -493,8 +521,10 @@ impl Renderer for HtmlHandlebars {
|
||||
destination: destination.to_path_buf(),
|
||||
data: data.clone(),
|
||||
is_index,
|
||||
book_config: book_config.clone(),
|
||||
html_config: html_config.clone(),
|
||||
edition: ctx.config.rust.edition,
|
||||
chapter_titles: &ctx.chapter_titles,
|
||||
};
|
||||
self.render_item(item, ctx, &mut print_content)?;
|
||||
is_index = false;
|
||||
@@ -512,19 +542,21 @@ impl Renderer for HtmlHandlebars {
|
||||
}
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
if html_config.print.enable {
|
||||
debug!("Render template");
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
|
||||
let rendered =
|
||||
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
|
||||
let rendered =
|
||||
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
|
||||
|
||||
utils::fs::write_file(&destination, "print.html", rendered.as_bytes())?;
|
||||
debug!("Creating print.html ✓");
|
||||
utils::fs::write_file(destination, "print.html", rendered.as_bytes())?;
|
||||
debug!("Creating print.html ✓");
|
||||
}
|
||||
|
||||
debug!("Copy static files");
|
||||
self.copy_static_files(&destination, &theme, &html_config)
|
||||
self.copy_static_files(destination, &theme, &html_config)
|
||||
.with_context(|| "Unable to copy across static files")?;
|
||||
self.copy_additional_css_and_js(&html_config, &ctx.root, &destination)
|
||||
self.copy_additional_css_and_js(&html_config, &ctx.root, destination)
|
||||
.with_context(|| "Unable to copy across additional CSS and JS")?;
|
||||
|
||||
// Render search index
|
||||
@@ -532,7 +564,7 @@ impl Renderer for HtmlHandlebars {
|
||||
{
|
||||
let search = html_config.search.unwrap_or_default();
|
||||
if search.enable {
|
||||
super::search::create_files(&search, &destination, &book)?;
|
||||
super::search::create_files(&search, destination, book)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,7 +572,7 @@ impl Renderer for HtmlHandlebars {
|
||||
.context("Unable to emit redirects")?;
|
||||
|
||||
// Copy all remaining files, avoid a recursive copy from/to the book build dir
|
||||
utils::fs::copy_files_except_ext(&src_dir, &destination, true, Some(&build_dir), &["md"])?;
|
||||
utils::fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -640,8 +672,9 @@ fn make_data(
|
||||
data.insert("playground_copyable".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
data.insert("fold_enable".to_owned(), json!((html_config.fold.enable)));
|
||||
data.insert("fold_level".to_owned(), json!((html_config.fold.level)));
|
||||
data.insert("print_enable".to_owned(), json!(html_config.print.enable));
|
||||
data.insert("fold_enable".to_owned(), json!(html_config.fold.enable));
|
||||
data.insert("fold_level".to_owned(), json!(html_config.fold.level));
|
||||
|
||||
let search = html_config.search.clone();
|
||||
if cfg!(feature = "search") {
|
||||
@@ -747,7 +780,7 @@ fn insert_link_into_header(
|
||||
*id_count += 1;
|
||||
|
||||
format!(
|
||||
r##"<h{level}><a class="header" href="#{id}" id="{id}">{text}</a></h{level}>"##,
|
||||
r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content
|
||||
@@ -800,13 +833,15 @@ fn add_playground_pre(
|
||||
{
|
||||
let contains_e2015 = classes.contains("edition2015");
|
||||
let contains_e2018 = classes.contains("edition2018");
|
||||
let edition_class = if contains_e2015 || contains_e2018 {
|
||||
let contains_e2021 = classes.contains("edition2021");
|
||||
let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 {
|
||||
// the user forced edition, we should not overwrite it
|
||||
""
|
||||
} else {
|
||||
match edition {
|
||||
Some(RustEdition::E2015) => " edition2015",
|
||||
Some(RustEdition::E2018) => " edition2018",
|
||||
Some(RustEdition::E2021) => " edition2021",
|
||||
None => "",
|
||||
}
|
||||
};
|
||||
@@ -890,10 +925,10 @@ fn partition_source(s: &str) -> (String, String) {
|
||||
if !header || after_header {
|
||||
after_header = true;
|
||||
after.push_str(line);
|
||||
after.push_str("\n");
|
||||
after.push('\n');
|
||||
} else {
|
||||
before.push_str(line);
|
||||
before.push_str("\n");
|
||||
before.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,8 +940,10 @@ struct RenderItemContext<'a> {
|
||||
destination: PathBuf,
|
||||
data: serde_json::Map<String, serde_json::Value>,
|
||||
is_index: bool,
|
||||
book_config: BookConfig,
|
||||
html_config: HtmlConfig,
|
||||
edition: Option<RustEdition>,
|
||||
chapter_titles: &'a HashMap<PathBuf, String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -918,32 +955,32 @@ mod tests {
|
||||
let inputs = vec![
|
||||
(
|
||||
"blah blah <h1>Foo</h1>",
|
||||
r##"blah blah <h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
|
||||
r##"blah blah <h1 id="foo"><a class="header" href="#foo">Foo</a></h1>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1>",
|
||||
r##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
|
||||
r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1>"##,
|
||||
),
|
||||
(
|
||||
"<h3>Foo^bar</h3>",
|
||||
r##"<h3><a class="header" href="#foobar" id="foobar">Foo^bar</a></h3>"##,
|
||||
r##"<h3 id="foobar"><a class="header" href="#foobar">Foo^bar</a></h3>"##,
|
||||
),
|
||||
(
|
||||
"<h4></h4>",
|
||||
r##"<h4><a class="header" href="#" id=""></a></h4>"##,
|
||||
r##"<h4 id=""><a class="header" href="#"></a></h4>"##,
|
||||
),
|
||||
(
|
||||
"<h4><em>Hï</em></h4>",
|
||||
r##"<h4><a class="header" href="#hï" id="hï"><em>Hï</em></a></h4>"##,
|
||||
r##"<h4 id="hï"><a class="header" href="#hï"><em>Hï</em></a></h4>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1><h3>Foo</h3>",
|
||||
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>"##,
|
||||
r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1><h3 id="foo-1"><a class="header" href="#foo-1">Foo</a></h3>"##,
|
||||
),
|
||||
];
|
||||
|
||||
for (src, should_be) in inputs {
|
||||
let got = build_header_links(&src);
|
||||
let got = build_header_links(src);
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
@@ -1026,4 +1063,28 @@ mod tests {
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn add_playground_edition2021() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
src,
|
||||
&Playground {
|
||||
editable: true,
|
||||
..Playground::default()
|
||||
},
|
||||
Some(RustEdition::E2021),
|
||||
);
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ fn find_chapter(
|
||||
match item.get("path") {
|
||||
Some(path) if !path.is_empty() => {
|
||||
if let Some(previous) = previous {
|
||||
if let Some(item) = target.find(&base_path, &path, &item, &previous)? {
|
||||
if let Some(item) = target.find(&base_path, path, &item, &previous)? {
|
||||
return Ok(Some(item));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::utils;
|
||||
@@ -102,38 +103,38 @@ impl HelperDef for RenderToc {
|
||||
// Part title
|
||||
if let Some(title) = item.get("part") {
|
||||
out.write("<li class=\"part-title\">")?;
|
||||
out.write(title)?;
|
||||
write_escaped(out, title)?;
|
||||
out.write("</li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Link
|
||||
let path_exists = if let Some(path) = item.get("path") {
|
||||
if !path.is_empty() {
|
||||
out.write("<a href=\"")?;
|
||||
let path_exists = if let Some(path) =
|
||||
item.get("path")
|
||||
.and_then(|p| if p.is_empty() { None } else { Some(p) })
|
||||
{
|
||||
out.write("<a href=\"")?;
|
||||
|
||||
let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)"))
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
.replace("\\", "/");
|
||||
let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)"))
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
.replace("\\", "/");
|
||||
|
||||
// Add link
|
||||
out.write(&utils::fs::path_to_root(¤t_path))?;
|
||||
out.write(&tmp)?;
|
||||
out.write("\"")?;
|
||||
// Add link
|
||||
out.write(&utils::fs::path_to_root(¤t_path))?;
|
||||
out.write(&tmp)?;
|
||||
out.write("\"")?;
|
||||
|
||||
if path == ¤t_path {
|
||||
out.write(" class=\"active\"")?;
|
||||
}
|
||||
|
||||
out.write(">")?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
if path == ¤t_path {
|
||||
out.write(" class=\"active\"")?;
|
||||
}
|
||||
|
||||
out.write(">")?;
|
||||
true
|
||||
} else {
|
||||
out.write("<div>")?;
|
||||
false
|
||||
};
|
||||
|
||||
@@ -141,7 +142,7 @@ impl HelperDef for RenderToc {
|
||||
// Section does not necessarily exist
|
||||
if let Some(section) = item.get("section") {
|
||||
out.write("<strong aria-hidden=\"true\">")?;
|
||||
out.write(§ion)?;
|
||||
out.write(section)?;
|
||||
out.write("</strong> ")?;
|
||||
}
|
||||
}
|
||||
@@ -160,11 +161,13 @@ impl HelperDef for RenderToc {
|
||||
html::push_html(&mut markdown_parsed_name, parser);
|
||||
|
||||
// write to the handlebars template
|
||||
out.write(&markdown_parsed_name)?;
|
||||
write_escaped(out, &markdown_parsed_name)?;
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
out.write("</a>")?;
|
||||
} else {
|
||||
out.write("</div>")?;
|
||||
}
|
||||
|
||||
// Render expand/collapse toggle
|
||||
@@ -202,3 +205,18 @@ fn write_li_open_tag(
|
||||
li.push_str("\">");
|
||||
out.write(&li)
|
||||
}
|
||||
|
||||
fn write_escaped(out: &mut dyn Output, mut title: &str) -> io::Result<()> {
|
||||
let needs_escape: &[char] = &['<', '>'];
|
||||
while let Some(next) = title.find(needs_escape) {
|
||||
out.write(&title[..next])?;
|
||||
match title.as_bytes()[next] {
|
||||
b'<' => out.write("<")?,
|
||||
b'>' => out.write(">")?,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
title = &title[next + 1..];
|
||||
}
|
||||
out.write(title)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||
let mut doc_urls = Vec::with_capacity(book.sections.len());
|
||||
|
||||
for item in book.iter() {
|
||||
render_item(&mut index, &search_config, &mut doc_urls, item)?;
|
||||
render_item(&mut index, search_config, &mut doc_urls, item)?;
|
||||
}
|
||||
|
||||
let index = write_to_json(index, &search_config, doc_urls)?;
|
||||
let index = write_to_json(index, search_config, doc_urls)?;
|
||||
debug!("Writing search index ✓");
|
||||
if index.len() > 10_000_000 {
|
||||
warn!("searchindex.json is very large ({} bytes)", index.len());
|
||||
@@ -95,6 +95,8 @@ fn render_item(
|
||||
let mut breadcrumbs = chapter.parent_names.clone();
|
||||
let mut footnote_numbers = HashMap::new();
|
||||
|
||||
breadcrumbs.push(chapter.name.clone());
|
||||
|
||||
while let Some(event) = p.next() {
|
||||
match event {
|
||||
Event::Start(Tag::Heading(i)) if i <= max_section_depth => {
|
||||
@@ -132,14 +134,14 @@ fn render_item(
|
||||
// in an HtmlBlock tag. We must collect consecutive Html events
|
||||
// into a block ourselves.
|
||||
while let Some(Event::Html(html)) = p.peek() {
|
||||
html_block.push_str(&html);
|
||||
html_block.push_str(html);
|
||||
p.next();
|
||||
}
|
||||
|
||||
body.push_str(&clean_html(&html_block));
|
||||
}
|
||||
Event::Start(_) | Event::End(_) | Event::Rule | Event::SoftBreak | Event::HardBreak => {
|
||||
// Insert spaces where HTML output would usually seperate text
|
||||
// Insert spaces where HTML output would usually separate text
|
||||
// to ensure words don't get merged together
|
||||
if in_heading {
|
||||
heading.push(' ');
|
||||
@@ -163,7 +165,12 @@ fn render_item(
|
||||
}
|
||||
}
|
||||
|
||||
if !heading.is_empty() {
|
||||
if !body.is_empty() || !heading.is_empty() {
|
||||
if heading.is_empty() {
|
||||
if let Some(chapter) = breadcrumbs.first() {
|
||||
heading = chapter.clone();
|
||||
}
|
||||
}
|
||||
// Make sure the last section is added to the index
|
||||
add_doc(
|
||||
index,
|
||||
|
||||
@@ -18,9 +18,10 @@ mod html_handlebars;
|
||||
mod markdown_renderer;
|
||||
|
||||
use shlex::Shlex;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::{self, ErrorKind, Read};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use crate::book::Book;
|
||||
@@ -34,12 +35,9 @@ use toml::Value;
|
||||
/// provide your own renderer, there are two main renderer implementations that
|
||||
/// 99% of users will ever use:
|
||||
///
|
||||
/// - [HtmlHandlebars] - the built-in HTML renderer
|
||||
/// - [CmdRenderer] - a generic renderer which shells out to a program to do the
|
||||
/// - [`HtmlHandlebars`] - the built-in HTML renderer
|
||||
/// - [`CmdRenderer`] - a generic renderer which shells out to a program to do the
|
||||
/// actual rendering
|
||||
///
|
||||
/// [HtmlHandlebars]: struct.HtmlHandlebars.html
|
||||
/// [CmdRenderer]: struct.CmdRenderer.html
|
||||
pub trait Renderer {
|
||||
/// The `Renderer`'s name.
|
||||
fn name(&self) -> &str;
|
||||
@@ -67,6 +65,8 @@ pub struct RenderContext {
|
||||
/// guaranteed to be empty or even exist.
|
||||
pub destination: PathBuf,
|
||||
#[serde(skip)]
|
||||
pub(crate) chapter_titles: HashMap<PathBuf, String>,
|
||||
#[serde(skip)]
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ impl RenderContext {
|
||||
version: crate::MDBOOK_VERSION.to_string(),
|
||||
root: root.into(),
|
||||
destination: destination.into(),
|
||||
chapter_titles: HashMap::new(),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
}
|
||||
@@ -133,14 +134,44 @@ impl CmdRenderer {
|
||||
CmdRenderer { name, cmd }
|
||||
}
|
||||
|
||||
fn compose_command(&self) -> Result<Command> {
|
||||
fn compose_command(&self, root: &Path, destination: &Path) -> Result<Command> {
|
||||
let mut words = Shlex::new(&self.cmd);
|
||||
let executable = match words.next() {
|
||||
Some(e) => e,
|
||||
let exe = match words.next() {
|
||||
Some(e) => PathBuf::from(e),
|
||||
None => bail!("Command string was empty"),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(executable);
|
||||
let exe = if exe.components().count() == 1 {
|
||||
// Search PATH for the executable.
|
||||
exe
|
||||
} else {
|
||||
// Relative paths are preferred to be relative to the book root.
|
||||
let abs_exe = root.join(&exe);
|
||||
if abs_exe.exists() {
|
||||
abs_exe
|
||||
} else {
|
||||
// Historically paths were relative to the destination, but
|
||||
// this is not the preferred way.
|
||||
let legacy_path = destination.join(&exe);
|
||||
if legacy_path.exists() {
|
||||
warn!(
|
||||
"Renderer command `{}` uses a path relative to the \
|
||||
renderer output directory `{}`. This was previously \
|
||||
accepted, but has been deprecated. Relative executable \
|
||||
paths should be relative to the book root.",
|
||||
exe.display(),
|
||||
destination.display()
|
||||
);
|
||||
legacy_path
|
||||
} else {
|
||||
// Let this bubble through to later be handled by
|
||||
// handle_render_command_error.
|
||||
abs_exe
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(exe);
|
||||
|
||||
for arg in words {
|
||||
cmd.arg(arg);
|
||||
@@ -195,7 +226,7 @@ impl Renderer for CmdRenderer {
|
||||
let _ = fs::create_dir_all(&ctx.destination);
|
||||
|
||||
let mut child = match self
|
||||
.compose_command()?
|
||||
.compose_command(&ctx.root, &ctx.destination)?
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
|
||||
@@ -108,9 +108,12 @@ function playground_text(playground) {
|
||||
|
||||
let text = playground_text(code_block);
|
||||
let classes = code_block.querySelector('code').classList;
|
||||
let has_2018 = classes.contains("edition2018");
|
||||
let edition = has_2018 ? "2018" : "2015";
|
||||
|
||||
let edition = "2015";
|
||||
if(classes.contains("edition2018")) {
|
||||
edition = "2018";
|
||||
} else if(classes.contains("edition2021")) {
|
||||
edition = "2021";
|
||||
}
|
||||
var params = {
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
@@ -133,7 +136,15 @@ function playground_text(playground) {
|
||||
body: JSON.stringify(params)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => result_block.innerText = response.result)
|
||||
.then(response => {
|
||||
if (response.result.trim() === '') {
|
||||
result_block.innerText = "No output";
|
||||
result_block.classList.add("result-no-output");
|
||||
} else {
|
||||
result_block.innerText = response.result;
|
||||
result_block.classList.remove("result-no-output");
|
||||
}
|
||||
})
|
||||
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
|
||||
}
|
||||
|
||||
@@ -151,12 +162,13 @@ function playground_text(playground) {
|
||||
if (window.ace) {
|
||||
// language-rust class needs to be removed for editable
|
||||
// blocks or highlightjs will capture events
|
||||
Array
|
||||
.from(document.querySelectorAll('code.editable'))
|
||||
code_nodes
|
||||
.filter(function (node) {return node.classList.contains("editable"); })
|
||||
.forEach(function (block) { block.classList.remove('language-rust'); });
|
||||
|
||||
Array
|
||||
.from(document.querySelectorAll('code:not(.editable)'))
|
||||
code_nodes
|
||||
.filter(function (node) {return !node.classList.contains("editable"); })
|
||||
.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
} else {
|
||||
code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
@@ -359,7 +371,14 @@ function playground_text(playground) {
|
||||
});
|
||||
|
||||
themePopup.addEventListener('click', function (e) {
|
||||
var theme = e.target.id || e.target.parentElement.id;
|
||||
var theme;
|
||||
if (e.target.className === "theme") {
|
||||
theme = e.target.id;
|
||||
} else if (e.target.parentElement.className === "theme") {
|
||||
theme = e.target.parentElement.id;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
set_theme(theme);
|
||||
});
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ a > .hljs {
|
||||
.menu-title {
|
||||
display: inline-block;
|
||||
font-weight: 200;
|
||||
font-size: 2rem;
|
||||
font-size: 2.4rem;
|
||||
line-height: var(--menu-bar-height);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
|
||||
@@ -45,20 +45,23 @@ h4, h5 { margin-top: 2em; }
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
h1 a.header:target::before,
|
||||
h2 a.header:target::before,
|
||||
h3 a.header:target::before,
|
||||
h4 a.header:target::before {
|
||||
h1:target::before,
|
||||
h2:target::before,
|
||||
h3:target::before,
|
||||
h4:target::before,
|
||||
h5:target::before,
|
||||
h6:target::before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-left: -30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
h1 a.header:target,
|
||||
h2 a.header:target,
|
||||
h3 a.header:target,
|
||||
h4 a.header:target {
|
||||
/* This is broken on Safari as of version 14, but is fixed
|
||||
in Safari Technology Preview 117 which I think will be Safari 14.2.
|
||||
https://bugs.webkit.org/show_bug.cgi?id=218076
|
||||
*/
|
||||
:target {
|
||||
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
|
||||
}
|
||||
|
||||
@@ -89,7 +92,7 @@ h4 a.header:target {
|
||||
.content ul { line-height: 1.45em; }
|
||||
.content a { text-decoration: none; }
|
||||
.content a:hover { text-decoration: underline; }
|
||||
.content img { max-width: 100%; }
|
||||
.content img, .content video { max-width: 100%; }
|
||||
.content .header:link,
|
||||
.content .header:visited {
|
||||
color: var(--fg);
|
||||
@@ -172,3 +175,7 @@ blockquote {
|
||||
margin: 5px 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.result-no-output {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;;
|
||||
--inline-code-color: #c5c8c6;
|
||||
|
||||
--theme-popup-bg: #141617;
|
||||
--theme-popup-border: #43484d;
|
||||
@@ -92,22 +92,22 @@
|
||||
|
||||
.light {
|
||||
--bg: hsl(0, 0%, 100%);
|
||||
--fg: #333333;
|
||||
--fg: hsl(0, 0%, 0%);
|
||||
|
||||
--sidebar-bg: #fafafa;
|
||||
--sidebar-fg: #364149;
|
||||
--sidebar-fg: hsl(0, 0%, 0%);
|
||||
--sidebar-non-existant: #aaaaaa;
|
||||
--sidebar-active: #008cff;
|
||||
--sidebar-active: #1f1fff;
|
||||
--sidebar-spacer: #f4f4f4;
|
||||
|
||||
--scrollbar: #cccccc;
|
||||
--scrollbar: #8F8F8F;
|
||||
|
||||
--icons: #cccccc;
|
||||
--icons-hover: #333333;
|
||||
--icons: #747474;
|
||||
--icons-hover: #000000;
|
||||
|
||||
--links: #4183c4;
|
||||
--links: #20609f;
|
||||
|
||||
--inline-code-color: #6e6b5e;
|
||||
--inline-code-color: #301900;
|
||||
|
||||
--theme-popup-bg: #fafafa;
|
||||
--theme-popup-border: #cccccc;
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;;
|
||||
--inline-code-color: #c5c8c6;
|
||||
|
||||
--theme-popup-bg: #161923;
|
||||
--theme-popup-border: #737480;
|
||||
@@ -228,7 +228,7 @@
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;;
|
||||
--inline-code-color: #c5c8c6;
|
||||
|
||||
--theme-popup-bg: #141617;
|
||||
--theme-popup-border: #43484d;
|
||||
|
||||
@@ -96,6 +96,5 @@
|
||||
font-family: 'Source Code Pro';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local('Source Code Pro Medium'), local('SourceCodePro-Medium'),
|
||||
url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
|
||||
src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
/* Base16 Atelier Dune Light - Theme */
|
||||
/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */
|
||||
/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */
|
||||
/*
|
||||
* An increased contrast highlighting scheme loosely based on the
|
||||
* "Base16 Atelier Dune Light" theme by Bram de Haan
|
||||
* (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune)
|
||||
* Original Base16 color scheme by Chris Kempson
|
||||
* (https://github.com/chriskempson/base16)
|
||||
*/
|
||||
|
||||
/* Atelier-Dune Comment */
|
||||
/* Comment */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #AAA;
|
||||
color: #575757;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Red */
|
||||
/* Red */
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
@@ -19,10 +23,10 @@
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #d73737;
|
||||
color: #d70025;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Orange */
|
||||
/* Orange */
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-built_in,
|
||||
@@ -30,33 +34,33 @@
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #b65611;
|
||||
color: #b21e00;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Green */
|
||||
/* Green */
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #60ac39;
|
||||
color: #008200;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Blue */
|
||||
/* Blue */
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #6684e1;
|
||||
color: #0030f2;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Purple */
|
||||
/* Purple */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #b854d4;
|
||||
color: #9d00ec;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
background: #f1f1f1;
|
||||
color: #6e6b5e;
|
||||
background: #f6f7f6;
|
||||
color: #000;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
|
||||
{{#if print_enable}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
|
||||
{{/if}}
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
@@ -136,21 +138,29 @@
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
|
||||
<div class="right-buttons">
|
||||
{{#if print_enable}}
|
||||
<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}}
|
||||
{{#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}}
|
||||
{{#if git_repository_edit_url}}
|
||||
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit">
|
||||
<i id="git-edit-button" class="fa fa-edit"></i>
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if search_enabled}}
|
||||
<div id="search-wrapper" class="hidden">
|
||||
<form id="searchbar-outer" class="searchbar-outer">
|
||||
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
</form>
|
||||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||
<div id="searchresults-header" class="searchresults-header"></div>
|
||||
|
||||
@@ -145,6 +145,11 @@ window.search = window.search || {};
|
||||
url.push("");
|
||||
}
|
||||
|
||||
// encodeURIComponent escapes all chars that could allow an XSS except
|
||||
// for '. Due to that we also manually replace ' with its url-encoded
|
||||
// representation (%27).
|
||||
var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27");
|
||||
|
||||
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
|
||||
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
|
||||
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
|
||||
@@ -291,7 +296,7 @@ window.search = window.search || {};
|
||||
}
|
||||
|
||||
if (url.params.hasOwnProperty(URL_MARK_PARAM)) {
|
||||
var words = url.params[URL_MARK_PARAM].split(' ');
|
||||
var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' ');
|
||||
marker.mark(words, {
|
||||
exclude: mark_exclude
|
||||
});
|
||||
@@ -422,6 +427,7 @@ window.search = window.search || {};
|
||||
delete url.params[URL_MARK_PARAM];
|
||||
url.hash = "";
|
||||
} else {
|
||||
delete url.params[URL_MARK_PARAM];
|
||||
delete url.params[URL_SEARCH_PARAM];
|
||||
}
|
||||
// A new search will also add a new history item, so the user can go back
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
/// Naively replaces any path seperator with a forward-slash '/'
|
||||
/// Naively replaces any path separator with a forward-slash '/'
|
||||
pub fn normalize_path(path: &str) -> String {
|
||||
use std::path::is_separator;
|
||||
path.chars()
|
||||
@@ -110,7 +110,10 @@ pub fn copy_files_except_ext(
|
||||
|
||||
for entry in fs::read_dir(from)? {
|
||||
let entry = entry?;
|
||||
let metadata = entry.metadata()?;
|
||||
let metadata = entry
|
||||
.path()
|
||||
.metadata()
|
||||
.with_context(|| format!("Failed to read {:?}", entry.path()))?;
|
||||
|
||||
// If the entry is a dir and the recursive option is enabled, call itself
|
||||
if metadata.is_dir() && recursive {
|
||||
@@ -187,7 +190,17 @@ pub fn get_404_output_file(input_404: &Option<String>) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::copy_files_except_ext;
|
||||
use std::fs;
|
||||
use std::{fs, io::Result, path::Path};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
|
||||
std::os::windows::fs::symlink_file(src, dst)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
|
||||
std::os::unix::fs::symlink(src, dst)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_files_except_ext_test() {
|
||||
@@ -218,6 +231,12 @@ mod tests {
|
||||
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);
|
||||
}
|
||||
if let Err(err) = symlink(
|
||||
&tmp.path().join("file.png"),
|
||||
&tmp.path().join("symlink.png"),
|
||||
) {
|
||||
panic!("Could not symlink file.png: {}", err);
|
||||
}
|
||||
|
||||
// Create output dir
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("output")) {
|
||||
@@ -228,7 +247,7 @@ mod tests {
|
||||
}
|
||||
|
||||
if let Err(e) =
|
||||
copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, None, &["md"])
|
||||
copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"])
|
||||
{
|
||||
panic!("Error while executing the function:\n{:?}", e);
|
||||
}
|
||||
@@ -249,5 +268,8 @@ mod tests {
|
||||
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/symlink.png")).exists() {
|
||||
panic!("output/symlink.png should exist")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +228,14 @@ impl EventQuoteConverter {
|
||||
fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
|
||||
match event {
|
||||
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info))) => {
|
||||
let info: String = info.chars().filter(|ch| !ch.is_whitespace()).collect();
|
||||
let info: String = info
|
||||
.chars()
|
||||
.map(|x| match x {
|
||||
' ' | '\t' => ',',
|
||||
_ => x,
|
||||
})
|
||||
.filter(|ch| !ch.is_whitespace())
|
||||
.collect();
|
||||
|
||||
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::from(info))))
|
||||
}
|
||||
@@ -372,7 +379,7 @@ more text with spaces
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r#"<pre><code class="language-rust,no_run,,,should_panic,,property_3"></code></pre>
|
||||
let expected = r#"<pre><code class="language-rust,,,,,no_run,,,should_panic,,,,property_3"></code></pre>
|
||||
"#;
|
||||
assert_eq!(render_markdown(input, false), expected);
|
||||
assert_eq!(render_markdown(input, true), expected);
|
||||
|
||||
@@ -70,7 +70,7 @@ pub fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> S
|
||||
output.push_str("# ");
|
||||
}
|
||||
output.push_str(line);
|
||||
output.push_str("\n");
|
||||
output.push('\n');
|
||||
}
|
||||
output.pop();
|
||||
output
|
||||
@@ -95,7 +95,7 @@ pub fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
None => {
|
||||
if !ANCHOR_START.is_match(l) {
|
||||
output.push_str(l);
|
||||
output.push_str("\n");
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ pub fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
} else if !ANCHOR_END.is_match(l) {
|
||||
output.push_str("# ");
|
||||
output.push_str(l);
|
||||
output.push_str("\n");
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ impl TomlExt for Value {
|
||||
}
|
||||
|
||||
fn split(key: &str) -> Option<(&str, &str)> {
|
||||
let ix = key.find(".")?;
|
||||
let ix = key.find('.')?;
|
||||
|
||||
let (head, tail) = key.split_at(ix);
|
||||
// splitting will leave the "."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use mdbook::config::Config;
|
||||
use mdbook::MDBook;
|
||||
#[cfg(not(windows))]
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
@@ -71,6 +71,45 @@ fn backends_receive_render_context_via_stdin() {
|
||||
assert!(got.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_command_path() {
|
||||
// Checks behavior of relative paths for the `command` setting.
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
let renderers = temp.path().join("renderers");
|
||||
fs::create_dir(&renderers).unwrap();
|
||||
rust_exe(
|
||||
&renderers,
|
||||
"myrenderer",
|
||||
r#"fn main() {
|
||||
std::fs::write("output", "test").unwrap();
|
||||
}"#,
|
||||
);
|
||||
let do_test = |cmd_path| {
|
||||
let mut config = Config::default();
|
||||
config
|
||||
.set("output.html", toml::value::Table::new())
|
||||
.unwrap();
|
||||
config.set("output.myrenderer.command", cmd_path).unwrap();
|
||||
let md = MDBook::init(&temp.path())
|
||||
.with_config(config)
|
||||
.build()
|
||||
.unwrap();
|
||||
let output = temp.path().join("book/myrenderer/output");
|
||||
assert!(!output.exists());
|
||||
md.build().unwrap();
|
||||
assert!(output.exists());
|
||||
fs::remove_file(output).unwrap();
|
||||
};
|
||||
// Legacy paths work, relative to the output directory.
|
||||
if cfg!(windows) {
|
||||
do_test("../../renderers/myrenderer.exe");
|
||||
} else {
|
||||
do_test("../../renderers/myrenderer");
|
||||
}
|
||||
// Modern path, relative to the book directory.
|
||||
do_test("renderers/myrenderer");
|
||||
}
|
||||
|
||||
fn dummy_book_with_backend(
|
||||
name: &str,
|
||||
command: &str,
|
||||
@@ -112,3 +151,14 @@ fn success_cmd() -> &'static str {
|
||||
"true"
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_exe(temp: &Path, name: &str, src: &str) {
|
||||
let rs = temp.join(name).with_extension("rs");
|
||||
fs::write(&rs, src).unwrap();
|
||||
let status = std::process::Command::new("rustc")
|
||||
.arg(rs)
|
||||
.current_dir(temp)
|
||||
.status()
|
||||
.expect("rustc should run");
|
||||
assert!(status.success());
|
||||
}
|
||||
|
||||
29
tests/cli/build.rs
Normal file
29
tests/cli/build.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use crate::dummy_book::DummyBook;
|
||||
|
||||
use assert_cmd::Command;
|
||||
|
||||
#[test]
|
||||
fn mdbook_cli_dummy_book_generates_index_html() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
|
||||
// doesn't exist before
|
||||
assert!(!temp.path().join("book").exists());
|
||||
|
||||
let mut cmd = Command::cargo_bin("mdbook").unwrap();
|
||||
cmd.arg("build").current_dir(temp.path());
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stderr(
|
||||
predicates::str::is_match(r##"Stack depth exceeded in first[\\/]recursive.md."##)
|
||||
.unwrap(),
|
||||
)
|
||||
.stderr(predicates::str::contains(
|
||||
r##"[INFO] (mdbook::book): Running the html backend"##,
|
||||
));
|
||||
|
||||
// exists afterward
|
||||
assert!(temp.path().join("book").exists());
|
||||
|
||||
let index_file = temp.path().join("book/index.html");
|
||||
assert!(index_file.exists());
|
||||
}
|
||||
2
tests/cli/mod.rs
Normal file
2
tests/cli/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod build;
|
||||
mod test;
|
||||
34
tests/cli/test.rs
Normal file
34
tests/cli/test.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use crate::dummy_book::DummyBook;
|
||||
|
||||
use assert_cmd::Command;
|
||||
use predicates::boolean::PredicateBooleanExt;
|
||||
|
||||
#[test]
|
||||
fn mdbook_cli_can_correctly_test_a_passing_book() {
|
||||
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("mdbook").unwrap();
|
||||
cmd.arg("test").current_dir(temp.path());
|
||||
cmd.assert().success()
|
||||
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]README.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]intro.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]first[\\/]index.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]first[\\/]nested.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"rustdoc returned an error:\n\n"##).unwrap().not())
|
||||
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap().not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mdbook_cli_detects_book_with_failing_tests() {
|
||||
let temp = DummyBook::new().with_passing_test(false).build().unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("mdbook").unwrap();
|
||||
cmd.arg("test").current_dir(temp.path());
|
||||
cmd.assert().failure()
|
||||
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]README.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]intro.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]first[\\/]index.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]first[\\/]nested.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"rustdoc returned an error:\n\n"##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap());
|
||||
}
|
||||
2
tests/cli_tests.rs
Normal file
2
tests/cli_tests.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod cli;
|
||||
mod dummy_book;
|
||||
@@ -135,11 +135,11 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
|
||||
}
|
||||
|
||||
pub fn new_copy_of_example_book() -> Result<TempDir> {
|
||||
let temp = TempFileBuilder::new().prefix("book-example").tempdir()?;
|
||||
let temp = TempFileBuilder::new().prefix("guide").tempdir()?;
|
||||
|
||||
let book_example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
|
||||
let guide = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
|
||||
|
||||
recursive_copy(book_example, temp.path())?;
|
||||
recursive_copy(guide, temp.path())?;
|
||||
|
||||
Ok(temp)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
- [Recursive](first/recursive.md)
|
||||
- [Markdown](first/markdown.md)
|
||||
- [Unicode](first/unicode.md)
|
||||
- [No Headers](first/no-headers.md)
|
||||
- [Second Chapter](second.md)
|
||||
- [Nested Chapter](second/nested.md)
|
||||
|
||||
|
||||
3
tests/dummy_book/src/first/no-headers.md
Normal file
3
tests/dummy_book/src/first/no-headers.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Capybara capybara capybara.
|
||||
|
||||
Capybara capybara capybara.
|
||||
@@ -91,6 +91,12 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
|
||||
file
|
||||
);
|
||||
}
|
||||
|
||||
let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
|
||||
assert_eq!(
|
||||
contents,
|
||||
"[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = false\nsrc = \"in\"\n\n[build]\nbuild-dir = \"out\"\ncreate-missing = true\nuse-default-preprocessors = true\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -102,3 +108,37 @@ fn book_toml_isnt_required() {
|
||||
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_theme() {
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
MDBook::init(temp.path()).copy_theme(true).build().unwrap();
|
||||
let expected = vec![
|
||||
"book.js",
|
||||
"css/chrome.css",
|
||||
"css/general.css",
|
||||
"css/print.css",
|
||||
"css/variables.css",
|
||||
"favicon.png",
|
||||
"favicon.svg",
|
||||
"highlight.css",
|
||||
"highlight.js",
|
||||
"index.hbs",
|
||||
];
|
||||
let theme_dir = temp.path().join("theme");
|
||||
let mut actual: Vec<_> = walkdir::WalkDir::new(&theme_dir)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| !e.file_type().is_dir())
|
||||
.map(|e| {
|
||||
e.path()
|
||||
.strip_prefix(&theme_dir)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace('\\', "/")
|
||||
})
|
||||
.collect();
|
||||
actual.sort();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ const TOC_SECOND_LEVEL: &[&str] = &[
|
||||
"1.3. Recursive",
|
||||
"1.4. Markdown",
|
||||
"1.5. Unicode",
|
||||
"1.6. No Headers",
|
||||
"2.1. Nested Chapter",
|
||||
];
|
||||
|
||||
@@ -104,12 +105,12 @@ fn check_correct_cross_links_in_nested_dir() {
|
||||
|
||||
assert_contains_strings(
|
||||
first.join("index.html"),
|
||||
&[r##"href="#some-section" id="some-section""##],
|
||||
&[r##"<h2 id="some-section"><a class="header" href="#some-section">"##],
|
||||
);
|
||||
|
||||
assert_contains_strings(
|
||||
first.join("nested.html"),
|
||||
&[r##"href="#some-section" id="some-section""##],
|
||||
&[r##"<h2 id="some-section"><a class="header" href="#some-section">"##],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -264,7 +265,7 @@ fn root_index_html() -> Result<Document> {
|
||||
fn check_second_toc_level() {
|
||||
let doc = root_index_html().unwrap();
|
||||
let mut should_be = Vec::from(TOC_SECOND_LEVEL);
|
||||
should_be.sort();
|
||||
should_be.sort_unstable();
|
||||
|
||||
let pred = descendants!(
|
||||
Class("chapter"),
|
||||
@@ -288,7 +289,7 @@ fn check_first_toc_level() {
|
||||
let mut should_be = Vec::from(TOC_TOP_LEVEL);
|
||||
|
||||
should_be.extend(TOC_SECOND_LEVEL);
|
||||
should_be.sort();
|
||||
should_be.sort_unstable();
|
||||
|
||||
let pred = descendants!(
|
||||
Class("chapter"),
|
||||
@@ -345,7 +346,7 @@ fn create_missing_file_with_config() {
|
||||
}
|
||||
|
||||
/// This makes sure you can include a Rust file with `{{#playground example.rs}}`.
|
||||
/// Specification is in `book-example/src/format/rust.md`
|
||||
/// Specification is in `guide/src/format/rust.md`
|
||||
#[test]
|
||||
fn able_to_include_playground_files_in_chapters() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
@@ -373,7 +374,7 @@ fn able_to_include_files_in_chapters() {
|
||||
let includes = temp.path().join("book/first/includes.html");
|
||||
|
||||
let summary_strings = &[
|
||||
r##"<h1><a class="header" href="#summary" id="summary">Summary</a></h1>"##,
|
||||
r##"<h1 id="summary"><a class="header" href="#summary">Summary</a></h1>"##,
|
||||
">First Chapter</a>",
|
||||
];
|
||||
assert_contains_strings(&includes, summary_strings);
|
||||
@@ -535,12 +536,63 @@ fn redirects_are_emitted_correctly() {
|
||||
let mut redirect_file = md.build_dir_for("html");
|
||||
// append everything except the bits that make it absolute
|
||||
// (e.g. "/" or "C:\")
|
||||
redirect_file.extend(remove_absolute_components(&original));
|
||||
redirect_file.extend(remove_absolute_components(original));
|
||||
let contents = fs::read_to_string(&redirect_file).unwrap();
|
||||
assert!(contents.contains(redirect));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_url_has_default_src_dir_edit_url() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let book_toml = r#"
|
||||
[book]
|
||||
title = "implicit"
|
||||
|
||||
[output.html]
|
||||
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||
"#;
|
||||
|
||||
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
|
||||
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let index_html = temp.path().join("book").join("index.html");
|
||||
assert_contains_strings(
|
||||
index_html,
|
||||
&[
|
||||
r#"href="https://github.com/rust-lang/mdBook/edit/master/guide/src/README.md" title="Suggest an edit""#,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_url_has_configured_src_dir_edit_url() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let book_toml = r#"
|
||||
[book]
|
||||
title = "implicit"
|
||||
src = "src2"
|
||||
|
||||
[output.html]
|
||||
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||
"#;
|
||||
|
||||
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
|
||||
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let index_html = temp.path().join("book").join("index.html");
|
||||
assert_contains_strings(
|
||||
index_html,
|
||||
&[
|
||||
r#"href="https://github.com/rust-lang/mdBook/edit/master/guide/src2/README.md" title="Suggest an edit""#,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
fn remove_absolute_components(path: &Path) -> impl Iterator<Item = Component> + '_ {
|
||||
path.components().skip_while(|c| match c {
|
||||
Component::Prefix(_) | Component::RootDir => true,
|
||||
@@ -560,7 +612,7 @@ mod search {
|
||||
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()
|
||||
serde_json::from_str(index).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -580,6 +632,7 @@ mod search {
|
||||
let introduction = get_doc_ref("intro.html#introduction");
|
||||
let some_section = get_doc_ref("first/index.html#some-section");
|
||||
let summary = get_doc_ref("first/includes.html#summary");
|
||||
let no_headers = get_doc_ref("first/no-headers.html");
|
||||
let conclusion = get_doc_ref("conclusion.html#conclusion");
|
||||
|
||||
let bodyidx = &index["index"]["index"]["body"]["root"];
|
||||
@@ -593,10 +646,21 @@ mod search {
|
||||
assert_eq!(docs[&some_section]["body"], "");
|
||||
assert_eq!(
|
||||
docs[&summary]["body"],
|
||||
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode Second Chapter Nested Chapter Conclusion"
|
||||
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Second Chapter Nested Chapter Conclusion"
|
||||
);
|
||||
assert_eq!(
|
||||
docs[&summary]["breadcrumbs"],
|
||||
"First Chapter » Includes » Summary"
|
||||
);
|
||||
assert_eq!(docs[&summary]["breadcrumbs"], "First Chapter » Summary");
|
||||
assert_eq!(docs[&conclusion]["body"], "I put <HTML> in here!");
|
||||
assert_eq!(
|
||||
docs[&no_headers]["breadcrumbs"],
|
||||
"First Chapter » No Headers"
|
||||
);
|
||||
assert_eq!(
|
||||
docs[&no_headers]["body"],
|
||||
"Capybara capybara capybara. Capybara capybara capybara."
|
||||
);
|
||||
}
|
||||
|
||||
// Setting this to `true` may cause issues with `cargo watch`,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user