Merge branch 'master' of https://github.com/rust-lang/mdBook
This commit is contained in:
commit
fd3eadb84d
|
@ -31,7 +31,8 @@ jobs:
|
|||
rust: stable
|
||||
- build: msrv
|
||||
os: ubuntu-latest
|
||||
rust: 1.46.0
|
||||
# sync MSRV with docs: guide/src/guide/installation.md
|
||||
rust: 1.56.0
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust
|
||||
|
|
86
CHANGELOG.md
86
CHANGELOG.md
|
@ -1,5 +1,91 @@
|
|||
# Changelog
|
||||
|
||||
## mdBook 0.4.21
|
||||
[92afe9b...8f01d02](https://github.com/rust-lang/mdBook/compare/92afe9b...8f01d02)
|
||||
|
||||
### Fixed
|
||||
- Fixed an issue where mdBook would fail to compile with Rust nightly-2022-07-22.
|
||||
[#1861](https://github.com/rust-lang/mdBook/pull/1861)
|
||||
|
||||
## mdBook 0.4.20
|
||||
[53055e0...da166e0](https://github.com/rust-lang/mdBook/compare/53055e0...da166e0)
|
||||
|
||||
### Fixed
|
||||
- Fixed a regression in 0.4.19 where inline code would have excessive padding
|
||||
in some situations such as headings.
|
||||
[#1855](https://github.com/rust-lang/mdBook/pull/1855)
|
||||
|
||||
## mdBook 0.4.19
|
||||
[ae275ad...53055e0](https://github.com/rust-lang/mdBook/compare/ae275ad...53055e0)
|
||||
|
||||
### Added
|
||||
- The `serve` command now supports HEAD requests.
|
||||
[#1825](https://github.com/rust-lang/mdBook/pull/1825)
|
||||
|
||||
### Changed
|
||||
- An error is now generated when a custom theme directory does not exist.
|
||||
[#1791](https://github.com/rust-lang/mdBook/pull/1791)
|
||||
- Very wide tables now have independent horizontal scrolling so that scrolling
|
||||
to see the rest of the table will not scroll the entire page.
|
||||
[#1617](https://github.com/rust-lang/mdBook/pull/1617)
|
||||
- The buttons on code blocks are now only shown when the mouse cursor hovers
|
||||
over them (or tapped on mobile). There is also some extra spacing to reduce
|
||||
the overlap with the code.
|
||||
[#1806](https://github.com/rust-lang/mdBook/pull/1806)
|
||||
- The first chapter always generates an `index.html` file. Previously it would
|
||||
only generate the index file for prefix chapters.
|
||||
[#1829](https://github.com/rust-lang/mdBook/pull/1829)
|
||||
|
||||
### Fixed
|
||||
- `mdbook serve --open` now properly handles the case if the first chapter is a draft.
|
||||
[#1714](https://github.com/rust-lang/mdBook/pull/1714)
|
||||
[#1830](https://github.com/rust-lang/mdBook/pull/1830)
|
||||
- Very long words (over 80 characters) are no longer indexed to avoid a stack overflow.
|
||||
[#1833](https://github.com/rust-lang/mdBook/pull/1833)
|
||||
|
||||
## mdBook 0.4.18
|
||||
[981b79b...ae275ad](https://github.com/rust-lang/mdBook/compare/981b79b...ae275ad)
|
||||
|
||||
### Fixed
|
||||
- Fixed rendering of SUMMARY links that contain markdown escapes or other
|
||||
markdown elements.
|
||||
[#1785](https://github.com/rust-lang/mdBook/pull/1785)
|
||||
|
||||
## mdBook 0.4.17
|
||||
[a5fddfa...981b79b](https://github.com/rust-lang/mdBook/compare/a5fddfa...981b79b)
|
||||
|
||||
### Fixed
|
||||
- Fixed parsing of `output.html.print` configuration table.
|
||||
[#1775](https://github.com/rust-lang/mdBook/pull/1775)
|
||||
|
||||
## mdBook 0.4.16
|
||||
[68a5c09...a5fddfa](https://github.com/rust-lang/mdBook/compare/68a5c09...a5fddfa)
|
||||
|
||||
### Added
|
||||
- Added `output.html.print.page-break` config option to control whether or not
|
||||
there is a page break between chapters in the print output.
|
||||
[#1728](https://github.com/rust-lang/mdBook/pull/1728)
|
||||
- Added `output.html.playground.runnable` config option to globally disable
|
||||
the run button in code blocks.
|
||||
[#1546](https://github.com/rust-lang/mdBook/pull/1546)
|
||||
|
||||
### Changed
|
||||
- The `mdbook serve` live reload websocket now uses the protocol, host, and
|
||||
port of the current page, allowing access through a proxy.
|
||||
[#1771](https://github.com/rust-lang/mdBook/pull/1771)
|
||||
- The 404 not-found page now includes the books title in the HTML title tag.
|
||||
[#1693](https://github.com/rust-lang/mdBook/pull/1693)
|
||||
- Migrated to clap 3.0 which which handles CLI option parsing.
|
||||
[#1731](https://github.com/rust-lang/mdBook/pull/1731)
|
||||
|
||||
### Fixed
|
||||
- Minor fixes to the markdown parser.
|
||||
[#1729](https://github.com/rust-lang/mdBook/pull/1729)
|
||||
- Fixed incorrect parsing in `SUMMARY.md` when it didn't start with a title.
|
||||
[#1744](https://github.com/rust-lang/mdBook/pull/1744)
|
||||
- Fixed duplicate anchor IDs for links in search results.
|
||||
[#1749](https://github.com/rust-lang/mdBook/pull/1749)
|
||||
|
||||
## mdBook 0.4.15
|
||||
[5eb7d46...68a5c09](https://github.com/rust-lang/mdBook/compare/5eb7d46...68a5c09)
|
||||
|
||||
|
|
|
@ -28,9 +28,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.11.0"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
@ -185,17 +185,27 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.33.3"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
|
||||
checksum = "7a30c3bf9ff12dfe5dae53f0a96e0febcd18420d1c0e7fad77796d9d5c4b5375"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"atty",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"os_str_bytes",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
"unicode-width",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d044e9db8cd0f68191becdeb5246b7462e4cf0c069b19ae00d1bf3fa9889498d"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -218,10 +228,10 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "difference"
|
||||
version = "2.0.0"
|
||||
name = "diff"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
|
||||
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
|
||||
|
||||
[[package]]
|
||||
name = "difflib"
|
||||
|
@ -261,24 +271,21 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
|||
|
||||
[[package]]
|
||||
name = "elasticlunr-rs"
|
||||
version = "2.3.13"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "515a402b5acb08002194dd926065be7733003bb37ac0f030dfd39160028238e1"
|
||||
checksum = "e6dae5cac90640734ee881bc5f21b6e5123f4e5235e52428db114abffc2391d6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.7.1"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
|
||||
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"humantime",
|
||||
|
@ -375,25 +382,11 @@ dependencies = [
|
|||
"new_debug_unreachable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.16"
|
||||
version = "0.3.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9"
|
||||
checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
|
@ -401,15 +394,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.16"
|
||||
version = "0.3.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582"
|
||||
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
|
@ -426,9 +413,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.16"
|
||||
version = "0.3.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53"
|
||||
checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
|
@ -538,7 +525,7 @@ dependencies = [
|
|||
"log",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"quick-error 2.0.1",
|
||||
"quick-error",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
@ -574,15 +561,6 @@ dependencies = [
|
|||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
|
@ -642,12 +620,9 @@ checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
|
|||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "1.3.0"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
|
||||
dependencies = [
|
||||
"quick-error 1.2.3",
|
||||
]
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
|
@ -714,15 +689,6 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "input_buffer"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iovec"
|
||||
version = "0.1.4"
|
||||
|
@ -830,13 +796,14 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
|||
|
||||
[[package]]
|
||||
name = "mdbook"
|
||||
version = "0.4.15"
|
||||
version = "0.4.21"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"elasticlunr-rs",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
|
@ -854,7 +821,6 @@ dependencies = [
|
|||
"select",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"tempfile",
|
||||
|
@ -1053,6 +1019,15 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "output_vt100"
|
||||
version = "0.1.2"
|
||||
|
@ -1225,13 +1200,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "0.6.1"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
|
||||
checksum = "c89f989ac94207d048d92db058e4f6ec7342b0971fc58d1271ca148b799b3563"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"ctor",
|
||||
"difference",
|
||||
"diff",
|
||||
"output_vt100",
|
||||
]
|
||||
|
||||
|
@ -1267,12 +1242,6 @@ dependencies = [
|
|||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
|
@ -1463,6 +1432,9 @@ name = "serde"
|
|||
version = "1.0.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1f72836d2aa753853178eda473a3b9d8e4eefdaf20523b919677e6de489f8f1"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
|
@ -1578,27 +1550,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
|
@ -1647,11 +1601,28 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1681,11 +1652,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.10.0"
|
||||
version = "1.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b"
|
||||
checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
"libc",
|
||||
"memchr",
|
||||
|
@ -1698,9 +1668,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "1.3.0"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
|
||||
checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1720,9 +1690,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.13.0"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1a5f475f1b9d077ea1017ecbc60890fda8e54942d680ca0b1d2b47cfa2d861b"
|
||||
checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
|
@ -1801,19 +1771,19 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
|
|||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.12.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24"
|
||||
checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"http",
|
||||
"httparse",
|
||||
"input_buffer",
|
||||
"log",
|
||||
"rand 0.8.4",
|
||||
"sha-1 0.9.7",
|
||||
"thiserror",
|
||||
"url",
|
||||
"utf-8",
|
||||
]
|
||||
|
@ -1854,18 +1824,6 @@ dependencies = [
|
|||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
|
@ -1890,12 +1848,6 @@ version = "0.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.3"
|
||||
|
@ -1934,12 +1886,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "warp"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "332d47745e9a0c38636dbd454729b147d16bd1ed08ae67b3ab281c4506771054"
|
||||
checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"headers",
|
||||
"http",
|
||||
"hyper",
|
||||
|
|
18
Cargo.toml
18
Cargo.toml
|
@ -1,13 +1,13 @@
|
|||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.4.15"
|
||||
version = "0.4.21"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
"Matt Ickstadt <mattico8@gmail.com>"
|
||||
]
|
||||
documentation = "http://rust-lang.github.io/mdBook/index.html"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
exclude = ["/guide/*"]
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
license = "MPL-2.0"
|
||||
|
@ -18,8 +18,9 @@ description = "Creates a book from markdown files"
|
|||
[dependencies]
|
||||
anyhow = "1.0.28"
|
||||
chrono = "0.4"
|
||||
clap = "2.24"
|
||||
env_logger = "0.7.1"
|
||||
clap = { version = "3.0", features = ["cargo"] }
|
||||
clap_complete = "3.0"
|
||||
env_logger = "0.9.0"
|
||||
handlebars = "4.0"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4"
|
||||
|
@ -27,8 +28,7 @@ memchr = "2.0"
|
|||
opener = "0.5"
|
||||
pulldown-cmark = { version = "0.9.1", default-features = false }
|
||||
regex = "1.5.5"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
shlex = "1"
|
||||
tempfile = "3.0"
|
||||
|
@ -42,10 +42,10 @@ gitignore = { version = "1.0", optional = true }
|
|||
# Serve feature
|
||||
futures-util = { version = "0.3.4", optional = true }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
warp = { version = "0.3.1", default-features = false, features = ["websocket"], optional = true }
|
||||
warp = { version = "0.3.2", default-features = false, features = ["websocket"], optional = true }
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
||||
elasticlunr-rs = { version = "3.0.0", optional = true }
|
||||
ammonia = { version = "3", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -53,7 +53,7 @@ assert_cmd = "1"
|
|||
predicates = "2"
|
||||
select = "0.5"
|
||||
semver = "1.0"
|
||||
pretty_assertions = "0.6"
|
||||
pretty_assertions = "1.2.1"
|
||||
walkdir = "2.0"
|
||||
|
||||
[features]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::nop_lib::Nop;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use clap::{App, Arg, ArgMatches};
|
||||
use mdbook::book::Book;
|
||||
use mdbook::errors::Error;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
||||
|
@ -7,12 +7,12 @@ use semver::{Version, VersionReq};
|
|||
use std::io;
|
||||
use std::process;
|
||||
|
||||
pub fn make_app() -> App<'static, 'static> {
|
||||
pub fn make_app() -> App<'static> {
|
||||
App::new("nop-preprocessor")
|
||||
.about("A mdbook preprocessor which does precisely nothing")
|
||||
.subcommand(
|
||||
SubCommand::with_name("supports")
|
||||
.arg(Arg::with_name("renderer").required(true))
|
||||
App::new("supports")
|
||||
.arg(Arg::new("renderer").required(true))
|
||||
.about("Check whether a renderer is supported by this preprocessor"),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -61,3 +61,8 @@ The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
|||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
#### --chapter
|
||||
|
||||
The `--chapter` (`-c`) option allows you to test a specific chapter of the
|
||||
book using the chapter name or the relative path to the chapter.
|
|
@ -21,7 +21,7 @@ A simple approach would be to use the popular `curl` CLI tool to download the ex
|
|||
|
||||
```sh
|
||||
mkdir bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.15/mdbook-v0.4.15-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.21/mdbook-v0.4.21-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
bin/mdbook build
|
||||
```
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ Here is an example of what a ***book.toml*** file might look like:
|
|||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
author = "John Doe"
|
||||
authors = ["John Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
[rust]
|
||||
|
|
|
@ -157,7 +157,8 @@ The following configuration options are available:
|
|||
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 `/`.
|
||||
urls in subdirectories. Defaults to `/`. If `site-url` is set,
|
||||
make sure to use document relative links for your assets, meaning they should not start with `/`.
|
||||
- **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
|
||||
|
@ -207,6 +208,7 @@ editable = false # allows editing the source code
|
|||
copyable = true # include the copy button for copying code snippets
|
||||
copy-js = true # includes the JavaScript for the code editor
|
||||
line-numbers = false # displays line numbers for editable code
|
||||
runnable = true # displays a run button for rust code
|
||||
```
|
||||
|
||||
- **editable:** Allow editing the source code. Defaults to `false`.
|
||||
|
@ -214,6 +216,7 @@ line-numbers = false # displays line numbers for editable code
|
|||
- **copy-js:** Copy JavaScript files for the editor to the output directory.
|
||||
Defaults to `true`.
|
||||
- **line-numbers** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`.
|
||||
- **runnable** Displays a run button for rust code snippets. Changing this to `false` will disable the run in playground feature globally. Defaults to `true`.
|
||||
|
||||
[Ace]: https://ace.c9.io/
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ println!("Hello, World!");
|
|||
|
||||
If there is no `main` function, then the code is automatically wrapped inside one.
|
||||
|
||||
If you wish to disable the play button, you can include the `noplayground` option on the code block like this:
|
||||
If you wish to disable the play button for a code block, you can include the `noplayground` option on the code block like this:
|
||||
|
||||
~~~markdown
|
||||
```rust,noplayground
|
||||
|
@ -51,6 +51,13 @@ println!("Hello {}!", name);
|
|||
```
|
||||
~~~
|
||||
|
||||
Or, if you wish to disable the play button for all code blocks in your book, you can write the config to the `book.toml` like this.
|
||||
|
||||
```toml
|
||||
[output.html.playground]
|
||||
runnable = false
|
||||
```
|
||||
|
||||
## Rust code block attributes
|
||||
|
||||
Additional attributes can be included in Rust code blocks with comma, space, or tab-separated terms just after the language term. For example:
|
||||
|
|
|
@ -97,7 +97,7 @@ So if you have images or other static files, just include them somewhere in the
|
|||
|
||||
Once you've written your book, you may want to host it somewhere for others to view.
|
||||
The first step is to build the output of the book.
|
||||
This can be done with the `mbdook build` command in the same directory where the `book.toml` file is located:
|
||||
This can be done with the `mdbook build` command in the same directory where the `book.toml` file is located:
|
||||
|
||||
```sh
|
||||
mdbook build
|
||||
|
|
|
@ -20,7 +20,7 @@ To make it easier to run, put the path to the binary into your `PATH`.
|
|||
|
||||
To build the `mdbook` executable from source, you will first need to install Rust and Cargo.
|
||||
Follow the instructions on the [Rust installation page].
|
||||
mdBook currently requires at least Rust version 1.46.
|
||||
mdBook currently requires at least Rust version 1.54.
|
||||
|
||||
Once you have installed Rust, the following command can be used to build and install mdBook:
|
||||
|
||||
|
@ -30,6 +30,8 @@ cargo install mdbook
|
|||
|
||||
This will automatically download mdBook from [crates.io], build it, and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
|
||||
|
||||
To uninstall, run the command `cargo uninstall mdbook`.
|
||||
|
||||
[Rust installation page]: https://www.rust-lang.org/tools/install
|
||||
[crates.io]: https://crates.io/
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@ use std::path::{Path, PathBuf};
|
|||
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
use crate::config::BuildConfig;
|
||||
use crate::errors::*;
|
||||
use crate::utils::bracket_escape;
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Load a book into memory from its `src/` directory.
|
||||
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
|
||||
|
@ -53,7 +56,7 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
|||
let mut f = File::create(&filename).with_context(|| {
|
||||
format!("Unable to create missing file: {}", filename.display())
|
||||
})?;
|
||||
writeln!(f, "# {}", link.name)?;
|
||||
writeln!(f, "# {}", bracket_escape(&link.name))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use super::MDBook;
|
|||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use crate::theme;
|
||||
use log::{debug, error, info, trace};
|
||||
|
||||
/// A helper for setting up a new book and its directory structure.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
|
|
@ -14,6 +14,7 @@ pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
|
|||
pub use self::init::BookBuilder;
|
||||
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
|
||||
use log::{debug, error, info, log_enabled, trace, warn};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
@ -246,6 +247,13 @@ impl MDBook {
|
|||
|
||||
/// Run `rustdoc` tests on the book, linking against the provided libraries.
|
||||
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
|
||||
// test_chapter with chapter:None will run all tests.
|
||||
self.test_chapter(library_paths, None)
|
||||
}
|
||||
|
||||
/// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries.
|
||||
/// If `chapter` is `None`, all tests will be run.
|
||||
pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
|
||||
let library_args: Vec<&str> = (0..library_paths.len())
|
||||
.map(|_| "-L")
|
||||
.zip(library_paths.into_iter())
|
||||
|
@ -254,6 +262,8 @@ impl MDBook {
|
|||
|
||||
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
||||
|
||||
let mut chapter_found = false;
|
||||
|
||||
// FIXME: Is "test" the proper renderer name to use here?
|
||||
let preprocess_context =
|
||||
PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
|
||||
|
@ -270,8 +280,16 @@ impl MDBook {
|
|||
_ => continue,
|
||||
};
|
||||
|
||||
let path = self.source_dir().join(&chapter_path);
|
||||
info!("Testing file: {:?}", path);
|
||||
if let Some(chapter) = chapter {
|
||||
if ch.name != chapter && chapter_path.to_str() != Some(chapter) {
|
||||
if chapter == "?" {
|
||||
info!("Skipping chapter '{}'...", ch.name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
chapter_found = true;
|
||||
info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
|
||||
|
||||
// write preprocessed file to tempdir
|
||||
let path = temp_dir.path().join(&chapter_path);
|
||||
|
@ -311,6 +329,11 @@ impl MDBook {
|
|||
if failed {
|
||||
bail!("One or more tests failed");
|
||||
}
|
||||
if let Some(chapter) = chapter {
|
||||
if !chapter_found {
|
||||
bail!("Chapter not found: {}", chapter);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -386,7 +409,7 @@ fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
|
|||
renderers
|
||||
}
|
||||
|
||||
const DEFAULT_PREPROCESSORS: &[&'static str] = &["links", "index"];
|
||||
const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
|
||||
|
||||
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
|
||||
let name = pre.name();
|
||||
|
@ -756,10 +779,9 @@ mod tests {
|
|||
|
||||
let preprocessors = determine_preprocessors(&cfg).unwrap();
|
||||
|
||||
assert!(preprocessors
|
||||
assert!(!preprocessors
|
||||
.iter()
|
||||
.find(|preprocessor| preprocessor.name() == "random")
|
||||
.is_none());
|
||||
.any(|preprocessor| preprocessor.name() == "random"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -776,10 +798,9 @@ mod tests {
|
|||
|
||||
let preprocessors = determine_preprocessors(&cfg).unwrap();
|
||||
|
||||
assert!(preprocessors
|
||||
assert!(!preprocessors
|
||||
.iter()
|
||||
.find(|preprocessor| preprocessor.name() == "links")
|
||||
.is_none());
|
||||
.any(|preprocessor| preprocessor.name() == "links"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use crate::errors::*;
|
||||
use log::{debug, trace, warn};
|
||||
use memchr::{self, Memchr};
|
||||
use pulldown_cmark::{self, Event, HeadingLevel, Tag};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::iter::FromIterator;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
@ -536,6 +538,10 @@ impl<'a> SummaryParser<'a> {
|
|||
// Skip a HTML element such as a comment line.
|
||||
Some(Event::Html(_)) => {}
|
||||
// Otherwise, no title.
|
||||
Some(ev) => {
|
||||
self.back(ev);
|
||||
return None;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
@ -647,6 +653,18 @@ mod tests {
|
|||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_initial_title() {
|
||||
let src = "[Link]()";
|
||||
let mut parser = SummaryParser::new(src);
|
||||
|
||||
assert!(parser.parse_title().is_none());
|
||||
assert!(matches!(
|
||||
parser.next_event(),
|
||||
Some(Event::Start(Tag::Paragraph))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_title_with_styling() {
|
||||
let src = "# My **Awesome** Summary";
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
use crate::{get_book_dir, open};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("build")
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("build")
|
||||
.about("Builds a book from its markdown files")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
.arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
|
||||
),
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(arg!(-o --open "Opens the compiled book in a web browser"))
|
||||
}
|
||||
|
||||
// Build command implementation
|
||||
|
@ -32,7 +38,12 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
|
||||
if args.is_present("open") {
|
||||
// FIXME: What's the right behaviour if we don't use the HTML renderer?
|
||||
open(book.build_dir_for("html").join("index.html"));
|
||||
let path = book.build_dir_for("html").join("index.html");
|
||||
if !path.exists() {
|
||||
error!("No chapter available to open");
|
||||
std::process::exit(1)
|
||||
}
|
||||
open(path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
use crate::get_book_dir;
|
||||
use anyhow::Context;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::MDBook;
|
||||
use std::fs;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("clean")
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("clean")
|
||||
.about("Deletes a built book")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
Running this command deletes this directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
.arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
|
||||
),
|
||||
)
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
}
|
||||
|
||||
// Clean command implementation
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::get_book_dir;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::config;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
|
@ -8,25 +8,25 @@ use std::io::Write;
|
|||
use std::process::Command;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("init")
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("init")
|
||||
.about("Creates the boilerplate structure and files for a new book")
|
||||
// the {n} denotes a newline which will properly aligned in all help messages
|
||||
.arg_from_usage(
|
||||
"[dir] 'Directory to create the book in{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
||||
.arg_from_usage("--force 'Skips confirmation prompts'")
|
||||
.arg(arg!([dir]
|
||||
"Directory to create the book in{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(arg!(--theme "Copies the default theme into your source folder"))
|
||||
.arg(arg!(--force "Skips confirmation prompts"))
|
||||
.arg(
|
||||
Arg::with_name("title")
|
||||
Arg::new("title")
|
||||
.long("title")
|
||||
.takes_value(true)
|
||||
.help("Sets the book title")
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("ignore")
|
||||
Arg::new("ignore")
|
||||
.long("ignore")
|
||||
.takes_value(true)
|
||||
.possible_values(&["none", "git"])
|
||||
|
@ -122,8 +122,5 @@ fn confirm() -> bool {
|
|||
io::stdout().flush().unwrap();
|
||||
let mut s = String::new();
|
||||
io::stdin().read_line(&mut s).ok();
|
||||
match &*s.trim() {
|
||||
"Y" | "y" | "yes" | "Yes" => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(&*s.trim(), "Y" | "y" | "yes" | "Yes")
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#[cfg(feature = "watch")]
|
||||
use super::watch;
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use futures_util::sink::SinkExt;
|
||||
use futures_util::StreamExt;
|
||||
use mdbook::errors::*;
|
||||
|
@ -18,37 +18,43 @@ use warp::Filter;
|
|||
const LIVE_RELOAD_ENDPOINT: &str = "__livereload";
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("serve")
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("serve")
|
||||
.about("Serves a book at http://localhost:3000, and rebuilds it on changes")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("hostname")
|
||||
.short("n")
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
|
||||
),
|
||||
)
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(
|
||||
Arg::new("hostname")
|
||||
.short('n')
|
||||
.long("hostname")
|
||||
.takes_value(true)
|
||||
.default_value("localhost")
|
||||
.empty_values(false)
|
||||
.forbid_empty_values(true)
|
||||
.help("Hostname to listen on for HTTP connections"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("port")
|
||||
.short("p")
|
||||
Arg::new("port")
|
||||
.short('p')
|
||||
.long("port")
|
||||
.takes_value(true)
|
||||
.default_value("3000")
|
||||
.empty_values(false)
|
||||
.forbid_empty_values(true)
|
||||
.help("Port to use for HTTP connections"),
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Opens the book server in a web browser'")
|
||||
.arg(arg!(-o --open "Opens the compiled book in a web browser"))
|
||||
}
|
||||
|
||||
// Serve command implementation
|
||||
|
@ -62,11 +68,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
|
||||
let address = format!("{}:{}", hostname, port);
|
||||
|
||||
let livereload_url = format!("ws://{}/{}", address, LIVE_RELOAD_ENDPOINT);
|
||||
let update_config = |book: &mut MDBook| {
|
||||
book.config
|
||||
.set("output.html.livereload-url", &livereload_url)
|
||||
.expect("livereload-url update failed");
|
||||
.set("output.html.live-reload-endpoint", &LIVE_RELOAD_ENDPOINT)
|
||||
.expect("live-reload-endpoint update failed");
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
|
@ -84,8 +89,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
let input_404 = book
|
||||
.config
|
||||
.get("output.html.input-404")
|
||||
.map(toml::Value::as_str)
|
||||
.and_then(std::convert::identity) // flatten
|
||||
.and_then(toml::Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
let file_404 = get_404_output_file(&input_404);
|
||||
|
||||
|
|
|
@ -1,29 +1,47 @@
|
|||
use crate::get_book_dir;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("test")
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("test")
|
||||
.about("Tests that a book's Rust code samples compile")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
.arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
|
||||
),
|
||||
).arg(
|
||||
Arg::new("chapter")
|
||||
.short('c')
|
||||
.long("chapter")
|
||||
.value_name("chapter")
|
||||
.help(
|
||||
"Only test the specified chapter{n}\
|
||||
Where the name of the chapter is defined in the SUMMARY.md file.{n}\
|
||||
Use the special name \"?\" to the list of chapter names."
|
||||
)
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg(Arg::with_name("library-path")
|
||||
.short("L")
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(Arg::new("library-path")
|
||||
.short('L')
|
||||
.long("library-path")
|
||||
.value_name("dir")
|
||||
.takes_value(true)
|
||||
.use_delimiter(true)
|
||||
.require_delimiter(true)
|
||||
.multiple(true)
|
||||
.empty_values(false)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.forbid_empty_values(true)
|
||||
.help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
|
||||
}
|
||||
|
||||
|
@ -33,14 +51,18 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
.values_of("library-path")
|
||||
.map(std::iter::Iterator::collect)
|
||||
.unwrap_or_default();
|
||||
let chapter: Option<&str> = args.value_of("chapter");
|
||||
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
|
||||
book.test(library_paths)?;
|
||||
match chapter {
|
||||
Some(_) => book.test_chapter(library_paths, chapter),
|
||||
None => book.test(library_paths),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{get_book_dir, open};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
|
@ -10,19 +10,25 @@ use std::thread::sleep;
|
|||
use std::time::Duration;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("watch")
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("watch")
|
||||
.about("Watches a book's files and rebuilds it on changes")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
.arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
|
||||
),
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(arg!(-o --open "Opens the compiled book in a web browser"))
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
|
@ -39,7 +45,12 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
|
||||
if args.is_present("open") {
|
||||
book.build()?;
|
||||
open(book.build_dir_for("html").join("index.html"));
|
||||
let path = book.build_dir_for("html").join("index.html");
|
||||
if !path.exists() {
|
||||
error!("No chapter available to open");
|
||||
std::process::exit(1)
|
||||
}
|
||||
open(path);
|
||||
}
|
||||
|
||||
trigger_on_change(&book, |paths, book_dir| {
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use log::{debug, trace, warn};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
|
@ -227,10 +228,10 @@ impl Config {
|
|||
let value = Value::try_from(value)
|
||||
.with_context(|| "Unable to represent the item as a JSON Value")?;
|
||||
|
||||
if index.starts_with("book.") {
|
||||
self.book.update_value(&index[5..], value);
|
||||
} else if index.starts_with("build.") {
|
||||
self.build.update_value(&index[6..], value);
|
||||
if let Some(key) = index.strip_prefix("book.") {
|
||||
self.book.update_value(key, value);
|
||||
} else if let Some(key) = index.strip_prefix("build.") {
|
||||
self.build.update_value(key, value);
|
||||
} else {
|
||||
self.rest.insert(index, value);
|
||||
}
|
||||
|
@ -295,7 +296,7 @@ impl Default for Config {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Config {
|
||||
impl<'de> serde::Deserialize<'de> for Config {
|
||||
fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
|
||||
let raw = Value::deserialize(de)?;
|
||||
|
||||
|
@ -371,15 +372,8 @@ impl Serialize for Config {
|
|||
}
|
||||
|
||||
fn parse_env(key: &str) -> Option<String> {
|
||||
const PREFIX: &str = "MDBOOK_";
|
||||
|
||||
if key.starts_with(PREFIX) {
|
||||
let key = &key[PREFIX.len()..];
|
||||
|
||||
Some(key.to_lowercase().replace("__", ".").replace("_", "-"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
key.strip_prefix("MDBOOK_")
|
||||
.map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
|
||||
}
|
||||
|
||||
fn is_legacy_format(table: &Value) -> bool {
|
||||
|
@ -533,14 +527,14 @@ pub struct HtmlConfig {
|
|||
/// 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
|
||||
/// to point livereloading at, if it has been enabled.
|
||||
/// Endpoint of websocket, for livereload usage. Value loaded from .toml file
|
||||
/// is ignored, because our code overrides this field with the value [`LIVE_RELOAD_ENDPOINT`]
|
||||
///
|
||||
/// [`LIVE_RELOAD_ENDPOINT`]: cmd::serve::LIVE_RELOAD_ENDPOINT
|
||||
///
|
||||
/// This config item *should not be edited* by the end user.
|
||||
#[doc(hidden)]
|
||||
pub livereload_url: Option<String>,
|
||||
pub live_reload_endpoint: Option<String>,
|
||||
/// The mapping from old pages to new pages/URLs to use when generating
|
||||
/// redirects.
|
||||
pub redirect: HashMap<String, String>,
|
||||
|
@ -569,7 +563,7 @@ impl Default for HtmlConfig {
|
|||
input_404: None,
|
||||
site_url: None,
|
||||
cname: None,
|
||||
livereload_url: None,
|
||||
live_reload_endpoint: None,
|
||||
redirect: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
@ -588,7 +582,7 @@ 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")]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Print {
|
||||
/// Whether print support is enabled.
|
||||
pub enable: bool,
|
||||
|
@ -633,6 +627,8 @@ pub struct Playground {
|
|||
/// Set's the language the playground will work with
|
||||
/// TODO: Use an array when there's support for multiple languages simultaneously
|
||||
pub language: String,
|
||||
/// Display the run button. Default: `true`
|
||||
pub runnable: bool,
|
||||
}
|
||||
|
||||
impl Default for Playground {
|
||||
|
@ -643,6 +639,7 @@ impl Default for Playground {
|
|||
copy_js: true,
|
||||
line_numbers: false,
|
||||
language: "rust".to_string(),
|
||||
runnable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -725,6 +722,7 @@ impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::fs::get_404_output_file;
|
||||
use serde_json::json;
|
||||
|
||||
const COMPLEX_CONFIG: &str = r#"
|
||||
[book]
|
||||
|
@ -787,6 +785,7 @@ mod tests {
|
|||
copy_js: true,
|
||||
line_numbers: false,
|
||||
language: "rust".to_string(),
|
||||
runnable: true,
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
|
@ -817,6 +816,22 @@ mod tests {
|
|||
assert_eq!(got.html_config().unwrap(), html_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disable_runnable() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
description = "book book book"
|
||||
authors = ["Shogo Takata"]
|
||||
|
||||
[output.html.playground]
|
||||
runnable = false
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert!(!got.html_config().unwrap().playground.runnable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edition_2015() {
|
||||
let src = r#"
|
||||
|
@ -1023,7 +1038,7 @@ mod tests {
|
|||
fn encode_env_var(key: &str) -> String {
|
||||
format!(
|
||||
"MDBOOK_{}",
|
||||
key.to_uppercase().replace('.', "__").replace("-", "_")
|
||||
key.to_uppercase().replace('.', "__").replace('-', "_")
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1047,11 +1062,10 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::approx_constant)]
|
||||
fn update_config_using_env_var_and_complex_value() {
|
||||
let mut cfg = Config::default();
|
||||
let key = "foo-bar.baz";
|
||||
let value = json!({"array": [1, 2, 3], "number": 3.14});
|
||||
let value = json!({"array": [1, 2, 3], "number": 13.37});
|
||||
let value_str = serde_json::to_string(&value).unwrap();
|
||||
|
||||
assert!(cfg.get(key).is_none());
|
||||
|
@ -1161,4 +1175,24 @@ mod tests {
|
|||
|
||||
Config::from_str(src).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_config() {
|
||||
let src = r#"
|
||||
[output.html.print]
|
||||
enable = false
|
||||
"#;
|
||||
let got = Config::from_str(src).unwrap();
|
||||
let html_config = got.html_config().unwrap();
|
||||
assert!(!html_config.print.enable);
|
||||
assert!(html_config.print.page_break);
|
||||
let src = r#"
|
||||
[output.html.print]
|
||||
page-break = false
|
||||
"#;
|
||||
let got = Config::from_str(src).unwrap();
|
||||
let html_config = got.html_config().unwrap();
|
||||
assert!(html_config.print.enable);
|
||||
assert!(!html_config.print.page_break);
|
||||
}
|
||||
}
|
||||
|
|
14
src/lib.rs
14
src/lib.rs
|
@ -82,20 +82,6 @@
|
|||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(clippy::comparison_chain)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate pretty_assertions;
|
||||
|
||||
pub mod book;
|
||||
pub mod config;
|
||||
|
|
43
src/main.rs
43
src/main.rs
|
@ -5,7 +5,8 @@ extern crate log;
|
|||
|
||||
use anyhow::anyhow;
|
||||
use chrono::Local;
|
||||
use clap::{App, AppSettings, Arg, ArgMatches, Shell, SubCommand};
|
||||
use clap::{App, AppSettings, Arg, ArgMatches};
|
||||
use clap_complete::Shell;
|
||||
use env_logger::Builder;
|
||||
use log::LevelFilter;
|
||||
use mdbook::utils;
|
||||
|
@ -25,25 +26,31 @@ fn main() {
|
|||
|
||||
// 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),
|
||||
Some(("init", sub_matches)) => cmd::init::execute(sub_matches),
|
||||
Some(("build", sub_matches)) => cmd::build::execute(sub_matches),
|
||||
Some(("clean", sub_matches)) => cmd::clean::execute(sub_matches),
|
||||
#[cfg(feature = "watch")]
|
||||
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches),
|
||||
Some(("watch", 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)) => (|| {
|
||||
Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches),
|
||||
Some(("test", sub_matches)) => cmd::test::execute(sub_matches),
|
||||
Some(("completions", 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());
|
||||
let mut complete_app = create_clap_app();
|
||||
clap_complete::generate(
|
||||
shell,
|
||||
&mut complete_app,
|
||||
"mdbook",
|
||||
&mut std::io::stdout().lock(),
|
||||
);
|
||||
Ok(())
|
||||
})(),
|
||||
(_, _) => unreachable!(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
|
@ -54,14 +61,13 @@ fn main() {
|
|||
}
|
||||
|
||||
/// Create a list of valid arguments and sub-commands
|
||||
fn create_clap_app<'a, 'b>() -> App<'a, 'b> {
|
||||
fn create_clap_app() -> App<'static> {
|
||||
let app = App::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
.version(VERSION)
|
||||
.setting(AppSettings::GlobalVersion)
|
||||
.setting(AppSettings::PropagateVersion)
|
||||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.setting(AppSettings::ColoredHelp)
|
||||
.after_help(
|
||||
"For more information about a specific command, try `mdbook <command> --help`\n\
|
||||
The source code for mdBook is available at: https://github.com/rust-lang/mdBook",
|
||||
|
@ -71,12 +77,12 @@ fn create_clap_app<'a, 'b>() -> App<'a, 'b> {
|
|||
.subcommand(cmd::test::make_subcommand())
|
||||
.subcommand(cmd::clean::make_subcommand())
|
||||
.subcommand(
|
||||
SubCommand::with_name("completions")
|
||||
App::new("completions")
|
||||
.about("Generate shell completions for your shell to stdout")
|
||||
.arg(
|
||||
Arg::with_name("shell")
|
||||
Arg::new("shell")
|
||||
.takes_value(true)
|
||||
.possible_values(&Shell::variants())
|
||||
.possible_values(Shell::possible_values())
|
||||
.help("the shell to generate completions for")
|
||||
.value_name("SHELL")
|
||||
.required(true),
|
||||
|
@ -137,3 +143,8 @@ fn open<P: AsRef<OsStr>>(path: P) {
|
|||
error!("Error opening web browser: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_app() {
|
||||
create_clap_app().debug_assert();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::Book;
|
||||
use crate::errors::*;
|
||||
use log::{debug, trace, warn};
|
||||
use shlex::Shlex;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use regex::Regex;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::errors::*;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::errors::*;
|
||||
use lazy_static::lazy_static;
|
||||
use log::warn;
|
||||
|
||||
/// A preprocessor for converting file name `README.md` to `index.md` since
|
||||
/// `README.md` is the de facto index file in markdown-based documentation.
|
||||
|
|
|
@ -10,6 +10,8 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::{Book, BookItem};
|
||||
use lazy_static::lazy_static;
|
||||
use log::{error, warn};
|
||||
|
||||
const ESCAPE_CHAR: char = '\\';
|
||||
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
|
@ -146,6 +148,7 @@ enum RangeOrAnchor {
|
|||
}
|
||||
|
||||
// A range of lines specified with some include directive.
|
||||
#[allow(clippy::enum_variant_names)] // The prefix can't be removed, and is meant to mirror the contained type
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum LineRange {
|
||||
Range(Range<usize>),
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::book::Book;
|
|||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
|
|
@ -14,7 +14,10 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use crate::utils::fs::get_404_output_file;
|
||||
use handlebars::Handlebars;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{debug, trace, warn};
|
||||
use regex::{Captures, Regex};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HtmlHandlebars;
|
||||
|
@ -116,7 +119,7 @@ impl HtmlHandlebars {
|
|||
if ctx.is_index {
|
||||
ctx.data.insert("path".to_owned(), json!("index.md"));
|
||||
ctx.data.insert("path_to_root".to_owned(), json!(""));
|
||||
ctx.data.insert("is_index".to_owned(), json!("true"));
|
||||
ctx.data.insert("is_index".to_owned(), json!(true));
|
||||
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
|
||||
let rendered_index =
|
||||
self.post_process(rendered_index, &ctx.html_config.playground, ctx.edition);
|
||||
|
@ -170,6 +173,13 @@ impl HtmlHandlebars {
|
|||
// Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
|
||||
data_404.insert("path".to_owned(), json!("404.md"));
|
||||
data_404.insert("content".to_owned(), json!(html_content_404));
|
||||
|
||||
let mut title = String::from("Page not found");
|
||||
if let Some(book_title) = &ctx.config.book.title {
|
||||
title.push_str(" - ");
|
||||
title.push_str(book_title);
|
||||
}
|
||||
data_404.insert("title".to_owned(), json!(title));
|
||||
let rendered = handlebars.render("index", &data_404)?;
|
||||
|
||||
let rendered =
|
||||
|
@ -474,7 +484,13 @@ impl Renderer for HtmlHandlebars {
|
|||
let mut handlebars = Handlebars::new();
|
||||
|
||||
let theme_dir = match html_config.theme {
|
||||
Some(ref theme) => ctx.root.join(theme),
|
||||
Some(ref theme) => {
|
||||
let dir = ctx.root.join(theme);
|
||||
if !dir.is_dir() {
|
||||
bail!("theme dir {} does not exist", dir.display());
|
||||
}
|
||||
dir
|
||||
}
|
||||
None => ctx.root.join("theme"),
|
||||
};
|
||||
|
||||
|
@ -527,7 +543,8 @@ impl Renderer for HtmlHandlebars {
|
|||
chapter_titles: &ctx.chapter_titles,
|
||||
};
|
||||
self.render_item(item, ctx, &mut print_content)?;
|
||||
is_index = false;
|
||||
// Only the first non-draft chapter item should be treated as the "index"
|
||||
is_index &= !matches!(item, BookItem::Chapter(ch) if !ch.is_draft_chapter());
|
||||
}
|
||||
|
||||
// Render 404 page
|
||||
|
@ -606,8 +623,11 @@ fn make_data(
|
|||
if theme.favicon_svg.is_some() {
|
||||
data.insert("favicon_svg".to_owned(), json!("favicon.svg"));
|
||||
}
|
||||
if let Some(ref livereload) = html_config.livereload_url {
|
||||
data.insert("livereload".to_owned(), json!(livereload));
|
||||
if let Some(ref live_reload_endpoint) = html_config.live_reload_endpoint {
|
||||
data.insert(
|
||||
"live_reload_endpoint".to_owned(),
|
||||
json!(live_reload_endpoint),
|
||||
);
|
||||
}
|
||||
|
||||
let default_theme = match html_config.default_theme {
|
||||
|
@ -747,10 +767,13 @@ fn make_data(
|
|||
/// Goes through the rendered HTML, making sure all header tags have
|
||||
/// an anchor respectively so people can link to sections directly.
|
||||
fn build_header_links(html: &str) -> String {
|
||||
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
||||
lazy_static! {
|
||||
static ref BUILD_HEADER_LINKS: Regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
||||
}
|
||||
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
regex
|
||||
BUILD_HEADER_LINKS
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let level = caps[1]
|
||||
.parse()
|
||||
|
@ -768,16 +791,7 @@ fn insert_link_into_header(
|
|||
content: &str,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
) -> String {
|
||||
let raw_id = utils::id_from_content(content);
|
||||
|
||||
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
|
||||
|
||||
let id = match *id_count {
|
||||
0 => raw_id,
|
||||
other => format!("{}-{}", raw_id, other),
|
||||
};
|
||||
|
||||
*id_count += 1;
|
||||
let id = utils::unique_id_from_content(content, id_counter);
|
||||
|
||||
format!(
|
||||
r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##,
|
||||
|
@ -796,11 +810,15 @@ fn insert_link_into_header(
|
|||
// ```
|
||||
// This function replaces all commas by spaces in the code block classes
|
||||
fn fix_code_blocks(html: &str) -> String {
|
||||
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
||||
regex
|
||||
lazy_static! {
|
||||
static ref FIX_CODE_BLOCKS: Regex =
|
||||
Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
||||
}
|
||||
|
||||
FIX_CODE_BLOCKS
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let before = &caps[1];
|
||||
let classes = &caps[2].replace(",", " ");
|
||||
let classes = &caps[2].replace(',', " ");
|
||||
let after = &caps[3];
|
||||
|
||||
format!(
|
||||
|
@ -818,8 +836,11 @@ fn add_playground_pre(
|
|||
playground_config: &Playground,
|
||||
edition: Option<RustEdition>,
|
||||
) -> String {
|
||||
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
regex
|
||||
lazy_static! {
|
||||
static ref ADD_PLAYGROUND_PRE: Regex =
|
||||
Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
}
|
||||
ADD_PLAYGROUND_PRE
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
|
@ -853,7 +874,8 @@ fn add_playground_pre_rust(
|
|||
) -> String {
|
||||
if (!classes.contains("ignore")
|
||||
&& !classes.contains("noplayground")
|
||||
&& !classes.contains("noplaypen"))
|
||||
&& !classes.contains("noplaypen")
|
||||
&& playground_config.runnable)
|
||||
|| classes.contains("mdbook-runnable")
|
||||
{
|
||||
let contains_e2015 = classes.contains("edition2015");
|
||||
|
@ -870,12 +892,12 @@ fn add_playground_pre_rust(
|
|||
None => "",
|
||||
}
|
||||
};
|
||||
let all_classes = format!("{}{}", classes, edition_class);
|
||||
|
||||
// wrap the contents in an external pre block
|
||||
format!(
|
||||
"<pre class=\"playground\"><code class=\"{}\">{}</code></pre>",
|
||||
all_classes,
|
||||
"<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
|
||||
classes,
|
||||
edition_class,
|
||||
{
|
||||
let content: Cow<'_, str> = if playground_config.editable
|
||||
&& classes.contains("editable")
|
||||
|
@ -887,7 +909,7 @@ fn add_playground_pre_rust(
|
|||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
|
||||
format!("\n# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code).into()
|
||||
format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code).into()
|
||||
};
|
||||
hide_lines(&content)
|
||||
}
|
||||
|
@ -902,14 +924,21 @@ lazy_static! {
|
|||
}
|
||||
|
||||
fn hide_lines(content: &str) -> String {
|
||||
lazy_static! {
|
||||
static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap();
|
||||
}
|
||||
|
||||
let mut result = String::with_capacity(content.len());
|
||||
for line in content.lines() {
|
||||
let mut lines = content.lines().peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
// Don't include newline on the last line.
|
||||
let newline = if lines.peek().is_none() { "" } else { "\n" };
|
||||
if let Some(caps) = BORING_LINES_REGEX.captures(line) {
|
||||
if &caps[2] == "#" {
|
||||
result += &caps[1];
|
||||
result += &caps[2];
|
||||
result += &caps[3];
|
||||
result += "\n";
|
||||
result += newline;
|
||||
continue;
|
||||
} else if &caps[2] != "!" && &caps[2] != "[" {
|
||||
result += "<span class=\"boring\">";
|
||||
|
@ -918,13 +947,13 @@ fn hide_lines(content: &str) -> String {
|
|||
result += &caps[2];
|
||||
}
|
||||
result += &caps[3];
|
||||
result += "\n";
|
||||
result += newline;
|
||||
result += "</span>";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result += line;
|
||||
result += "\n";
|
||||
result += newline;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
@ -1004,19 +1033,19 @@ mod tests {
|
|||
fn add_playground() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";</code></pre>"),
|
||||
("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
|
||||
"<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code>"),
|
||||
"<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code>"),
|
||||
("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
|
@ -1034,13 +1063,13 @@ mod tests {
|
|||
fn add_playground_edition2015() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
|
@ -1058,13 +1087,13 @@ mod tests {
|
|||
fn add_playground_edition2018() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
|
@ -1082,13 +1111,13 @@ mod tests {
|
|||
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>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
|
|
|
@ -4,6 +4,8 @@ use std::path::Path;
|
|||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
|
||||
|
||||
use crate::utils;
|
||||
use log::{debug, trace};
|
||||
use serde_json::json;
|
||||
|
||||
type StringMap = BTreeMap<String, String>;
|
||||
|
||||
|
@ -61,7 +63,7 @@ fn find_chapter(
|
|||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
.replace('\"', "");
|
||||
|
||||
if !rc.evaluate(ctx, "@root/is_index")?.is_missing() {
|
||||
// Special case for index.md which may be a synthetic page.
|
||||
|
@ -121,7 +123,7 @@ fn render(
|
|||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
.replace('\"', "");
|
||||
|
||||
context.insert(
|
||||
"path_to_root".to_owned(),
|
||||
|
@ -141,20 +143,17 @@ fn render(
|
|||
.with_extension("html")
|
||||
.to_str()
|
||||
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
|
||||
.map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/"))))
|
||||
.map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/"))))
|
||||
})?;
|
||||
|
||||
trace!("Render template");
|
||||
|
||||
_h.template()
|
||||
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
||||
.and_then(|t| {
|
||||
let mut local_rc = rc.clone();
|
||||
let local_ctx = Context::wraps(&context)?;
|
||||
t.render(r, &local_ctx, &mut local_rc, out)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
let t = _h
|
||||
.template()
|
||||
.ok_or_else(|| RenderError::new("Error with the handlebars template"))?;
|
||||
let local_ctx = Context::wraps(&context)?;
|
||||
let mut local_rc = rc.clone();
|
||||
t.render(r, &local_ctx, &mut local_rc, out)
|
||||
}
|
||||
|
||||
pub fn previous(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
|
||||
use log::trace;
|
||||
|
||||
pub fn theme_option(
|
||||
h: &Helper<'_, '_>,
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::{cmp::Ordering, collections::BTreeMap};
|
||||
|
||||
use crate::utils;
|
||||
use crate::utils::bracket_escape;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
|
||||
use pulldown_cmark::{html, Event, Parser};
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
|
@ -34,7 +33,7 @@ impl HelperDef for RenderToc {
|
|||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
.replace('\"', "");
|
||||
|
||||
let current_section = rc
|
||||
.evaluate(ctx, "@root/section")?
|
||||
|
@ -58,6 +57,11 @@ impl HelperDef for RenderToc {
|
|||
out.write("<ol class=\"chapter\">")?;
|
||||
|
||||
let mut current_level = 1;
|
||||
// The "index" page, which has this attribute set, is supposed to alias the first chapter in
|
||||
// the book, i.e. the first link. There seems to be no easy way to determine which chapter
|
||||
// the "index" is aliasing from within the renderer, so this is used instead to force the
|
||||
// first link to be active. See further below.
|
||||
let mut is_first_chapter = ctx.data().get("is_index").is_some();
|
||||
|
||||
for item in chapters {
|
||||
// Spacer
|
||||
|
@ -82,61 +86,66 @@ impl HelperDef for RenderToc {
|
|||
level - 1 < fold_level as usize
|
||||
};
|
||||
|
||||
if level > current_level {
|
||||
while level > current_level {
|
||||
out.write("<li>")?;
|
||||
out.write("<ol class=\"section\">")?;
|
||||
current_level += 1;
|
||||
match level.cmp(¤t_level) {
|
||||
Ordering::Greater => {
|
||||
while level > current_level {
|
||||
out.write("<li>")?;
|
||||
out.write("<ol class=\"section\">")?;
|
||||
current_level += 1;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
} else if level < current_level {
|
||||
while level < current_level {
|
||||
out.write("</ol>")?;
|
||||
out.write("</li>")?;
|
||||
current_level -= 1;
|
||||
Ordering::Less => {
|
||||
while level < current_level {
|
||||
out.write("</ol>")?;
|
||||
out.write("</li>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
} else {
|
||||
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
|
||||
}
|
||||
|
||||
// Part title
|
||||
if let Some(title) = item.get("part") {
|
||||
out.write("<li class=\"part-title\">")?;
|
||||
write_escaped(out, title)?;
|
||||
out.write(&bracket_escape(title))?;
|
||||
out.write("</li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Link
|
||||
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 path_exists: bool;
|
||||
match item.get("path") {
|
||||
Some(path) if !path.is_empty() => {
|
||||
out.write("<a href=\"")?;
|
||||
let tmp = Path::new(path)
|
||||
.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 || is_first_chapter {
|
||||
is_first_chapter = false;
|
||||
out.write(" class=\"active\"")?;
|
||||
}
|
||||
|
||||
if path == ¤t_path {
|
||||
out.write(" class=\"active\"")?;
|
||||
out.write(">")?;
|
||||
path_exists = true;
|
||||
}
|
||||
|
||||
out.write(">")?;
|
||||
true
|
||||
} else {
|
||||
out.write("<div>")?;
|
||||
false
|
||||
};
|
||||
_ => {
|
||||
out.write("<div>")?;
|
||||
path_exists = false;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.no_section_label {
|
||||
// Section does not necessarily exist
|
||||
|
@ -148,20 +157,7 @@ impl HelperDef for RenderToc {
|
|||
}
|
||||
|
||||
if let Some(name) = item.get("name") {
|
||||
// Render only inline code blocks
|
||||
|
||||
// filter all events that are not inline code blocks
|
||||
let parser = Parser::new(name).filter(|event| match *event {
|
||||
Event::Code(_) | Event::Html(_) | Event::Text(_) => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// render markdown to html
|
||||
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
|
||||
html::push_html(&mut markdown_parsed_name, parser);
|
||||
|
||||
// write to the handlebars template
|
||||
write_escaped(out, &markdown_parsed_name)?;
|
||||
out.write(&bracket_escape(name))?
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
|
@ -205,18 +201,3 @@ 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(())
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::borrow::Cow;
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use elasticlunr::Index;
|
||||
use elasticlunr::{Index, IndexBuilder};
|
||||
use pulldown_cmark::*;
|
||||
|
||||
use crate::book::{Book, BookItem};
|
||||
|
@ -10,10 +10,29 @@ use crate::config::Search;
|
|||
use crate::errors::*;
|
||||
use crate::theme::searcher;
|
||||
use crate::utils;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{debug, warn};
|
||||
use serde::Serialize;
|
||||
|
||||
const MAX_WORD_LENGTH_TO_INDEX: usize = 80;
|
||||
|
||||
/// Tokenizes in the same way as elasticlunr-rs (for English), but also drops long tokens.
|
||||
fn tokenize(text: &str) -> Vec<String> {
|
||||
text.split(|c: char| c.is_whitespace() || c == '-')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.trim().to_lowercase())
|
||||
.filter(|s| s.len() <= MAX_WORD_LENGTH_TO_INDEX)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Creates all files required for search.
|
||||
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
|
||||
let mut index = Index::new(&["title", "body", "breadcrumbs"]);
|
||||
let mut index = IndexBuilder::new()
|
||||
.add_field_with_tokenizer("title", Box::new(&tokenize))
|
||||
.add_field_with_tokenizer("body", Box::new(&tokenize))
|
||||
.add_field_with_tokenizer("breadcrumbs", Box::new(&tokenize))
|
||||
.build();
|
||||
|
||||
let mut doc_urls = Vec::with_capacity(book.sections.len());
|
||||
|
||||
for item in book.iter() {
|
||||
|
@ -97,6 +116,7 @@ fn render_item(
|
|||
|
||||
breadcrumbs.push(chapter.name.clone());
|
||||
|
||||
let mut id_counter = HashMap::new();
|
||||
while let Some(event) = p.next() {
|
||||
match event {
|
||||
Event::Start(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
|
||||
|
@ -120,7 +140,7 @@ fn render_item(
|
|||
}
|
||||
Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
|
||||
in_heading = false;
|
||||
section_id = Some(utils::id_from_content(&heading));
|
||||
section_id = Some(utils::unique_id_from_content(&heading, &mut id_counter));
|
||||
breadcrumbs.push(heading.clone());
|
||||
}
|
||||
Event::Start(Tag::FootnoteDefinition(name)) => {
|
||||
|
@ -208,12 +228,13 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) ->
|
|||
|
||||
let mut fields = BTreeMap::new();
|
||||
let mut opt = SearchOptionsField::default();
|
||||
opt.boost = Some(search_config.boost_title);
|
||||
fields.insert("title".into(), opt);
|
||||
opt.boost = Some(search_config.boost_paragraph);
|
||||
fields.insert("body".into(), opt);
|
||||
opt.boost = Some(search_config.boost_hierarchy);
|
||||
fields.insert("breadcrumbs".into(), opt);
|
||||
let mut insert_boost = |key: &str, boost| {
|
||||
opt.boost = Some(boost);
|
||||
fields.insert(key.into(), opt);
|
||||
};
|
||||
insert_boost("title", search_config.boost_title);
|
||||
insert_boost("body", search_config.boost_paragraph);
|
||||
insert_boost("breadcrumbs", search_config.boost_hierarchy);
|
||||
|
||||
let search_options = SearchOptions {
|
||||
bool: if search_config.use_boolean_and {
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::book::BookItem;
|
|||
use crate::errors::*;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::utils;
|
||||
|
||||
use log::trace;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
|
@ -27,8 +27,11 @@ use std::process::{Command, Stdio};
|
|||
use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use log::{error, info, trace, warn};
|
||||
use toml::Value;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An arbitrary `mdbook` backend.
|
||||
///
|
||||
/// Although it's quite possible for you to import `mdbook` as a library and
|
||||
|
|
|
@ -8,7 +8,6 @@ Original by Dempfi (https://github.com/dempfi/ayu)
|
|||
overflow-x: auto;
|
||||
background: #191f26;
|
||||
color: #e6e1cf;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
|
|
|
@ -208,24 +208,63 @@ pre {
|
|||
pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
right: 0px;
|
||||
top: 2px;
|
||||
margin: 0px;
|
||||
padding: 2px 0px;
|
||||
|
||||
color: var(--sidebar-fg);
|
||||
cursor: pointer;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: visibility 0.1s linear, opacity 0.1s linear;
|
||||
}
|
||||
pre:hover > .buttons {
|
||||
visibility: visible;
|
||||
opacity: 1
|
||||
}
|
||||
pre > .buttons :hover {
|
||||
color: var(--sidebar-active);
|
||||
border-color: var(--icons-hover);
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
pre > .buttons i {
|
||||
margin-left: 8px;
|
||||
}
|
||||
pre > .buttons button {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: inherit;
|
||||
margin: 0px 5px;
|
||||
padding: 3px 5px;
|
||||
font-size: 14px;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
border-color: var(--icons);
|
||||
background-color: var(--theme-popup-bg);
|
||||
transition: 100ms;
|
||||
transition-property: color,border-color,background-color;
|
||||
color: var(--icons);
|
||||
}
|
||||
@media (pointer: coarse) {
|
||||
pre > .buttons button {
|
||||
/* On mobile, make it easier to tap buttons. */
|
||||
padding: 0.3rem 1rem;
|
||||
}
|
||||
}
|
||||
pre > code {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* FIXME: ACE editors overlap their buttons because ACE does absolute
|
||||
positioning within the code block which breaks padding. The only solution I
|
||||
can think of is to move the padding to the outer pre tag (or insert a div
|
||||
wrapper), but that would require fixing a whole bunch of CSS rules.
|
||||
*/
|
||||
.hljs.ace_editor {
|
||||
padding: 0rem 0rem;
|
||||
}
|
||||
|
||||
pre > .result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,16 @@ code {
|
|||
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
|
||||
}
|
||||
|
||||
/* make long words/inline code not x overflow */
|
||||
main {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* make wide tables scroll if they overflow */
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Don't change font size in headers. */
|
||||
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
||||
font-size: unset;
|
||||
|
@ -80,8 +90,7 @@ h6:target::before {
|
|||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
padding: 0 15px;
|
||||
padding-bottom: 50px;
|
||||
padding: 0 5px 50px 5px;
|
||||
}
|
||||
.content main {
|
||||
margin-left: auto;
|
||||
|
|
|
@ -61,7 +61,6 @@
|
|||
overflow-x: auto;
|
||||
background: #f6f7f6;
|
||||
color: #000;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
|
|
|
@ -51,18 +51,18 @@
|
|||
|
||||
{{#if mathjax_support}}
|
||||
<!-- MathJax -->
|
||||
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{{/if}}
|
||||
</head>
|
||||
<body>
|
||||
<!-- Provide site root to javascript -->
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var path_to_root = "{{ path_to_root }}";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
|
||||
</script>
|
||||
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
try {
|
||||
var theme = localStorage.getItem('mdbook-theme');
|
||||
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||
|
@ -78,7 +78,7 @@
|
|||
</script>
|
||||
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||
|
@ -90,7 +90,7 @@
|
|||
</script>
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var html = document.querySelector('html');
|
||||
var sidebar = 'hidden';
|
||||
if (document.body.clientWidth >= 1080) {
|
||||
|
@ -171,7 +171,7 @@
|
|||
{{/if}}
|
||||
|
||||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||
|
@ -219,10 +219,12 @@
|
|||
|
||||
</div>
|
||||
|
||||
{{#if livereload}}
|
||||
{{#if live_reload_endpoint}}
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
<script type="text/javascript">
|
||||
var socket = new WebSocket("{{{livereload}}}");
|
||||
<script>
|
||||
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
|
||||
const socket = new WebSocket(wsAddress);
|
||||
socket.onmessage = function (event) {
|
||||
if (event.data === "reload") {
|
||||
socket.close();
|
||||
|
@ -238,7 +240,7 @@
|
|||
|
||||
{{#if google_analytics}}
|
||||
<!-- Google Analytics Tag -->
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var localAddrs = ["localhost", "127.0.0.1", ""];
|
||||
|
||||
// make sure we don't activate google analytics if the developer is
|
||||
|
@ -256,43 +258,43 @@
|
|||
{{/if}}
|
||||
|
||||
{{#if playground_line_numbers}}
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
window.playground_line_numbers = true;
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_copyable}}
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
window.playground_copyable = true;
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_js}}
|
||||
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}ace.js" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}editor.js" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}mode-rust.js" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}theme-dawn.js" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}theme-tomorrow_night.js" charset="utf-8"></script>
|
||||
{{/if}}
|
||||
|
||||
{{#if search_js}}
|
||||
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}elasticlunr.min.js" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}mark.min.js" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}searcher.js" charset="utf-8"></script>
|
||||
{{/if}}
|
||||
|
||||
<script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}clipboard.min.js" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}highlight.js" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}book.js" charset="utf-8"></script>
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
{{#each additional_js}}
|
||||
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script>
|
||||
<script src="{{ ../path_to_root }}{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
{{#if is_print}}
|
||||
{{#if mathjax_support}}
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
MathJax.Hub.Register.StartupHook('End', function() {
|
||||
window.setTimeout(window.print, 100);
|
||||
|
@ -300,7 +302,7 @@
|
|||
});
|
||||
</script>
|
||||
{{else}}
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
window.setTimeout(window.print, 100);
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ use std::io::Read;
|
|||
use std::path::Path;
|
||||
|
||||
use crate::errors::*;
|
||||
|
||||
use log::warn;
|
||||
pub static INDEX: &[u8] = include_bytes!("index.hbs");
|
||||
pub static HEAD: &[u8] = include_bytes!("head.hbs");
|
||||
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
|
||||
|
|
|
@ -81,8 +81,6 @@
|
|||
overflow-x: auto;
|
||||
background: #1d1f21;
|
||||
color: #c5c8c6;
|
||||
padding: 0.5em;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
.coffeescript .javascript,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::errors::*;
|
||||
use log::{debug, trace};
|
||||
use std::convert::Into;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
|
|
124
src/utils/mod.rs
124
src/utils/mod.rs
|
@ -4,11 +4,13 @@ pub mod fs;
|
|||
mod string;
|
||||
pub(crate) mod toml_ext;
|
||||
use crate::errors::Error;
|
||||
use lazy_static::lazy_static;
|
||||
use log::error;
|
||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
|
||||
use regex::Regex;
|
||||
|
||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
|
||||
|
@ -44,6 +46,8 @@ pub fn normalize_id(content: &str) -> String {
|
|||
|
||||
/// Generate an ID for use with anchors which is derived from a "normalised"
|
||||
/// string.
|
||||
// This function should be made private when the deprecation expires.
|
||||
#[deprecated(since = "0.4.16", note = "use unique_id_from_content instead")]
|
||||
pub fn id_from_content(content: &str) -> String {
|
||||
let mut content = content.to_string();
|
||||
|
||||
|
@ -59,10 +63,30 @@ pub fn id_from_content(content: &str) -> String {
|
|||
|
||||
// Remove spaces and hashes indicating a header
|
||||
let trimmed = content.trim().trim_start_matches('#').trim();
|
||||
|
||||
normalize_id(trimmed)
|
||||
}
|
||||
|
||||
/// Generate an ID for use with anchors which is derived from a "normalised"
|
||||
/// string.
|
||||
///
|
||||
/// Each ID returned will be unique, if the same `id_counter` is provided on
|
||||
/// each call.
|
||||
pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, usize>) -> String {
|
||||
let id = {
|
||||
#[allow(deprecated)]
|
||||
id_from_content(content)
|
||||
};
|
||||
|
||||
// If we have headers with the same normalized id, append an incrementing counter
|
||||
let id_count = id_counter.entry(id.clone()).or_insert(0);
|
||||
let unique_id = match *id_count {
|
||||
0 => id,
|
||||
id_count => format!("{}-{}", id, id_count),
|
||||
};
|
||||
*id_count += 1;
|
||||
unique_id
|
||||
}
|
||||
|
||||
/// Fix links to the correct location.
|
||||
///
|
||||
/// This adjusts links, such as turning `.md` extensions to `.html`.
|
||||
|
@ -177,12 +201,28 @@ pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&P
|
|||
let p = new_cmark_parser(text, curly_quotes);
|
||||
let events = p
|
||||
.map(clean_codeblock_headers)
|
||||
.map(|event| adjust_links(event, path));
|
||||
.map(|event| adjust_links(event, path))
|
||||
.flat_map(|event| {
|
||||
let (a, b) = wrap_tables(event);
|
||||
a.into_iter().chain(b)
|
||||
});
|
||||
|
||||
html::push_html(&mut s, events);
|
||||
s
|
||||
}
|
||||
|
||||
/// Wraps tables in a `.table-wrapper` class to apply overflow-x rules to.
|
||||
fn wrap_tables(event: Event<'_>) -> (Option<Event<'_>>, Option<Event<'_>>) {
|
||||
match event {
|
||||
Event::Start(Tag::Table(_)) => (
|
||||
Some(Event::Html(r#"<div class="table-wrapper">"#.into())),
|
||||
Some(event),
|
||||
),
|
||||
Event::End(Tag::Table(_)) => (Some(event), Some(Event::Html(r#"</div>"#.into()))),
|
||||
_ => (Some(event), None),
|
||||
}
|
||||
}
|
||||
|
||||
fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
|
||||
match event {
|
||||
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info))) => {
|
||||
|
@ -210,8 +250,26 @@ pub fn log_backtrace(e: &Error) {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bracket_escape(mut s: &str) -> String {
|
||||
let mut escaped = String::with_capacity(s.len());
|
||||
let needs_escape: &[char] = &['<', '>'];
|
||||
while let Some(next) = s.find(needs_escape) {
|
||||
escaped.push_str(&s[..next]);
|
||||
match s.as_bytes()[next] {
|
||||
b'<' => escaped.push_str("<"),
|
||||
b'>' => escaped.push_str(">"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
s = &s[next + 1..];
|
||||
}
|
||||
escaped.push_str(s);
|
||||
escaped
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::bracket_escape;
|
||||
|
||||
mod render_markdown {
|
||||
use super::super::render_markdown;
|
||||
|
||||
|
@ -241,6 +299,22 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_wrap_tables() {
|
||||
let src = r#"
|
||||
| Original | Punycode | Punycode + Encoding |
|
||||
|-----------------|-----------------|---------------------|
|
||||
| føø | f-5gaa | f_5gaa |
|
||||
"#;
|
||||
let out = r#"
|
||||
<div class="table-wrapper"><table><thead><tr><th>Original</th><th>Punycode</th><th>Punycode + Encoding</th></tr></thead><tbody>
|
||||
<tr><td>føø</td><td>f-5gaa</td><td>f_5gaa</td></tr>
|
||||
</tbody></table>
|
||||
</div>
|
||||
"#.trim();
|
||||
assert_eq!(render_markdown(src, false), out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_keep_quotes_straight() {
|
||||
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
|
||||
|
@ -332,8 +406,9 @@ more text with spaces
|
|||
}
|
||||
}
|
||||
|
||||
mod html_munging {
|
||||
use super::super::{id_from_content, normalize_id};
|
||||
#[allow(deprecated)]
|
||||
mod id_from_content {
|
||||
use super::super::id_from_content;
|
||||
|
||||
#[test]
|
||||
fn it_generates_anchors() {
|
||||
|
@ -361,6 +436,10 @@ more text with spaces
|
|||
);
|
||||
assert_eq!(id_from_content("## Über"), "Über");
|
||||
}
|
||||
}
|
||||
|
||||
mod html_munging {
|
||||
use super::super::{normalize_id, unique_id_from_content};
|
||||
|
||||
#[test]
|
||||
fn it_normalizes_ids() {
|
||||
|
@ -379,5 +458,38 @@ more text with spaces
|
|||
assert_eq!(normalize_id("한국어"), "한국어");
|
||||
assert_eq!(normalize_id(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_generates_unique_ids_from_content() {
|
||||
// Same id if not given shared state
|
||||
assert_eq!(
|
||||
unique_id_from_content("## 中文標題 CJK title", &mut Default::default()),
|
||||
"中文標題-cjk-title"
|
||||
);
|
||||
assert_eq!(
|
||||
unique_id_from_content("## 中文標題 CJK title", &mut Default::default()),
|
||||
"中文標題-cjk-title"
|
||||
);
|
||||
|
||||
// Different id if given shared state
|
||||
let mut id_counter = Default::default();
|
||||
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über");
|
||||
assert_eq!(
|
||||
unique_id_from_content("## 中文標題 CJK title", &mut id_counter),
|
||||
"中文標題-cjk-title"
|
||||
);
|
||||
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-1");
|
||||
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-2");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_brackets() {
|
||||
assert_eq!(bracket_escape(""), "");
|
||||
assert_eq!(bracket_escape("<"), "<");
|
||||
assert_eq!(bracket_escape(">"), ">");
|
||||
assert_eq!(bracket_escape("<>"), "<>");
|
||||
assert_eq!(bracket_escape("<test>"), "<test>");
|
||||
assert_eq!(bracket_escape("a<test>b"), "a<test>b");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use std::ops::Bound::{Excluded, Included, Unbounded};
|
||||
use std::ops::RangeBounds;
|
||||
|
@ -122,6 +123,7 @@ mod tests {
|
|||
};
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::reversed_empty_ranges)] // Intentionally checking that those are correctly handled
|
||||
fn take_lines_test() {
|
||||
let s = "Lorem\nipsum\ndolor\nsit\namet";
|
||||
assert_eq!(take_lines(s, 1..3), "ipsum\ndolor");
|
||||
|
@ -163,6 +165,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::reversed_empty_ranges)] // Intentionally checking that those are correctly handled
|
||||
fn take_rustdoc_include_lines_test() {
|
||||
let s = "Lorem\nipsum\ndolor\nsit\namet";
|
||||
assert_eq!(
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use crate::cli::cmd::mdbook_cmd;
|
||||
use crate::dummy_book::DummyBook;
|
||||
|
||||
use assert_cmd::Command;
|
||||
|
||||
#[test]
|
||||
fn mdbook_cli_dummy_book_generates_index_html() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
|
@ -9,7 +8,7 @@ fn mdbook_cli_dummy_book_generates_index_html() {
|
|||
// doesn't exist before
|
||||
assert!(!temp.path().join("book").exists());
|
||||
|
||||
let mut cmd = Command::cargo_bin("mdbook").unwrap();
|
||||
let mut cmd = mdbook_cmd();
|
||||
cmd.arg("build").current_dir(temp.path());
|
||||
cmd.assert()
|
||||
.success()
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
use assert_cmd::Command;
|
||||
|
||||
pub(crate) fn mdbook_cmd() -> Command {
|
||||
let mut cmd = Command::cargo_bin("mdbook").unwrap();
|
||||
cmd.env_remove("RUST_LOG");
|
||||
cmd
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
mod build;
|
||||
mod cmd;
|
||||
mod test;
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
use crate::cli::cmd::mdbook_cmd;
|
||||
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();
|
||||
let mut cmd = mdbook_cmd();
|
||||
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##"Testing chapter [^:]*: "README.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "intro.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]index.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]nested.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"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());
|
||||
}
|
||||
|
||||
|
@ -22,13 +22,13 @@ fn mdbook_cli_can_correctly_test_a_passing_book() {
|
|||
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();
|
||||
let mut cmd = mdbook_cmd();
|
||||
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##"Testing chapter [^:]*: "README.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "intro.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]index.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]nested.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"returned an error:\n\n"##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap());
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# Summary
|
||||
|
||||
---
|
||||
|
||||
- [None of these should be treated as the "index chapter"]()
|
||||
|
||||
# Part 1
|
||||
|
||||
- [Not this either]()
|
||||
- [Chapter 1](./chapter_1.md)
|
||||
- [And not this]()
|
|
@ -0,0 +1 @@
|
|||
# Chapter 1
|
|
@ -13,6 +13,7 @@
|
|||
- [Markdown](first/markdown.md)
|
||||
- [Unicode](first/unicode.md)
|
||||
- [No Headers](first/no-headers.md)
|
||||
- [Duplicate Headers](first/duplicate-headers.md)
|
||||
- [Second Chapter](second.md)
|
||||
- [Nested Chapter](second/nested.md)
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# Duplicate headers
|
||||
|
||||
This page validates behaviour of duplicate headers.
|
||||
|
||||
# Header Text
|
||||
|
||||
# Header Text
|
||||
|
||||
# header-text
|
|
@ -1,3 +1,5 @@
|
|||
Capybara capybara capybara.
|
||||
|
||||
Capybara capybara capybara.
|
||||
|
||||
ThisLongWordIsIncludedSoWeCanCheckThatSufficientlyLongWordsAreOmittedFromTheSearchIndex.
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# Summary formatting tests
|
||||
|
||||
- [*Italic* `code` \*escape\* \`escape2\`](formatted-summary.md)
|
||||
- [Soft
|
||||
line break](soft.md)
|
||||
- [\<escaped tag\>](escaped-tag.md)
|
|
@ -17,6 +17,7 @@ use std::ffi::OsStr;
|
|||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
|
@ -35,6 +36,7 @@ const TOC_SECOND_LEVEL: &[&str] = &[
|
|||
"1.4. Markdown",
|
||||
"1.5. Unicode",
|
||||
"1.6. No Headers",
|
||||
"1.7. Duplicate Headers",
|
||||
"2.1. Nested Chapter",
|
||||
];
|
||||
|
||||
|
@ -150,6 +152,25 @@ fn rendered_code_has_playground_stuff() {
|
|||
assert_contains_strings(book_js, &[".playground"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rendered_code_does_not_have_playground_stuff_in_html_when_disabled_in_config() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let config = Config::from_str(
|
||||
"
|
||||
[output.html.playground]
|
||||
runnable = false
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
let md = MDBook::load_with_config(temp.path(), config).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let nested = temp.path().join("book/first/nested.html");
|
||||
let playground_class = vec![r#"class="playground""#];
|
||||
|
||||
assert_doesnt_contain_strings(nested, &playground_class);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchors_include_text_between_but_not_anchor_comments() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
|
@ -446,6 +467,21 @@ fn by_default_mdbook_use_index_preprocessor_to_convert_readme_to_index() {
|
|||
assert_doesnt_contain_strings(&second_index, &unexpected_strings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_chapter_is_copied_as_index_even_if_not_first_elem() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut cfg = Config::default();
|
||||
cfg.set("book.src", "index_html_test")
|
||||
.expect("Couldn't set config.book.src to \"index_html_test\"");
|
||||
let md = MDBook::load_with_config(temp.path(), cfg).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let root = temp.path().join("book");
|
||||
let chapter = fs::read_to_string(root.join("chapter_1.html")).expect("read chapter 1");
|
||||
let index = fs::read_to_string(root.join("index.html")).expect("read index");
|
||||
pretty_assertions::assert_eq!(chapter, index);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_dir_overrides_work_correctly() {
|
||||
let book_dir = dummy_book::new_copy_of_example_book().unwrap();
|
||||
|
@ -600,6 +636,93 @@ fn remove_absolute_components(path: &Path) -> impl Iterator<Item = Component> +
|
|||
})
|
||||
}
|
||||
|
||||
/// Checks formatting of summary names with inline elements.
|
||||
#[test]
|
||||
fn summary_with_markdown_formatting() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut cfg = Config::default();
|
||||
cfg.set("book.src", "summary-formatting").unwrap();
|
||||
let md = MDBook::load_with_config(temp.path(), cfg).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let rendered_path = temp.path().join("book/formatted-summary.html");
|
||||
assert_contains_strings(
|
||||
rendered_path,
|
||||
&[
|
||||
r#"<a href="formatted-summary.html" class="active"><strong aria-hidden="true">1.</strong> Italic code *escape* `escape2`</a>"#,
|
||||
r#"<a href="soft.html"><strong aria-hidden="true">2.</strong> Soft line break</a>"#,
|
||||
r#"<a href="escaped-tag.html"><strong aria-hidden="true">3.</strong> <escaped tag></a>"#,
|
||||
],
|
||||
);
|
||||
|
||||
let generated_md = temp.path().join("summary-formatting/formatted-summary.md");
|
||||
assert_eq!(
|
||||
fs::read_to_string(generated_md).unwrap(),
|
||||
"# Italic code *escape* `escape2`\n"
|
||||
);
|
||||
let generated_md = temp.path().join("summary-formatting/soft.md");
|
||||
assert_eq!(
|
||||
fs::read_to_string(generated_md).unwrap(),
|
||||
"# Soft line break\n"
|
||||
);
|
||||
let generated_md = temp.path().join("summary-formatting/escaped-tag.md");
|
||||
assert_eq!(
|
||||
fs::read_to_string(generated_md).unwrap(),
|
||||
"# <escaped tag>\n"
|
||||
);
|
||||
}
|
||||
|
||||
/// Ensure building fails if `[output.html].theme` points to a non-existent directory
|
||||
#[test]
|
||||
fn failure_on_missing_theme_directory() {
|
||||
// 1. Using default theme should work
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let book_toml = r#"
|
||||
[book]
|
||||
title = "implicit"
|
||||
src = "src"
|
||||
"#;
|
||||
|
||||
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
let got = md.build();
|
||||
assert!(got.is_ok());
|
||||
|
||||
// 2. Pointing to a normal directory should work
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let created = fs::create_dir(temp.path().join("theme-directory"));
|
||||
assert!(created.is_ok());
|
||||
let book_toml = r#"
|
||||
[book]
|
||||
title = "implicit"
|
||||
src = "src"
|
||||
|
||||
[output.html]
|
||||
theme = "./theme-directory"
|
||||
"#;
|
||||
|
||||
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
let got = md.build();
|
||||
assert!(got.is_ok());
|
||||
|
||||
// 3. Pointing to a non-existent directory should fail
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let book_toml = r#"
|
||||
[book]
|
||||
title = "implicit"
|
||||
src = "src"
|
||||
|
||||
[output.html]
|
||||
theme = "./non-existent-directory"
|
||||
"#;
|
||||
|
||||
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
let got = md.build();
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[cfg(feature = "search")]
|
||||
mod search {
|
||||
use crate::dummy_book::DummyBook;
|
||||
|
@ -633,11 +756,12 @@ mod search {
|
|||
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 duplicate_headers_1 = get_doc_ref("first/duplicate-headers.html#header-text-1");
|
||||
let conclusion = get_doc_ref("conclusion.html#conclusion");
|
||||
|
||||
let bodyidx = &index["index"]["index"]["body"]["root"];
|
||||
let textidx = &bodyidx["t"]["e"]["x"]["t"];
|
||||
assert_eq!(textidx["df"], 2);
|
||||
assert_eq!(textidx["df"], 5);
|
||||
assert_eq!(textidx["docs"][&first_chapter]["tf"], 1.0);
|
||||
assert_eq!(textidx["docs"][&introduction]["tf"], 1.0);
|
||||
|
||||
|
@ -646,7 +770,7 @@ mod search {
|
|||
assert_eq!(docs[&some_section]["body"], "");
|
||||
assert_eq!(
|
||||
docs[&summary]["body"],
|
||||
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Second Chapter Nested Chapter Conclusion"
|
||||
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Duplicate Headers Second Chapter Nested Chapter Conclusion"
|
||||
);
|
||||
assert_eq!(
|
||||
docs[&summary]["breadcrumbs"],
|
||||
|
@ -657,9 +781,13 @@ mod search {
|
|||
docs[&no_headers]["breadcrumbs"],
|
||||
"First Chapter » No Headers"
|
||||
);
|
||||
assert_eq!(
|
||||
docs[&duplicate_headers_1]["breadcrumbs"],
|
||||
"First Chapter » Duplicate Headers » Header Text"
|
||||
);
|
||||
assert_eq!(
|
||||
docs[&no_headers]["body"],
|
||||
"Capybara capybara capybara. Capybara capybara capybara."
|
||||
"Capybara capybara capybara. Capybara capybara capybara. ThisLongWordIsIncludedSoWeCanCheckThatSufficientlyLongWordsAreOmittedFromTheSearchIndex."
|
||||
);
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -24,3 +24,24 @@ fn mdbook_detects_book_with_failing_tests() {
|
|||
|
||||
assert!(md.test(vec![]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mdbook_test_chapter() {
|
||||
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
|
||||
let result = md.test_chapter(vec![], Some("Introduction"));
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"test_chapter failed with {}",
|
||||
result.err().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mdbook_test_chapter_not_found() {
|
||||
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
|
||||
assert!(md.test_chapter(vec![], Some("Bogus Chapter Name")).is_err());
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue