This commit is contained in:
Vance Palacio 2022-09-19 09:24:25 -07:00
commit fd3eadb84d
59 changed files with 1571 additions and 644 deletions

View File

@ -31,7 +31,8 @@ jobs:
rust: stable rust: stable
- build: msrv - build: msrv
os: ubuntu-latest os: ubuntu-latest
rust: 1.46.0 # sync MSRV with docs: guide/src/guide/installation.md
rust: 1.56.0
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Install Rust - name: Install Rust

View File

@ -1,5 +1,91 @@
# Changelog # 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 ## mdBook 0.4.15
[5eb7d46...68a5c09](https://github.com/rust-lang/mdBook/compare/5eb7d46...68a5c09) [5eb7d46...68a5c09](https://github.com/rust-lang/mdBook/compare/5eb7d46...68a5c09)

217
Cargo.lock generated
View File

@ -28,9 +28,9 @@ dependencies = [
[[package]] [[package]]
name = "ansi_term" name = "ansi_term"
version = "0.11.0" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
@ -185,17 +185,27 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "2.33.3" version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" checksum = "7a30c3bf9ff12dfe5dae53f0a96e0febcd18420d1c0e7fad77796d9d5c4b5375"
dependencies = [ dependencies = [
"ansi_term",
"atty", "atty",
"bitflags", "bitflags",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim", "strsim",
"termcolor",
"textwrap", "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]] [[package]]
@ -218,10 +228,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "difference" name = "diff"
version = "2.0.0" version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
[[package]] [[package]]
name = "difflib" name = "difflib"
@ -261,24 +271,21 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]] [[package]]
name = "elasticlunr-rs" name = "elasticlunr-rs"
version = "2.3.13" version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "515a402b5acb08002194dd926065be7733003bb37ac0f030dfd39160028238e1" checksum = "e6dae5cac90640734ee881bc5f21b6e5123f4e5235e52428db114abffc2391d6"
dependencies = [ dependencies = [
"lazy_static",
"regex", "regex",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"strum",
"strum_macros",
] ]
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.7.1" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
dependencies = [ dependencies = [
"atty", "atty",
"humantime", "humantime",
@ -375,25 +382,11 @@ dependencies = [
"new_debug_unreachable", "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]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.16" version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@ -401,15 +394,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.16" version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
[[package]]
name = "futures-io"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582"
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
@ -426,9 +413,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.16" version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
@ -538,7 +525,7 @@ dependencies = [
"log", "log",
"pest", "pest",
"pest_derive", "pest_derive",
"quick-error 2.0.1", "quick-error",
"serde", "serde",
"serde_json", "serde_json",
] ]
@ -574,15 +561,6 @@ dependencies = [
"http", "http",
] ]
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.19" version = "0.1.19"
@ -642,12 +620,9 @@ checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
[[package]] [[package]]
name = "humantime" name = "humantime"
version = "1.3.0" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
dependencies = [
"quick-error 1.2.3",
]
[[package]] [[package]]
name = "hyper" name = "hyper"
@ -714,15 +689,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "input_buffer"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413"
dependencies = [
"bytes",
]
[[package]] [[package]]
name = "iovec" name = "iovec"
version = "0.1.4" version = "0.1.4"
@ -830,13 +796,14 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "mdbook" name = "mdbook"
version = "0.4.15" version = "0.4.21"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"anyhow", "anyhow",
"assert_cmd", "assert_cmd",
"chrono", "chrono",
"clap", "clap",
"clap_complete",
"elasticlunr-rs", "elasticlunr-rs",
"env_logger", "env_logger",
"futures-util", "futures-util",
@ -854,7 +821,6 @@ dependencies = [
"select", "select",
"semver", "semver",
"serde", "serde",
"serde_derive",
"serde_json", "serde_json",
"shlex", "shlex",
"tempfile", "tempfile",
@ -1053,6 +1019,15 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "output_vt100" name = "output_vt100"
version = "0.1.2" version = "0.1.2"
@ -1225,13 +1200,13 @@ dependencies = [
[[package]] [[package]]
name = "pretty_assertions" name = "pretty_assertions"
version = "0.6.1" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" checksum = "c89f989ac94207d048d92db058e4f6ec7342b0971fc58d1271ca148b799b3563"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"ctor", "ctor",
"difference", "diff",
"output_vt100", "output_vt100",
] ]
@ -1267,12 +1242,6 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]] [[package]]
name = "quick-error" name = "quick-error"
version = "2.0.1" version = "2.0.1"
@ -1463,6 +1432,9 @@ name = "serde"
version = "1.0.129" version = "1.0.129"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1f72836d2aa753853178eda473a3b9d8e4eefdaf20523b919677e6de489f8f1" checksum = "d1f72836d2aa753853178eda473a3b9d8e4eefdaf20523b919677e6de489f8f1"
dependencies = [
"serde_derive",
]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
@ -1578,27 +1550,9 @@ dependencies = [
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[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",
]
[[package]] [[package]]
name = "syn" name = "syn"
@ -1647,11 +1601,28 @@ dependencies = [
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.11.0" version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ 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]] [[package]]
@ -1681,11 +1652,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.10.0" version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a"
dependencies = [ dependencies = [
"autocfg",
"bytes", "bytes",
"libc", "libc",
"memchr", "memchr",
@ -1698,9 +1668,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "1.3.0" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1720,9 +1690,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.13.0" version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1a5f475f1b9d077ea1017ecbc60890fda8e54942d680ca0b1d2b47cfa2d861b" checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
@ -1801,19 +1771,19 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.12.0" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24" checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5"
dependencies = [ dependencies = [
"base64", "base64",
"byteorder", "byteorder",
"bytes", "bytes",
"http", "http",
"httparse", "httparse",
"input_buffer",
"log", "log",
"rand 0.8.4", "rand 0.8.4",
"sha-1 0.9.7", "sha-1 0.9.7",
"thiserror",
"url", "url",
"utf-8", "utf-8",
] ]
@ -1854,18 +1824,6 @@ dependencies = [
"tinyvec", "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]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.2" version = "0.2.2"
@ -1890,12 +1848,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.3" version = "0.9.3"
@ -1934,12 +1886,13 @@ dependencies = [
[[package]] [[package]]
name = "warp" name = "warp"
version = "0.3.1" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "332d47745e9a0c38636dbd454729b147d16bd1ed08ae67b3ab281c4506771054" checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures", "futures-channel",
"futures-util",
"headers", "headers",
"http", "http",
"hyper", "hyper",

View File

@ -1,13 +1,13 @@
[package] [package]
name = "mdbook" name = "mdbook"
version = "0.4.15" version = "0.4.21"
authors = [ authors = [
"Mathieu David <mathieudavid@mathieudavid.org>", "Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>", "Michael-F-Bryan <michaelfbryan@gmail.com>",
"Matt Ickstadt <mattico8@gmail.com>" "Matt Ickstadt <mattico8@gmail.com>"
] ]
documentation = "http://rust-lang.github.io/mdBook/index.html" documentation = "http://rust-lang.github.io/mdBook/index.html"
edition = "2018" edition = "2021"
exclude = ["/guide/*"] exclude = ["/guide/*"]
keywords = ["book", "gitbook", "rustbook", "markdown"] keywords = ["book", "gitbook", "rustbook", "markdown"]
license = "MPL-2.0" license = "MPL-2.0"
@ -18,8 +18,9 @@ description = "Creates a book from markdown files"
[dependencies] [dependencies]
anyhow = "1.0.28" anyhow = "1.0.28"
chrono = "0.4" chrono = "0.4"
clap = "2.24" clap = { version = "3.0", features = ["cargo"] }
env_logger = "0.7.1" clap_complete = "3.0"
env_logger = "0.9.0"
handlebars = "4.0" handlebars = "4.0"
lazy_static = "1.0" lazy_static = "1.0"
log = "0.4" log = "0.4"
@ -27,8 +28,7 @@ memchr = "2.0"
opener = "0.5" opener = "0.5"
pulldown-cmark = { version = "0.9.1", default-features = false } pulldown-cmark = { version = "0.9.1", default-features = false }
regex = "1.5.5" regex = "1.5.5"
serde = "1.0" serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
shlex = "1" shlex = "1"
tempfile = "3.0" tempfile = "3.0"
@ -42,10 +42,10 @@ gitignore = { version = "1.0", optional = true }
# Serve feature # Serve feature
futures-util = { version = "0.3.4", optional = true } futures-util = { version = "0.3.4", optional = true }
tokio = { version = "1", features = ["macros", "rt-multi-thread"], 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 # 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 } ammonia = { version = "3", optional = true }
[dev-dependencies] [dev-dependencies]
@ -53,7 +53,7 @@ assert_cmd = "1"
predicates = "2" predicates = "2"
select = "0.5" select = "0.5"
semver = "1.0" semver = "1.0"
pretty_assertions = "0.6" pretty_assertions = "1.2.1"
walkdir = "2.0" walkdir = "2.0"
[features] [features]

View File

@ -1,5 +1,5 @@
use crate::nop_lib::Nop; use crate::nop_lib::Nop;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches};
use mdbook::book::Book; use mdbook::book::Book;
use mdbook::errors::Error; use mdbook::errors::Error;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext}; use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
@ -7,12 +7,12 @@ use semver::{Version, VersionReq};
use std::io; use std::io;
use std::process; use std::process;
pub fn make_app() -> App<'static, 'static> { pub fn make_app() -> App<'static> {
App::new("nop-preprocessor") App::new("nop-preprocessor")
.about("A mdbook preprocessor which does precisely nothing") .about("A mdbook preprocessor which does precisely nothing")
.subcommand( .subcommand(
SubCommand::with_name("supports") App::new("supports")
.arg(Arg::with_name("renderer").required(true)) .arg(Arg::new("renderer").required(true))
.about("Check whether a renderer is supported by this preprocessor"), .about("Check whether a renderer is supported by this preprocessor"),
) )
} }

View File

@ -43,7 +43,7 @@ mdbook test path/to/book
The `--library-path` (`-L`) option allows you to add directories to the library The `--library-path` (`-L`) option allows you to add directories to the library
search path used by `rustdoc` when it builds and tests the examples. Multiple search path used by `rustdoc` when it builds and tests the examples. Multiple
directories can be specified with multiple options (`-L foo -L bar`) or with a directories can be specified with multiple options (`-L foo -L bar`) or with a
comma-delimited list (`-L foo,bar`). The path should point to the Cargo comma-delimited list (`-L foo,bar`). The path should point to the Cargo
[build cache](https://doc.rust-lang.org/cargo/guide/build-cache.html) `deps` directory that [build cache](https://doc.rust-lang.org/cargo/guide/build-cache.html) `deps` directory that
contains the build output of your project. For example, if your Rust project's book is in a directory contains the build output of your project. For example, if your Rust project's book is in a directory
named `my-book`, the following command would include the crate's dependencies when running `test`: named `my-book`, the following command would include the crate's dependencies when running `test`:
@ -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 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 not specified it will default to the value of the `build.build-dir` key in
`book.toml`, or to `./book`. `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.

View File

@ -21,7 +21,7 @@ A simple approach would be to use the popular `curl` CLI tool to download the ex
```sh ```sh
mkdir bin 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 bin/mdbook build
``` ```

View File

@ -7,7 +7,7 @@ Here is an example of what a ***book.toml*** file might look like:
```toml ```toml
[book] [book]
title = "Example book" title = "Example book"
author = "John Doe" authors = ["John Doe"]
description = "The example book covers examples." description = "The example book covers examples."
[rust] [rust]

View File

@ -157,7 +157,8 @@ The following configuration options are available:
Defaults to `404.md`. Defaults to `404.md`.
- **site-url:** The url where the book will be hosted. This is required to ensure - **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 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. - **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 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 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 copyable = true # include the copy button for copying code snippets
copy-js = true # includes the JavaScript for the code editor copy-js = true # includes the JavaScript for the code editor
line-numbers = false # displays line numbers for editable code 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`. - **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. - **copy-js:** Copy JavaScript files for the editor to the output directory.
Defaults to `true`. 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`. - **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/ [Ace]: https://ace.c9.io/

View File

@ -41,7 +41,7 @@ println!("Hello, World!");
If there is no `main` function, then the code is automatically wrapped inside one. 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 ~~~markdown
```rust,noplayground ```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 ## 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: Additional attributes can be included in Rust code blocks with comma, space, or tab-separated terms just after the language term. For example:

View File

@ -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. 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. 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 ```sh
mdbook build mdbook build
@ -106,4 +106,4 @@ mdbook build
This will generate a directory named `book` which contains the HTML content of your book. This will generate a directory named `book` which contains the HTML content of your book.
You can then place this directory on any web server to host it. You can then place this directory on any web server to host it.
For more information about publishing and deploying, check out the [Continuous Integration chapter](../continuous-integration.md) for more. For more information about publishing and deploying, check out the [Continuous Integration chapter](../continuous-integration.md) for more.

View File

@ -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. To build the `mdbook` executable from source, you will first need to install Rust and Cargo.
Follow the instructions on the [Rust installation page]. 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: 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). 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 [Rust installation page]: https://www.rust-lang.org/tools/install
[crates.io]: https://crates.io/ [crates.io]: https://crates.io/

View File

@ -7,6 +7,9 @@ use std::path::{Path, PathBuf};
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
use crate::config::BuildConfig; use crate::config::BuildConfig;
use crate::errors::*; use crate::errors::*;
use crate::utils::bracket_escape;
use log::debug;
use serde::{Deserialize, Serialize};
/// Load a book into memory from its `src/` directory. /// Load a book into memory from its `src/` directory.
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> { 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(|| { let mut f = File::create(&filename).with_context(|| {
format!("Unable to create missing file: {}", filename.display()) format!("Unable to create missing file: {}", filename.display())
})?; })?;
writeln!(f, "# {}", link.name)?; writeln!(f, "# {}", bracket_escape(&link.name))?;
} }
} }

View File

@ -6,6 +6,7 @@ use super::MDBook;
use crate::config::Config; use crate::config::Config;
use crate::errors::*; use crate::errors::*;
use crate::theme; use crate::theme;
use log::{debug, error, info, trace};
/// A helper for setting up a new book and its directory structure. /// A helper for setting up a new book and its directory structure.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]

View File

@ -14,6 +14,7 @@ pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
pub use self::init::BookBuilder; pub use self::init::BookBuilder;
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; 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::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
@ -246,6 +247,13 @@ impl MDBook {
/// Run `rustdoc` tests on the book, linking against the provided libraries. /// Run `rustdoc` tests on the book, linking against the provided libraries.
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> { 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()) let library_args: Vec<&str> = (0..library_paths.len())
.map(|_| "-L") .map(|_| "-L")
.zip(library_paths.into_iter()) .zip(library_paths.into_iter())
@ -254,6 +262,8 @@ impl MDBook {
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?; let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
let mut chapter_found = false;
// FIXME: Is "test" the proper renderer name to use here? // FIXME: Is "test" the proper renderer name to use here?
let preprocess_context = let preprocess_context =
PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string()); PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
@ -270,8 +280,16 @@ impl MDBook {
_ => continue, _ => continue,
}; };
let path = self.source_dir().join(&chapter_path); if let Some(chapter) = chapter {
info!("Testing file: {:?}", path); 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 // write preprocessed file to tempdir
let path = temp_dir.path().join(&chapter_path); let path = temp_dir.path().join(&chapter_path);
@ -311,6 +329,11 @@ impl MDBook {
if failed { if failed {
bail!("One or more tests failed"); bail!("One or more tests failed");
} }
if let Some(chapter) = chapter {
if !chapter_found {
bail!("Chapter not found: {}", chapter);
}
}
Ok(()) Ok(())
} }
@ -386,7 +409,7 @@ fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
renderers renderers
} }
const DEFAULT_PREPROCESSORS: &[&'static str] = &["links", "index"]; const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool { fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
let name = pre.name(); let name = pre.name();
@ -756,10 +779,9 @@ mod tests {
let preprocessors = determine_preprocessors(&cfg).unwrap(); let preprocessors = determine_preprocessors(&cfg).unwrap();
assert!(preprocessors assert!(!preprocessors
.iter() .iter()
.find(|preprocessor| preprocessor.name() == "random") .any(|preprocessor| preprocessor.name() == "random"));
.is_none());
} }
#[test] #[test]
@ -776,10 +798,9 @@ mod tests {
let preprocessors = determine_preprocessors(&cfg).unwrap(); let preprocessors = determine_preprocessors(&cfg).unwrap();
assert!(preprocessors assert!(!preprocessors
.iter() .iter()
.find(|preprocessor| preprocessor.name() == "links") .any(|preprocessor| preprocessor.name() == "links"));
.is_none());
} }
#[test] #[test]

View File

@ -1,6 +1,8 @@
use crate::errors::*; use crate::errors::*;
use log::{debug, trace, warn};
use memchr::{self, Memchr}; use memchr::{self, Memchr};
use pulldown_cmark::{self, Event, HeadingLevel, Tag}; use pulldown_cmark::{self, Event, HeadingLevel, Tag};
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::iter::FromIterator; use std::iter::FromIterator;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
@ -536,6 +538,10 @@ impl<'a> SummaryParser<'a> {
// Skip a HTML element such as a comment line. // Skip a HTML element such as a comment line.
Some(Event::Html(_)) => {} Some(Event::Html(_)) => {}
// Otherwise, no title. // Otherwise, no title.
Some(ev) => {
self.back(ev);
return None;
}
_ => return None, _ => return None,
} }
} }
@ -647,6 +653,18 @@ mod tests {
assert_eq!(got, should_be); 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] #[test]
fn parse_title_with_styling() { fn parse_title_with_styling() {
let src = "# My **Awesome** Summary"; let src = "# My **Awesome** Summary";

View File

@ -1,22 +1,28 @@
use crate::{get_book_dir, open}; use crate::{get_book_dir, open};
use clap::{App, ArgMatches, SubCommand}; use clap::{arg, App, Arg, ArgMatches};
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::MDBook; use mdbook::MDBook;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'help>() -> App<'help> {
SubCommand::with_name("build") App::new("build")
.about("Builds a book from its markdown files") .about("Builds a book from its markdown files")
.arg_from_usage( .arg(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ Arg::new("dest-dir")
Relative paths are interpreted relative to the book's root directory.{n}\ .short('d')
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", .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( .arg(arg!([dir]
"[dir] 'Root directory for the book{n}\ "Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)"
) ))
.arg_from_usage("-o, --open 'Opens the compiled book in a web browser'") .arg(arg!(-o --open "Opens the compiled book in a web browser"))
} }
// Build command implementation // Build command implementation
@ -32,7 +38,12 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
if args.is_present("open") { if args.is_present("open") {
// FIXME: What's the right behaviour if we don't use the HTML renderer? // 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(()) Ok(())

View File

@ -1,23 +1,28 @@
use crate::get_book_dir; use crate::get_book_dir;
use anyhow::Context; use anyhow::Context;
use clap::{App, ArgMatches, SubCommand}; use clap::{arg, App, Arg, ArgMatches};
use mdbook::MDBook; use mdbook::MDBook;
use std::fs; use std::fs;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'help>() -> App<'help> {
SubCommand::with_name("clean") App::new("clean")
.about("Deletes a built book") .about("Deletes a built book")
.arg_from_usage( .arg(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ Arg::new("dest-dir")
Relative paths are interpreted relative to the book's root directory.{n}\ .short('d')
Running this command deletes this directory.{n}\ .long("dest-dir")
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", .value_name("dest-dir")
) .help(
.arg_from_usage( "Output directory for the book{n}\
"[dir] 'Root directory for the book{n}\ Relative paths are interpreted relative to the book's root directory.{n}\
(Defaults to the Current Directory when omitted)'", 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 // Clean command implementation

View File

@ -1,5 +1,5 @@
use crate::get_book_dir; use crate::get_book_dir;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{arg, App, Arg, ArgMatches};
use mdbook::config; use mdbook::config;
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::MDBook; use mdbook::MDBook;
@ -8,25 +8,25 @@ use std::io::Write;
use std::process::Command; use std::process::Command;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'help>() -> App<'help> {
SubCommand::with_name("init") App::new("init")
.about("Creates the boilerplate structure and files for a new book") .about("Creates the boilerplate structure and files for a new book")
// the {n} denotes a newline which will properly aligned in all help messages // the {n} denotes a newline which will properly aligned in all help messages
.arg_from_usage( .arg(arg!([dir]
"[dir] 'Directory to create the book in{n}\ "Directory to create the book in{n}\
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)"
) ))
.arg_from_usage("--theme 'Copies the default theme into your source folder'") .arg(arg!(--theme "Copies the default theme into your source folder"))
.arg_from_usage("--force 'Skips confirmation prompts'") .arg(arg!(--force "Skips confirmation prompts"))
.arg( .arg(
Arg::with_name("title") Arg::new("title")
.long("title") .long("title")
.takes_value(true) .takes_value(true)
.help("Sets the book title") .help("Sets the book title")
.required(false), .required(false),
) )
.arg( .arg(
Arg::with_name("ignore") Arg::new("ignore")
.long("ignore") .long("ignore")
.takes_value(true) .takes_value(true)
.possible_values(&["none", "git"]) .possible_values(&["none", "git"])
@ -122,8 +122,5 @@ fn confirm() -> bool {
io::stdout().flush().unwrap(); io::stdout().flush().unwrap();
let mut s = String::new(); let mut s = String::new();
io::stdin().read_line(&mut s).ok(); io::stdin().read_line(&mut s).ok();
match &*s.trim() { matches!(&*s.trim(), "Y" | "y" | "yes" | "Yes")
"Y" | "y" | "yes" | "Yes" => true,
_ => false,
}
} }

View File

@ -1,7 +1,7 @@
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
use super::watch; use super::watch;
use crate::{get_book_dir, open}; 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::sink::SinkExt;
use futures_util::StreamExt; use futures_util::StreamExt;
use mdbook::errors::*; use mdbook::errors::*;
@ -18,37 +18,43 @@ use warp::Filter;
const LIVE_RELOAD_ENDPOINT: &str = "__livereload"; const LIVE_RELOAD_ENDPOINT: &str = "__livereload";
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'help>() -> App<'help> {
SubCommand::with_name("serve") App::new("serve")
.about("Serves a book at http://localhost:3000, and rebuilds it on changes") .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(
Arg::with_name("hostname") Arg::new("dest-dir")
.short("n") .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") .long("hostname")
.takes_value(true) .takes_value(true)
.default_value("localhost") .default_value("localhost")
.empty_values(false) .forbid_empty_values(true)
.help("Hostname to listen on for HTTP connections"), .help("Hostname to listen on for HTTP connections"),
) )
.arg( .arg(
Arg::with_name("port") Arg::new("port")
.short("p") .short('p')
.long("port") .long("port")
.takes_value(true) .takes_value(true)
.default_value("3000") .default_value("3000")
.empty_values(false) .forbid_empty_values(true)
.help("Port to use for HTTP connections"), .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 // Serve command implementation
@ -62,11 +68,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let address = format!("{}:{}", hostname, port); let address = format!("{}:{}", hostname, port);
let livereload_url = format!("ws://{}/{}", address, LIVE_RELOAD_ENDPOINT);
let update_config = |book: &mut MDBook| { let update_config = |book: &mut MDBook| {
book.config book.config
.set("output.html.livereload-url", &livereload_url) .set("output.html.live-reload-endpoint", &LIVE_RELOAD_ENDPOINT)
.expect("livereload-url update failed"); .expect("live-reload-endpoint update failed");
if let Some(dest_dir) = args.value_of("dest-dir") { if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into(); book.config.build.build_dir = dest_dir.into();
} }
@ -84,8 +89,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let input_404 = book let input_404 = book
.config .config
.get("output.html.input-404") .get("output.html.input-404")
.map(toml::Value::as_str) .and_then(toml::Value::as_str)
.and_then(std::convert::identity) // flatten
.map(ToString::to_string); .map(ToString::to_string);
let file_404 = get_404_output_file(&input_404); let file_404 = get_404_output_file(&input_404);

View File

@ -1,29 +1,47 @@
use crate::get_book_dir; use crate::get_book_dir;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{arg, App, Arg, ArgMatches};
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::MDBook; use mdbook::MDBook;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'help>() -> App<'help> {
SubCommand::with_name("test") App::new("test")
.about("Tests that a book's Rust code samples compile") .about("Tests that a book's Rust code samples compile")
.arg_from_usage( .arg(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ Arg::new("dest-dir")
Relative paths are interpreted relative to the book's root directory.{n}\ .short('d')
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", .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( .arg(arg!([dir]
"[dir] 'Root directory for the book{n}\ "Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)"
) ))
.arg(Arg::with_name("library-path") .arg(Arg::new("library-path")
.short("L") .short('L')
.long("library-path") .long("library-path")
.value_name("dir") .value_name("dir")
.takes_value(true) .takes_value(true)
.use_delimiter(true)
.require_delimiter(true) .require_delimiter(true)
.multiple(true) .multiple_values(true)
.empty_values(false) .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")) .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") .values_of("library-path")
.map(std::iter::Iterator::collect) .map(std::iter::Iterator::collect)
.unwrap_or_default(); .unwrap_or_default();
let chapter: Option<&str> = args.value_of("chapter");
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?; let mut book = MDBook::load(&book_dir)?;
if let Some(dest_dir) = args.value_of("dest-dir") { if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into(); book.config.build.build_dir = dest_dir.into();
} }
match chapter {
book.test(library_paths)?; Some(_) => book.test_chapter(library_paths, chapter),
None => book.test(library_paths),
}?;
Ok(()) Ok(())
} }

View File

@ -1,5 +1,5 @@
use crate::{get_book_dir, open}; use crate::{get_book_dir, open};
use clap::{App, ArgMatches, SubCommand}; use clap::{arg, App, Arg, ArgMatches};
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::utils; use mdbook::utils;
use mdbook::MDBook; use mdbook::MDBook;
@ -10,19 +10,25 @@ use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'help>() -> App<'help> {
SubCommand::with_name("watch") App::new("watch")
.about("Watches a book's files and rebuilds it on changes") .about("Watches a book's files and rebuilds it on changes")
.arg_from_usage( .arg(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ Arg::new("dest-dir")
Relative paths are interpreted relative to the book's root directory.{n}\ .short('d')
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", .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( .arg(arg!([dir]
"[dir] 'Root directory for the book{n}\ "Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)"
) ))
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'") .arg(arg!(-o --open "Opens the compiled book in a web browser"))
} }
// Watch command implementation // Watch command implementation
@ -39,7 +45,12 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
if args.is_present("open") { if args.is_present("open") {
book.build()?; 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| { trigger_on_change(&book, |paths, book_dir| {

View File

@ -49,6 +49,7 @@
#![deny(missing_docs)] #![deny(missing_docs)]
use log::{debug, trace, warn};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
@ -227,10 +228,10 @@ impl Config {
let value = Value::try_from(value) let value = Value::try_from(value)
.with_context(|| "Unable to represent the item as a JSON Value")?; .with_context(|| "Unable to represent the item as a JSON Value")?;
if index.starts_with("book.") { if let Some(key) = index.strip_prefix("book.") {
self.book.update_value(&index[5..], value); self.book.update_value(key, value);
} else if index.starts_with("build.") { } else if let Some(key) = index.strip_prefix("build.") {
self.build.update_value(&index[6..], value); self.build.update_value(key, value);
} else { } else {
self.rest.insert(index, value); 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> { fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
let raw = Value::deserialize(de)?; let raw = Value::deserialize(de)?;
@ -371,15 +372,8 @@ impl Serialize for Config {
} }
fn parse_env(key: &str) -> Option<String> { fn parse_env(key: &str) -> Option<String> {
const PREFIX: &str = "MDBOOK_"; key.strip_prefix("MDBOOK_")
.map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
if key.starts_with(PREFIX) {
let key = &key[PREFIX.len()..];
Some(key.to_lowercase().replace("__", ".").replace("_", "-"))
} else {
None
}
} }
fn is_legacy_format(table: &Value) -> bool { fn is_legacy_format(table: &Value) -> bool {
@ -533,14 +527,14 @@ pub struct HtmlConfig {
/// directly jumping to editing the currently viewed page. /// directly jumping to editing the currently viewed page.
/// Contains {path} that is replaced with chapter source file path /// Contains {path} that is replaced with chapter source file path
pub edit_url_template: Option<String>, pub edit_url_template: Option<String>,
/// This is used as a bit of a workaround for the `mdbook serve` command. /// Endpoint of websocket, for livereload usage. Value loaded from .toml file
/// Basically, because you set the websocket port from the command line, the /// is ignored, because our code overrides this field with the value [`LIVE_RELOAD_ENDPOINT`]
/// `mdbook serve` command needs a way to let the HTML renderer know where ///
/// to point livereloading at, if it has been enabled. /// [`LIVE_RELOAD_ENDPOINT`]: cmd::serve::LIVE_RELOAD_ENDPOINT
/// ///
/// This config item *should not be edited* by the end user. /// This config item *should not be edited* by the end user.
#[doc(hidden)] #[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 /// The mapping from old pages to new pages/URLs to use when generating
/// redirects. /// redirects.
pub redirect: HashMap<String, String>, pub redirect: HashMap<String, String>,
@ -569,7 +563,7 @@ impl Default for HtmlConfig {
input_404: None, input_404: None,
site_url: None, site_url: None,
cname: None, cname: None,
livereload_url: None, live_reload_endpoint: None,
redirect: HashMap::new(), redirect: HashMap::new(),
} }
} }
@ -588,7 +582,7 @@ impl HtmlConfig {
/// Configuration for how to render the print icon, print.html, and print.css. /// Configuration for how to render the print icon, print.html, and print.css.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(default, rename_all = "kebab-case")]
pub struct Print { pub struct Print {
/// Whether print support is enabled. /// Whether print support is enabled.
pub enable: bool, pub enable: bool,
@ -633,6 +627,8 @@ pub struct Playground {
/// Set's the language the playground will work with /// Set's the language the playground will work with
/// TODO: Use an array when there's support for multiple languages simultaneously /// TODO: Use an array when there's support for multiple languages simultaneously
pub language: String, pub language: String,
/// Display the run button. Default: `true`
pub runnable: bool,
} }
impl Default for Playground { impl Default for Playground {
@ -643,6 +639,7 @@ impl Default for Playground {
copy_js: true, copy_js: true,
line_numbers: false, line_numbers: false,
language: "rust".to_string(), language: "rust".to_string(),
runnable: true,
} }
} }
} }
@ -725,6 +722,7 @@ impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
mod tests { mod tests {
use super::*; use super::*;
use crate::utils::fs::get_404_output_file; use crate::utils::fs::get_404_output_file;
use serde_json::json;
const COMPLEX_CONFIG: &str = r#" const COMPLEX_CONFIG: &str = r#"
[book] [book]
@ -787,6 +785,7 @@ mod tests {
copy_js: true, copy_js: true,
line_numbers: false, line_numbers: false,
language: "rust".to_string(), language: "rust".to_string(),
runnable: true,
}; };
let html_should_be = HtmlConfig { let html_should_be = HtmlConfig {
curly_quotes: true, curly_quotes: true,
@ -817,6 +816,22 @@ mod tests {
assert_eq!(got.html_config().unwrap(), html_should_be); 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] #[test]
fn edition_2015() { fn edition_2015() {
let src = r#" let src = r#"
@ -1023,7 +1038,7 @@ mod tests {
fn encode_env_var(key: &str) -> String { fn encode_env_var(key: &str) -> String {
format!( format!(
"MDBOOK_{}", "MDBOOK_{}",
key.to_uppercase().replace('.', "__").replace("-", "_") key.to_uppercase().replace('.', "__").replace('-', "_")
) )
} }
@ -1047,11 +1062,10 @@ mod tests {
} }
#[test] #[test]
#[allow(clippy::approx_constant)]
fn update_config_using_env_var_and_complex_value() { fn update_config_using_env_var_and_complex_value() {
let mut cfg = Config::default(); let mut cfg = Config::default();
let key = "foo-bar.baz"; 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(); let value_str = serde_json::to_string(&value).unwrap();
assert!(cfg.get(key).is_none()); assert!(cfg.get(key).is_none());
@ -1161,4 +1175,24 @@ mod tests {
Config::from_str(src).unwrap(); 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);
}
} }

View File

@ -82,20 +82,6 @@
#![deny(missing_docs)] #![deny(missing_docs)]
#![deny(rust_2018_idioms)] #![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 book;
pub mod config; pub mod config;

View File

@ -5,7 +5,8 @@ extern crate log;
use anyhow::anyhow; use anyhow::anyhow;
use chrono::Local; 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 env_logger::Builder;
use log::LevelFilter; use log::LevelFilter;
use mdbook::utils; use mdbook::utils;
@ -25,25 +26,31 @@ fn main() {
// Check which subcomamnd the user ran... // Check which subcomamnd the user ran...
let res = match app.get_matches().subcommand() { let res = match app.get_matches().subcommand() {
("init", Some(sub_matches)) => cmd::init::execute(sub_matches), Some(("init", sub_matches)) => cmd::init::execute(sub_matches),
("build", Some(sub_matches)) => cmd::build::execute(sub_matches), Some(("build", sub_matches)) => cmd::build::execute(sub_matches),
("clean", Some(sub_matches)) => cmd::clean::execute(sub_matches), Some(("clean", sub_matches)) => cmd::clean::execute(sub_matches),
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches), Some(("watch", sub_matches)) => cmd::watch::execute(sub_matches),
#[cfg(feature = "serve")] #[cfg(feature = "serve")]
("serve", Some(sub_matches)) => cmd::serve::execute(sub_matches), Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches),
("test", Some(sub_matches)) => cmd::test::execute(sub_matches), Some(("test", sub_matches)) => cmd::test::execute(sub_matches),
("completions", Some(sub_matches)) => (|| { Some(("completions", sub_matches)) => (|| {
let shell: Shell = sub_matches let shell: Shell = sub_matches
.value_of("shell") .value_of("shell")
.ok_or_else(|| anyhow!("Shell name missing."))? .ok_or_else(|| anyhow!("Shell name missing."))?
.parse() .parse()
.map_err(|s| anyhow!("Invalid shell: {}", s))?; .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(()) Ok(())
})(), })(),
(_, _) => unreachable!(), _ => unreachable!(),
}; };
if let Err(e) = res { if let Err(e) = res {
@ -54,14 +61,13 @@ fn main() {
} }
/// Create a list of valid arguments and sub-commands /// 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!()) let app = App::new(crate_name!())
.about(crate_description!()) .about(crate_description!())
.author("Mathieu David <mathieudavid@mathieudavid.org>") .author("Mathieu David <mathieudavid@mathieudavid.org>")
.version(VERSION) .version(VERSION)
.setting(AppSettings::GlobalVersion) .setting(AppSettings::PropagateVersion)
.setting(AppSettings::ArgRequiredElseHelp) .setting(AppSettings::ArgRequiredElseHelp)
.setting(AppSettings::ColoredHelp)
.after_help( .after_help(
"For more information about a specific command, try `mdbook <command> --help`\n\ "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", 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::test::make_subcommand())
.subcommand(cmd::clean::make_subcommand()) .subcommand(cmd::clean::make_subcommand())
.subcommand( .subcommand(
SubCommand::with_name("completions") App::new("completions")
.about("Generate shell completions for your shell to stdout") .about("Generate shell completions for your shell to stdout")
.arg( .arg(
Arg::with_name("shell") Arg::new("shell")
.takes_value(true) .takes_value(true)
.possible_values(&Shell::variants()) .possible_values(Shell::possible_values())
.help("the shell to generate completions for") .help("the shell to generate completions for")
.value_name("SHELL") .value_name("SHELL")
.required(true), .required(true),
@ -137,3 +143,8 @@ fn open<P: AsRef<OsStr>>(path: P) {
error!("Error opening web browser: {}", e); error!("Error opening web browser: {}", e);
} }
} }
#[test]
fn verify_app() {
create_clap_app().debug_assert();
}

View File

@ -1,6 +1,7 @@
use super::{Preprocessor, PreprocessorContext}; use super::{Preprocessor, PreprocessorContext};
use crate::book::Book; use crate::book::Book;
use crate::errors::*; use crate::errors::*;
use log::{debug, trace, warn};
use shlex::Shlex; use shlex::Shlex;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::process::{Child, Command, Stdio}; use std::process::{Child, Command, Stdio};

View File

@ -1,10 +1,11 @@
use regex::Regex; use regex::Regex;
use std::path::Path; use std::path::Path;
use crate::errors::*;
use super::{Preprocessor, PreprocessorContext}; use super::{Preprocessor, PreprocessorContext};
use crate::book::{Book, BookItem}; 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 /// A preprocessor for converting file name `README.md` to `index.md` since
/// `README.md` is the de facto index file in markdown-based documentation. /// `README.md` is the de facto index file in markdown-based documentation.

View File

@ -10,6 +10,8 @@ use std::path::{Path, PathBuf};
use super::{Preprocessor, PreprocessorContext}; use super::{Preprocessor, PreprocessorContext};
use crate::book::{Book, BookItem}; use crate::book::{Book, BookItem};
use lazy_static::lazy_static;
use log::{error, warn};
const ESCAPE_CHAR: char = '\\'; const ESCAPE_CHAR: char = '\\';
const MAX_LINK_NESTED_DEPTH: usize = 10; const MAX_LINK_NESTED_DEPTH: usize = 10;
@ -146,6 +148,7 @@ enum RangeOrAnchor {
} }
// A range of lines specified with some include directive. // 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)] #[derive(PartialEq, Debug, Clone)]
enum LineRange { enum LineRange {
Range(Range<usize>), Range(Range<usize>),

View File

@ -12,6 +12,7 @@ use crate::book::Book;
use crate::config::Config; use crate::config::Config;
use crate::errors::*; use crate::errors::*;
use serde::{Deserialize, Serialize};
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;

View File

@ -14,7 +14,10 @@ use std::path::{Path, PathBuf};
use crate::utils::fs::get_404_output_file; use crate::utils::fs::get_404_output_file;
use handlebars::Handlebars; use handlebars::Handlebars;
use lazy_static::lazy_static;
use log::{debug, trace, warn};
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use serde_json::json;
#[derive(Default)] #[derive(Default)]
pub struct HtmlHandlebars; pub struct HtmlHandlebars;
@ -116,7 +119,7 @@ impl HtmlHandlebars {
if ctx.is_index { if ctx.is_index {
ctx.data.insert("path".to_owned(), json!("index.md")); ctx.data.insert("path".to_owned(), json!("index.md"));
ctx.data.insert("path_to_root".to_owned(), json!("")); 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 = ctx.handlebars.render("index", &ctx.data)?;
let rendered_index = let rendered_index =
self.post_process(rendered_index, &ctx.html_config.playground, ctx.edition); 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 // 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("path".to_owned(), json!("404.md"));
data_404.insert("content".to_owned(), json!(html_content_404)); 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 = handlebars.render("index", &data_404)?;
let rendered = let rendered =
@ -474,7 +484,13 @@ impl Renderer for HtmlHandlebars {
let mut handlebars = Handlebars::new(); let mut handlebars = Handlebars::new();
let theme_dir = match html_config.theme { 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"), None => ctx.root.join("theme"),
}; };
@ -527,7 +543,8 @@ impl Renderer for HtmlHandlebars {
chapter_titles: &ctx.chapter_titles, chapter_titles: &ctx.chapter_titles,
}; };
self.render_item(item, ctx, &mut print_content)?; 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 // Render 404 page
@ -606,8 +623,11 @@ fn make_data(
if theme.favicon_svg.is_some() { if theme.favicon_svg.is_some() {
data.insert("favicon_svg".to_owned(), json!("favicon.svg")); data.insert("favicon_svg".to_owned(), json!("favicon.svg"));
} }
if let Some(ref livereload) = html_config.livereload_url { if let Some(ref live_reload_endpoint) = html_config.live_reload_endpoint {
data.insert("livereload".to_owned(), json!(livereload)); data.insert(
"live_reload_endpoint".to_owned(),
json!(live_reload_endpoint),
);
} }
let default_theme = match html_config.default_theme { 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 /// Goes through the rendered HTML, making sure all header tags have
/// an anchor respectively so people can link to sections directly. /// an anchor respectively so people can link to sections directly.
fn build_header_links(html: &str) -> String { 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(); let mut id_counter = HashMap::new();
regex BUILD_HEADER_LINKS
.replace_all(html, |caps: &Captures<'_>| { .replace_all(html, |caps: &Captures<'_>| {
let level = caps[1] let level = caps[1]
.parse() .parse()
@ -768,16 +791,7 @@ fn insert_link_into_header(
content: &str, content: &str,
id_counter: &mut HashMap<String, usize>, id_counter: &mut HashMap<String, usize>,
) -> String { ) -> String {
let raw_id = utils::id_from_content(content); let id = utils::unique_id_from_content(content, id_counter);
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;
format!( format!(
r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##, 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 // This function replaces all commas by spaces in the code block classes
fn fix_code_blocks(html: &str) -> String { fn fix_code_blocks(html: &str) -> String {
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap(); lazy_static! {
regex static ref FIX_CODE_BLOCKS: Regex =
Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
}
FIX_CODE_BLOCKS
.replace_all(html, |caps: &Captures<'_>| { .replace_all(html, |caps: &Captures<'_>| {
let before = &caps[1]; let before = &caps[1];
let classes = &caps[2].replace(",", " "); let classes = &caps[2].replace(',', " ");
let after = &caps[3]; let after = &caps[3];
format!( format!(
@ -818,8 +836,11 @@ fn add_playground_pre(
playground_config: &Playground, playground_config: &Playground,
edition: Option<RustEdition>, edition: Option<RustEdition>,
) -> String { ) -> String {
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap(); lazy_static! {
regex static ref ADD_PLAYGROUND_PRE: Regex =
Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
}
ADD_PLAYGROUND_PRE
.replace_all(html, |caps: &Captures<'_>| { .replace_all(html, |caps: &Captures<'_>| {
let text = &caps[1]; let text = &caps[1];
let classes = &caps[2]; let classes = &caps[2];
@ -853,7 +874,8 @@ fn add_playground_pre_rust(
) -> String { ) -> String {
if (!classes.contains("ignore") if (!classes.contains("ignore")
&& !classes.contains("noplayground") && !classes.contains("noplayground")
&& !classes.contains("noplaypen")) && !classes.contains("noplaypen")
&& playground_config.runnable)
|| classes.contains("mdbook-runnable") || classes.contains("mdbook-runnable")
{ {
let contains_e2015 = classes.contains("edition2015"); let contains_e2015 = classes.contains("edition2015");
@ -870,12 +892,12 @@ fn add_playground_pre_rust(
None => "", None => "",
} }
}; };
let all_classes = format!("{}{}", classes, edition_class);
// wrap the contents in an external pre block // wrap the contents in an external pre block
format!( format!(
"<pre class=\"playground\"><code class=\"{}\">{}</code></pre>", "<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
all_classes, classes,
edition_class,
{ {
let content: Cow<'_, str> = if playground_config.editable let content: Cow<'_, str> = if playground_config.editable
&& classes.contains("editable") && classes.contains("editable")
@ -887,7 +909,7 @@ fn add_playground_pre_rust(
// we need to inject our own main // we need to inject our own main
let (attrs, code) = partition_source(code); 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) hide_lines(&content)
} }
@ -902,14 +924,21 @@ lazy_static! {
} }
fn hide_lines(content: &str) -> String { 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()); 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 let Some(caps) = BORING_LINES_REGEX.captures(line) {
if &caps[2] == "#" { if &caps[2] == "#" {
result += &caps[1]; result += &caps[1];
result += &caps[2]; result += &caps[2];
result += &caps[3]; result += &caps[3];
result += "\n"; result += newline;
continue; continue;
} else if &caps[2] != "!" && &caps[2] != "[" { } else if &caps[2] != "!" && &caps[2] != "[" {
result += "<span class=\"boring\">"; result += "<span class=\"boring\">";
@ -918,13 +947,13 @@ fn hide_lines(content: &str) -> String {
result += &caps[2]; result += &caps[2];
} }
result += &caps[3]; result += &caps[3];
result += "\n"; result += newline;
result += "</span>"; result += "</span>";
continue; continue;
} }
} }
result += line; result += line;
result += "\n"; result += newline;
} }
result result
} }
@ -1004,19 +1033,19 @@ mod tests {
fn add_playground() { fn add_playground() {
let inputs = [ let inputs = [
("<code class=\"language-rust\">x()</code>", ("<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>", ("<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>", ("<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>", ("<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>", ("<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 # 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>", ("<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 { for (src, should_be) in &inputs {
let got = add_playground_pre( let got = add_playground_pre(
@ -1034,13 +1063,13 @@ mod tests {
fn add_playground_edition2015() { fn add_playground_edition2015() {
let inputs = [ let inputs = [
("<code class=\"language-rust\">x()</code>", ("<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>", ("<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>", ("<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>", ("<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 { for (src, should_be) in &inputs {
let got = add_playground_pre( let got = add_playground_pre(
@ -1058,13 +1087,13 @@ mod tests {
fn add_playground_edition2018() { fn add_playground_edition2018() {
let inputs = [ let inputs = [
("<code class=\"language-rust\">x()</code>", ("<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>", ("<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>", ("<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>", ("<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 { for (src, should_be) in &inputs {
let got = add_playground_pre( let got = add_playground_pre(
@ -1082,13 +1111,13 @@ mod tests {
fn add_playground_edition2021() { fn add_playground_edition2021() {
let inputs = [ let inputs = [
("<code class=\"language-rust\">x()</code>", ("<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>", ("<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>", ("<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>", ("<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 { for (src, should_be) in &inputs {
let got = add_playground_pre( let got = add_playground_pre(

View File

@ -4,6 +4,8 @@ use std::path::Path;
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable}; use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
use crate::utils; use crate::utils;
use log::{debug, trace};
use serde_json::json;
type StringMap = BTreeMap<String, String>; type StringMap = BTreeMap<String, String>;
@ -61,7 +63,7 @@ fn find_chapter(
.as_json() .as_json()
.as_str() .as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", ""); .replace('\"', "");
if !rc.evaluate(ctx, "@root/is_index")?.is_missing() { if !rc.evaluate(ctx, "@root/is_index")?.is_missing() {
// Special case for index.md which may be a synthetic page. // Special case for index.md which may be a synthetic page.
@ -121,7 +123,7 @@ fn render(
.as_json() .as_json()
.as_str() .as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", ""); .replace('\"', "");
context.insert( context.insert(
"path_to_root".to_owned(), "path_to_root".to_owned(),
@ -141,20 +143,17 @@ fn render(
.with_extension("html") .with_extension("html")
.to_str() .to_str()
.ok_or_else(|| RenderError::new("Link could not be converted 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"); trace!("Render template");
_h.template() let t = _h
.ok_or_else(|| RenderError::new("Error with the handlebars template")) .template()
.and_then(|t| { .ok_or_else(|| RenderError::new("Error with the handlebars template"))?;
let mut local_rc = rc.clone(); let local_ctx = Context::wraps(&context)?;
let local_ctx = Context::wraps(&context)?; let mut local_rc = rc.clone();
t.render(r, &local_ctx, &mut local_rc, out) t.render(r, &local_ctx, &mut local_rc, out)
})?;
Ok(())
} }
pub fn previous( pub fn previous(

View File

@ -1,4 +1,5 @@
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError}; use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
use log::trace;
pub fn theme_option( pub fn theme_option(
h: &Helper<'_, '_>, h: &Helper<'_, '_>,

View File

@ -1,11 +1,10 @@
use std::collections::BTreeMap;
use std::io;
use std::path::Path; use std::path::Path;
use std::{cmp::Ordering, collections::BTreeMap};
use crate::utils; use crate::utils;
use crate::utils::bracket_escape;
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError}; use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
use pulldown_cmark::{html, Event, Parser};
// Handlebars helper to construct TOC // Handlebars helper to construct TOC
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -34,7 +33,7 @@ impl HelperDef for RenderToc {
.as_json() .as_json()
.as_str() .as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", ""); .replace('\"', "");
let current_section = rc let current_section = rc
.evaluate(ctx, "@root/section")? .evaluate(ctx, "@root/section")?
@ -58,6 +57,11 @@ impl HelperDef for RenderToc {
out.write("<ol class=\"chapter\">")?; out.write("<ol class=\"chapter\">")?;
let mut current_level = 1; 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 { for item in chapters {
// Spacer // Spacer
@ -82,61 +86,66 @@ impl HelperDef for RenderToc {
level - 1 < fold_level as usize level - 1 < fold_level as usize
}; };
if level > current_level { match level.cmp(&current_level) {
while level > current_level { Ordering::Greater => {
out.write("<li>")?; while level > current_level {
out.write("<ol class=\"section\">")?; out.write("<li>")?;
current_level += 1; out.write("<ol class=\"section\">")?;
current_level += 1;
}
write_li_open_tag(out, is_expanded, false)?;
} }
write_li_open_tag(out, is_expanded, false)?; Ordering::Less => {
} else if level < current_level { while level < current_level {
while level < current_level { out.write("</ol>")?;
out.write("</ol>")?; out.write("</li>")?;
out.write("</li>")?; current_level -= 1;
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 // Part title
if let Some(title) = item.get("part") { if let Some(title) = item.get("part") {
out.write("<li class=\"part-title\">")?; out.write("<li class=\"part-title\">")?;
write_escaped(out, title)?; out.write(&bracket_escape(title))?;
out.write("</li>")?; out.write("</li>")?;
continue; continue;
} }
// Link // Link
let path_exists = if let Some(path) = let path_exists: bool;
item.get("path") match item.get("path") {
.and_then(|p| if p.is_empty() { None } else { Some(p) }) Some(path) if !path.is_empty() => {
{ out.write("<a href=\"")?;
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(_)")) // Add link
.with_extension("html") out.write(&utils::fs::path_to_root(&current_path))?;
.to_str() out.write(&tmp)?;
.unwrap() out.write("\"")?;
// Hack for windows who tends to use `\` as separator instead of `/`
.replace("\\", "/");
// Add link if path == &current_path || is_first_chapter {
out.write(&utils::fs::path_to_root(&current_path))?; is_first_chapter = false;
out.write(&tmp)?; out.write(" class=\"active\"")?;
out.write("\"")?; }
if path == &current_path { out.write(">")?;
out.write(" class=\"active\"")?; path_exists = true;
} }
_ => {
out.write(">")?; out.write("<div>")?;
true path_exists = false;
} else { }
out.write("<div>")?; }
false
};
if !self.no_section_label { if !self.no_section_label {
// Section does not necessarily exist // Section does not necessarily exist
@ -148,20 +157,7 @@ impl HelperDef for RenderToc {
} }
if let Some(name) = item.get("name") { if let Some(name) = item.get("name") {
// Render only inline code blocks out.write(&bracket_escape(name))?
// 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)?;
} }
if path_exists { if path_exists {
@ -205,18 +201,3 @@ fn write_li_open_tag(
li.push_str("\">"); li.push_str("\">");
out.write(&li) 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("&lt;")?,
b'>' => out.write("&gt;")?,
_ => unreachable!(),
}
title = &title[next + 1..];
}
out.write(title)?;
Ok(())
}

View File

@ -2,7 +2,7 @@ use std::borrow::Cow;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::Path; use std::path::Path;
use elasticlunr::Index; use elasticlunr::{Index, IndexBuilder};
use pulldown_cmark::*; use pulldown_cmark::*;
use crate::book::{Book, BookItem}; use crate::book::{Book, BookItem};
@ -10,10 +10,29 @@ use crate::config::Search;
use crate::errors::*; use crate::errors::*;
use crate::theme::searcher; use crate::theme::searcher;
use crate::utils; 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. /// Creates all files required for search.
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> { 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()); let mut doc_urls = Vec::with_capacity(book.sections.len());
for item in book.iter() { for item in book.iter() {
@ -97,6 +116,7 @@ fn render_item(
breadcrumbs.push(chapter.name.clone()); breadcrumbs.push(chapter.name.clone());
let mut id_counter = HashMap::new();
while let Some(event) = p.next() { while let Some(event) = p.next() {
match event { match event {
Event::Start(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => { 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 => { Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
in_heading = false; 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()); breadcrumbs.push(heading.clone());
} }
Event::Start(Tag::FootnoteDefinition(name)) => { 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 fields = BTreeMap::new();
let mut opt = SearchOptionsField::default(); let mut opt = SearchOptionsField::default();
opt.boost = Some(search_config.boost_title); let mut insert_boost = |key: &str, boost| {
fields.insert("title".into(), opt); opt.boost = Some(boost);
opt.boost = Some(search_config.boost_paragraph); fields.insert(key.into(), opt);
fields.insert("body".into(), opt); };
opt.boost = Some(search_config.boost_hierarchy); insert_boost("title", search_config.boost_title);
fields.insert("breadcrumbs".into(), opt); insert_boost("body", search_config.boost_paragraph);
insert_boost("breadcrumbs", search_config.boost_hierarchy);
let search_options = SearchOptions { let search_options = SearchOptions {
bool: if search_config.use_boolean_and { bool: if search_config.use_boolean_and {

View File

@ -2,7 +2,7 @@ use crate::book::BookItem;
use crate::errors::*; use crate::errors::*;
use crate::renderer::{RenderContext, Renderer}; use crate::renderer::{RenderContext, Renderer};
use crate::utils; use crate::utils;
use log::trace;
use std::fs; use std::fs;
#[derive(Default)] #[derive(Default)]

View File

@ -27,8 +27,11 @@ use std::process::{Command, Stdio};
use crate::book::Book; use crate::book::Book;
use crate::config::Config; use crate::config::Config;
use crate::errors::*; use crate::errors::*;
use log::{error, info, trace, warn};
use toml::Value; use toml::Value;
use serde::{Deserialize, Serialize};
/// An arbitrary `mdbook` backend. /// An arbitrary `mdbook` backend.
/// ///
/// Although it's quite possible for you to import `mdbook` as a library and /// Although it's quite possible for you to import `mdbook` as a library and

View File

@ -8,7 +8,6 @@ Original by Dempfi (https://github.com/dempfi/ayu)
overflow-x: auto; overflow-x: auto;
background: #191f26; background: #191f26;
color: #e6e1cf; color: #e6e1cf;
padding: 0.5em;
} }
.hljs-comment, .hljs-comment,

View File

@ -208,24 +208,63 @@ pre {
pre > .buttons { pre > .buttons {
position: absolute; position: absolute;
z-index: 100; z-index: 100;
right: 5px; right: 0px;
top: 5px; top: 2px;
margin: 0px;
padding: 2px 0px;
color: var(--sidebar-fg); color: var(--sidebar-fg);
cursor: pointer; 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 { pre > .buttons :hover {
color: var(--sidebar-active); color: var(--sidebar-active);
border-color: var(--icons-hover);
background-color: var(--theme-hover);
} }
pre > .buttons i { pre > .buttons i {
margin-left: 8px; margin-left: 8px;
} }
pre > .buttons button { pre > .buttons button {
color: inherit;
background: transparent;
border: none;
cursor: inherit; 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 { pre > .result {
margin-top: 10px; margin-top: 10px;
} }

View File

@ -26,6 +26,16 @@ code {
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */ 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. */ /* Don't change font size in headers. */
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
font-size: unset; font-size: unset;
@ -80,8 +90,7 @@ h6:target::before {
.content { .content {
overflow-y: auto; overflow-y: auto;
padding: 0 15px; padding: 0 5px 50px 5px;
padding-bottom: 50px;
} }
.content main { .content main {
margin-left: auto; margin-left: auto;

View File

@ -61,7 +61,6 @@
overflow-x: auto; overflow-x: auto;
background: #f6f7f6; background: #f6f7f6;
color: #000; color: #000;
padding: 0.5em;
} }
.hljs-emphasis { .hljs-emphasis {

View File

@ -51,18 +51,18 @@
{{#if mathjax_support}} {{#if mathjax_support}}
<!-- MathJax --> <!-- 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}} {{/if}}
</head> </head>
<body> <body>
<!-- Provide site root to javascript --> <!-- Provide site root to javascript -->
<script type="text/javascript"> <script>
var path_to_root = "{{ path_to_root }}"; var path_to_root = "{{ path_to_root }}";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}"; var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
</script> </script>
<!-- Work around some values being stored in localStorage wrapped in quotes --> <!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript"> <script>
try { try {
var theme = localStorage.getItem('mdbook-theme'); var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar'); var sidebar = localStorage.getItem('mdbook-sidebar');
@ -78,7 +78,7 @@
</script> </script>
<!-- Set the theme before any content is loaded, prevents flash --> <!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript"> <script>
var theme; var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { } try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; } if (theme === null || theme === undefined) { theme = default_theme; }
@ -90,7 +90,7 @@
</script> </script>
<!-- Hide / unhide sidebar before it is displayed --> <!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript"> <script>
var html = document.querySelector('html'); var html = document.querySelector('html');
var sidebar = 'hidden'; var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) { if (document.body.clientWidth >= 1080) {
@ -171,7 +171,7 @@
{{/if}} {{/if}}
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM --> <!-- 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-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible'); document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) { Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
@ -219,10 +219,12 @@
</div> </div>
{{#if livereload}} {{#if live_reload_endpoint}}
<!-- Livereload script (if served using the cli tool) --> <!-- Livereload script (if served using the cli tool) -->
<script type="text/javascript"> <script>
var socket = new WebSocket("{{{livereload}}}"); const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) { socket.onmessage = function (event) {
if (event.data === "reload") { if (event.data === "reload") {
socket.close(); socket.close();
@ -238,7 +240,7 @@
{{#if google_analytics}} {{#if google_analytics}}
<!-- Google Analytics Tag --> <!-- Google Analytics Tag -->
<script type="text/javascript"> <script>
var localAddrs = ["localhost", "127.0.0.1", ""]; var localAddrs = ["localhost", "127.0.0.1", ""];
// make sure we don't activate google analytics if the developer is // make sure we don't activate google analytics if the developer is
@ -256,43 +258,43 @@
{{/if}} {{/if}}
{{#if playground_line_numbers}} {{#if playground_line_numbers}}
<script type="text/javascript"> <script>
window.playground_line_numbers = true; window.playground_line_numbers = true;
</script> </script>
{{/if}} {{/if}}
{{#if playground_copyable}} {{#if playground_copyable}}
<script type="text/javascript"> <script>
window.playground_copyable = true; window.playground_copyable = true;
</script> </script>
{{/if}} {{/if}}
{{#if playground_js}} {{#if playground_js}}
<script src="{{ path_to_root }}ace.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" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}editor.js" charset="utf-8"></script>
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}mode-rust.js" 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-dawn.js" 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 }}theme-tomorrow_night.js" charset="utf-8"></script>
{{/if}} {{/if}}
{{#if search_js}} {{#if search_js}}
<script src="{{ path_to_root }}elasticlunr.min.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" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}mark.min.js" charset="utf-8"></script>
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}searcher.js" charset="utf-8"></script>
{{/if}} {{/if}}
<script src="{{ path_to_root }}clipboard.min.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" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}highlight.js" charset="utf-8"></script>
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}book.js" charset="utf-8"></script>
<!-- Custom JS scripts --> <!-- Custom JS scripts -->
{{#each additional_js}} {{#each additional_js}}
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script> <script src="{{ ../path_to_root }}{{this}}"></script>
{{/each}} {{/each}}
{{#if is_print}} {{#if is_print}}
{{#if mathjax_support}} {{#if mathjax_support}}
<script type="text/javascript"> <script>
window.addEventListener('load', function() { window.addEventListener('load', function() {
MathJax.Hub.Register.StartupHook('End', function() { MathJax.Hub.Register.StartupHook('End', function() {
window.setTimeout(window.print, 100); window.setTimeout(window.print, 100);
@ -300,7 +302,7 @@
}); });
</script> </script>
{{else}} {{else}}
<script type="text/javascript"> <script>
window.addEventListener('load', function() { window.addEventListener('load', function() {
window.setTimeout(window.print, 100); window.setTimeout(window.print, 100);
}); });

View File

@ -12,7 +12,7 @@ use std::io::Read;
use std::path::Path; use std::path::Path;
use crate::errors::*; use crate::errors::*;
use log::warn;
pub static INDEX: &[u8] = include_bytes!("index.hbs"); pub static INDEX: &[u8] = include_bytes!("index.hbs");
pub static HEAD: &[u8] = include_bytes!("head.hbs"); pub static HEAD: &[u8] = include_bytes!("head.hbs");
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs"); pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");

View File

@ -81,8 +81,6 @@
overflow-x: auto; overflow-x: auto;
background: #1d1f21; background: #1d1f21;
color: #c5c8c6; color: #c5c8c6;
padding: 0.5em;
-webkit-text-size-adjust: none;
} }
.coffeescript .javascript, .coffeescript .javascript,

View File

@ -1,4 +1,5 @@
use crate::errors::*; use crate::errors::*;
use log::{debug, trace};
use std::convert::Into; use std::convert::Into;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Write; use std::io::Write;

View File

@ -4,11 +4,13 @@ pub mod fs;
mod string; mod string;
pub(crate) mod toml_ext; pub(crate) mod toml_ext;
use crate::errors::Error; 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 regex::Regex;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Write; use std::fmt::Write;
use std::path::Path; 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" /// Generate an ID for use with anchors which is derived from a "normalised"
/// string. /// 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 { pub fn id_from_content(content: &str) -> String {
let mut content = content.to_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 // Remove spaces and hashes indicating a header
let trimmed = content.trim().trim_start_matches('#').trim(); let trimmed = content.trim().trim_start_matches('#').trim();
normalize_id(trimmed) 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. /// Fix links to the correct location.
/// ///
/// This adjusts links, such as turning `.md` extensions to `.html`. /// 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 p = new_cmark_parser(text, curly_quotes);
let events = p let events = p
.map(clean_codeblock_headers) .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); html::push_html(&mut s, events);
s 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<'_> { fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
match event { match event {
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info))) => { Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info))) => {
@ -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("&lt;"),
b'>' => escaped.push_str("&gt;"),
_ => unreachable!(),
}
s = &s[next + 1..];
}
escaped.push_str(s);
escaped
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::bracket_escape;
mod render_markdown { mod render_markdown {
use super::super::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] #[test]
fn it_can_keep_quotes_straight() { fn it_can_keep_quotes_straight() {
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n"); assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
@ -332,8 +406,9 @@ more text with spaces
} }
} }
mod html_munging { #[allow(deprecated)]
use super::super::{id_from_content, normalize_id}; mod id_from_content {
use super::super::id_from_content;
#[test] #[test]
fn it_generates_anchors() { fn it_generates_anchors() {
@ -361,6 +436,10 @@ more text with spaces
); );
assert_eq!(id_from_content("## Über"), "Über"); assert_eq!(id_from_content("## Über"), "Über");
} }
}
mod html_munging {
use super::super::{normalize_id, unique_id_from_content};
#[test] #[test]
fn it_normalizes_ids() { fn it_normalizes_ids() {
@ -379,5 +458,38 @@ more text with spaces
assert_eq!(normalize_id("한국어"), "한국어"); assert_eq!(normalize_id("한국어"), "한국어");
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("<"), "&lt;");
assert_eq!(bracket_escape(">"), "&gt;");
assert_eq!(bracket_escape("<>"), "&lt;&gt;");
assert_eq!(bracket_escape("<test>"), "&lt;test&gt;");
assert_eq!(bracket_escape("a<test>b"), "a&lt;test&gt;b");
} }
} }

View File

@ -1,3 +1,4 @@
use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use std::ops::Bound::{Excluded, Included, Unbounded}; use std::ops::Bound::{Excluded, Included, Unbounded};
use std::ops::RangeBounds; use std::ops::RangeBounds;
@ -122,6 +123,7 @@ mod tests {
}; };
#[test] #[test]
#[allow(clippy::reversed_empty_ranges)] // Intentionally checking that those are correctly handled
fn take_lines_test() { fn take_lines_test() {
let s = "Lorem\nipsum\ndolor\nsit\namet"; let s = "Lorem\nipsum\ndolor\nsit\namet";
assert_eq!(take_lines(s, 1..3), "ipsum\ndolor"); assert_eq!(take_lines(s, 1..3), "ipsum\ndolor");
@ -163,6 +165,7 @@ mod tests {
} }
#[test] #[test]
#[allow(clippy::reversed_empty_ranges)] // Intentionally checking that those are correctly handled
fn take_rustdoc_include_lines_test() { fn take_rustdoc_include_lines_test() {
let s = "Lorem\nipsum\ndolor\nsit\namet"; let s = "Lorem\nipsum\ndolor\nsit\namet";
assert_eq!( assert_eq!(

View File

@ -1,7 +1,6 @@
use crate::cli::cmd::mdbook_cmd;
use crate::dummy_book::DummyBook; use crate::dummy_book::DummyBook;
use assert_cmd::Command;
#[test] #[test]
fn mdbook_cli_dummy_book_generates_index_html() { fn mdbook_cli_dummy_book_generates_index_html() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
@ -9,7 +8,7 @@ fn mdbook_cli_dummy_book_generates_index_html() {
// doesn't exist before // doesn't exist before
assert!(!temp.path().join("book").exists()); 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.arg("build").current_dir(temp.path());
cmd.assert() cmd.assert()
.success() .success()

7
tests/cli/cmd.rs Normal file
View File

@ -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
}

View File

@ -1,2 +1,3 @@
mod build; mod build;
mod cmd;
mod test; mod test;

View File

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

View File

@ -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]()

View File

@ -0,0 +1 @@
# Chapter 1

View File

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

View File

@ -0,0 +1,9 @@
# Duplicate headers
This page validates behaviour of duplicate headers.
# Header Text
# Header Text
# header-text

View File

@ -1,3 +1,5 @@
Capybara capybara capybara. Capybara capybara capybara.
Capybara capybara capybara. Capybara capybara capybara.
ThisLongWordIsIncludedSoWeCanCheckThatSufficientlyLongWordsAreOmittedFromTheSearchIndex.

View File

@ -0,0 +1,6 @@
# Summary formatting tests
- [*Italic* `code` \*escape\* \`escape2\`](formatted-summary.md)
- [Soft
line break](soft.md)
- [\<escaped tag\>](escaped-tag.md)

View File

@ -17,6 +17,7 @@ use std::ffi::OsStr;
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::str::FromStr;
use tempfile::Builder as TempFileBuilder; use tempfile::Builder as TempFileBuilder;
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
@ -35,6 +36,7 @@ const TOC_SECOND_LEVEL: &[&str] = &[
"1.4. Markdown", "1.4. Markdown",
"1.5. Unicode", "1.5. Unicode",
"1.6. No Headers", "1.6. No Headers",
"1.7. Duplicate Headers",
"2.1. Nested Chapter", "2.1. Nested Chapter",
]; ];
@ -150,6 +152,25 @@ fn rendered_code_has_playground_stuff() {
assert_contains_strings(book_js, &[".playground"]); 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] #[test]
fn anchors_include_text_between_but_not_anchor_comments() { fn anchors_include_text_between_but_not_anchor_comments() {
let temp = DummyBook::new().build().unwrap(); 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); 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] #[test]
fn theme_dir_overrides_work_correctly() { fn theme_dir_overrides_work_correctly() {
let book_dir = dummy_book::new_copy_of_example_book().unwrap(); 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> &lt;escaped tag&gt;</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(),
"# &lt;escaped tag&gt;\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")] #[cfg(feature = "search")]
mod search { mod search {
use crate::dummy_book::DummyBook; use crate::dummy_book::DummyBook;
@ -633,11 +756,12 @@ mod search {
let some_section = get_doc_ref("first/index.html#some-section"); let some_section = get_doc_ref("first/index.html#some-section");
let summary = get_doc_ref("first/includes.html#summary"); let summary = get_doc_ref("first/includes.html#summary");
let no_headers = get_doc_ref("first/no-headers.html"); let 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 conclusion = get_doc_ref("conclusion.html#conclusion");
let bodyidx = &index["index"]["index"]["body"]["root"]; let bodyidx = &index["index"]["index"]["body"]["root"];
let textidx = &bodyidx["t"]["e"]["x"]["t"]; 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"][&first_chapter]["tf"], 1.0);
assert_eq!(textidx["docs"][&introduction]["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[&some_section]["body"], "");
assert_eq!( assert_eq!(
docs[&summary]["body"], 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!( assert_eq!(
docs[&summary]["breadcrumbs"], docs[&summary]["breadcrumbs"],
@ -657,9 +781,13 @@ mod search {
docs[&no_headers]["breadcrumbs"], docs[&no_headers]["breadcrumbs"],
"First Chapter » No Headers" "First Chapter » No Headers"
); );
assert_eq!(
docs[&duplicate_headers_1]["breadcrumbs"],
"First Chapter » Duplicate Headers » Header Text"
);
assert_eq!( assert_eq!(
docs[&no_headers]["body"], 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

View File

@ -24,3 +24,24 @@ fn mdbook_detects_book_with_failing_tests() {
assert!(md.test(vec![]).is_err()); 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());
}