diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 39624230..d5f1add2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,7 +31,8 @@ jobs: rust: stable - build: msrv os: ubuntu-latest - rust: 1.46.0 + # sync MSRV with docs: guide/src/guide/installation.md + rust: 1.56.0 steps: - uses: actions/checkout@master - name: Install Rust diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b3b6a2d..686004c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,91 @@ # Changelog +## mdBook 0.4.21 +[92afe9b...8f01d02](https://github.com/rust-lang/mdBook/compare/92afe9b...8f01d02) + +### Fixed +- Fixed an issue where mdBook would fail to compile with Rust nightly-2022-07-22. + [#1861](https://github.com/rust-lang/mdBook/pull/1861) + +## mdBook 0.4.20 +[53055e0...da166e0](https://github.com/rust-lang/mdBook/compare/53055e0...da166e0) + +### Fixed +- Fixed a regression in 0.4.19 where inline code would have excessive padding + in some situations such as headings. + [#1855](https://github.com/rust-lang/mdBook/pull/1855) + +## mdBook 0.4.19 +[ae275ad...53055e0](https://github.com/rust-lang/mdBook/compare/ae275ad...53055e0) + +### Added +- The `serve` command now supports HEAD requests. + [#1825](https://github.com/rust-lang/mdBook/pull/1825) + +### Changed +- An error is now generated when a custom theme directory does not exist. + [#1791](https://github.com/rust-lang/mdBook/pull/1791) +- Very wide tables now have independent horizontal scrolling so that scrolling + to see the rest of the table will not scroll the entire page. + [#1617](https://github.com/rust-lang/mdBook/pull/1617) +- The buttons on code blocks are now only shown when the mouse cursor hovers + over them (or tapped on mobile). There is also some extra spacing to reduce + the overlap with the code. + [#1806](https://github.com/rust-lang/mdBook/pull/1806) +- The first chapter always generates an `index.html` file. Previously it would + only generate the index file for prefix chapters. + [#1829](https://github.com/rust-lang/mdBook/pull/1829) + +### Fixed +- `mdbook serve --open` now properly handles the case if the first chapter is a draft. + [#1714](https://github.com/rust-lang/mdBook/pull/1714) + [#1830](https://github.com/rust-lang/mdBook/pull/1830) +- Very long words (over 80 characters) are no longer indexed to avoid a stack overflow. + [#1833](https://github.com/rust-lang/mdBook/pull/1833) + +## mdBook 0.4.18 +[981b79b...ae275ad](https://github.com/rust-lang/mdBook/compare/981b79b...ae275ad) + +### Fixed +- Fixed rendering of SUMMARY links that contain markdown escapes or other + markdown elements. + [#1785](https://github.com/rust-lang/mdBook/pull/1785) + +## mdBook 0.4.17 +[a5fddfa...981b79b](https://github.com/rust-lang/mdBook/compare/a5fddfa...981b79b) + +### Fixed +- Fixed parsing of `output.html.print` configuration table. + [#1775](https://github.com/rust-lang/mdBook/pull/1775) + +## mdBook 0.4.16 +[68a5c09...a5fddfa](https://github.com/rust-lang/mdBook/compare/68a5c09...a5fddfa) + +### Added +- Added `output.html.print.page-break` config option to control whether or not + there is a page break between chapters in the print output. + [#1728](https://github.com/rust-lang/mdBook/pull/1728) +- Added `output.html.playground.runnable` config option to globally disable + the run button in code blocks. + [#1546](https://github.com/rust-lang/mdBook/pull/1546) + +### Changed +- The `mdbook serve` live reload websocket now uses the protocol, host, and + port of the current page, allowing access through a proxy. + [#1771](https://github.com/rust-lang/mdBook/pull/1771) +- The 404 not-found page now includes the books title in the HTML title tag. + [#1693](https://github.com/rust-lang/mdBook/pull/1693) +- Migrated to clap 3.0 which which handles CLI option parsing. + [#1731](https://github.com/rust-lang/mdBook/pull/1731) + +### Fixed +- Minor fixes to the markdown parser. + [#1729](https://github.com/rust-lang/mdBook/pull/1729) +- Fixed incorrect parsing in `SUMMARY.md` when it didn't start with a title. + [#1744](https://github.com/rust-lang/mdBook/pull/1744) +- Fixed duplicate anchor IDs for links in search results. + [#1749](https://github.com/rust-lang/mdBook/pull/1749) + ## mdBook 0.4.15 [5eb7d46...68a5c09](https://github.com/rust-lang/mdBook/compare/5eb7d46...68a5c09) diff --git a/Cargo.lock b/Cargo.lock index bf6c881d..10fae527 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "ansi_term" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ "winapi 0.3.9", ] @@ -185,17 +185,27 @@ dependencies = [ [[package]] name = "clap" -version = "2.33.3" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "7a30c3bf9ff12dfe5dae53f0a96e0febcd18420d1c0e7fad77796d9d5c4b5375" dependencies = [ - "ansi_term", "atty", "bitflags", + "indexmap", + "lazy_static", + "os_str_bytes", "strsim", + "termcolor", "textwrap", - "unicode-width", - "vec_map", +] + +[[package]] +name = "clap_complete" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d044e9db8cd0f68191becdeb5246b7462e4cf0c069b19ae00d1bf3fa9889498d" +dependencies = [ + "clap", ] [[package]] @@ -218,10 +228,10 @@ dependencies = [ ] [[package]] -name = "difference" -version = "2.0.0" +name = "diff" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" [[package]] name = "difflib" @@ -261,24 +271,21 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "elasticlunr-rs" -version = "2.3.13" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "515a402b5acb08002194dd926065be7733003bb37ac0f030dfd39160028238e1" +checksum = "e6dae5cac90640734ee881bc5f21b6e5123f4e5235e52428db114abffc2391d6" dependencies = [ - "lazy_static", "regex", "serde", "serde_derive", "serde_json", - "strum", - "strum_macros", ] [[package]] name = "env_logger" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" dependencies = [ "atty", "humantime", @@ -375,25 +382,11 @@ dependencies = [ "new_debug_unreachable", ] -[[package]] -name = "futures" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" -version = "0.3.16" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -401,15 +394,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.16" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" - -[[package]] -name = "futures-io" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-macro" @@ -426,9 +413,9 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.16" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" @@ -538,7 +525,7 @@ dependencies = [ "log", "pest", "pest_derive", - "quick-error 2.0.1", + "quick-error", "serde", "serde_json", ] @@ -574,15 +561,6 @@ dependencies = [ "http", ] -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "hermit-abi" version = "0.1.19" @@ -642,12 +620,9 @@ checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" [[package]] name = "humantime" -version = "1.3.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -dependencies = [ - "quick-error 1.2.3", -] +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" @@ -714,15 +689,6 @@ dependencies = [ "libc", ] -[[package]] -name = "input_buffer" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413" -dependencies = [ - "bytes", -] - [[package]] name = "iovec" version = "0.1.4" @@ -830,13 +796,14 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "mdbook" -version = "0.4.15" +version = "0.4.21" dependencies = [ "ammonia", "anyhow", "assert_cmd", "chrono", "clap", + "clap_complete", "elasticlunr-rs", "env_logger", "futures-util", @@ -854,7 +821,6 @@ dependencies = [ "select", "semver", "serde", - "serde_derive", "serde_json", "shlex", "tempfile", @@ -1053,6 +1019,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + [[package]] name = "output_vt100" version = "0.1.2" @@ -1225,13 +1200,13 @@ dependencies = [ [[package]] name = "pretty_assertions" -version = "0.6.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" +checksum = "c89f989ac94207d048d92db058e4f6ec7342b0971fc58d1271ca148b799b3563" dependencies = [ "ansi_term", "ctor", - "difference", + "diff", "output_vt100", ] @@ -1267,12 +1242,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quick-error" version = "2.0.1" @@ -1463,6 +1432,9 @@ name = "serde" version = "1.0.129" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1f72836d2aa753853178eda473a3b9d8e4eefdaf20523b919677e6de489f8f1" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" @@ -1578,27 +1550,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "strum" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" - -[[package]] -name = "strum_macros" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" @@ -1647,11 +1601,28 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.11.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ - "unicode-width", + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1681,11 +1652,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.10.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" +checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" dependencies = [ - "autocfg", "bytes", "libc", "memchr", @@ -1698,9 +1668,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.3.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", @@ -1720,9 +1690,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.13.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1a5f475f1b9d077ea1017ecbc60890fda8e54942d680ca0b1d2b47cfa2d861b" +checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8" dependencies = [ "futures-util", "log", @@ -1801,19 +1771,19 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "tungstenite" -version = "0.12.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24" +checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" dependencies = [ "base64", "byteorder", "bytes", "http", "httparse", - "input_buffer", "log", "rand 0.8.4", "sha-1 0.9.7", + "thiserror", "url", "utf-8", ] @@ -1854,18 +1824,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" - -[[package]] -name = "unicode-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" - [[package]] name = "unicode-xid" version = "0.2.2" @@ -1890,12 +1848,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.3" @@ -1934,12 +1886,13 @@ dependencies = [ [[package]] name = "warp" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332d47745e9a0c38636dbd454729b147d16bd1ed08ae67b3ab281c4506771054" +checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e" dependencies = [ "bytes", - "futures", + "futures-channel", + "futures-util", "headers", "http", "hyper", diff --git a/Cargo.toml b/Cargo.toml index 6e225ea6..b1c095d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "mdbook" -version = "0.4.15" +version = "0.4.21" authors = [ "Mathieu David ", "Michael-F-Bryan ", "Matt Ickstadt " ] documentation = "http://rust-lang.github.io/mdBook/index.html" -edition = "2018" +edition = "2021" exclude = ["/guide/*"] keywords = ["book", "gitbook", "rustbook", "markdown"] license = "MPL-2.0" @@ -18,8 +18,9 @@ description = "Creates a book from markdown files" [dependencies] anyhow = "1.0.28" chrono = "0.4" -clap = "2.24" -env_logger = "0.7.1" +clap = { version = "3.0", features = ["cargo"] } +clap_complete = "3.0" +env_logger = "0.9.0" handlebars = "4.0" lazy_static = "1.0" log = "0.4" @@ -27,8 +28,7 @@ memchr = "2.0" opener = "0.5" pulldown-cmark = { version = "0.9.1", default-features = false } regex = "1.5.5" -serde = "1.0" -serde_derive = "1.0" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" shlex = "1" tempfile = "3.0" @@ -42,10 +42,10 @@ gitignore = { version = "1.0", optional = true } # Serve feature futures-util = { version = "0.3.4", optional = true } tokio = { version = "1", features = ["macros", "rt-multi-thread"], optional = true } -warp = { version = "0.3.1", default-features = false, features = ["websocket"], optional = true } +warp = { version = "0.3.2", default-features = false, features = ["websocket"], optional = true } # Search feature -elasticlunr-rs = { version = "2.3", optional = true, default-features = false } +elasticlunr-rs = { version = "3.0.0", optional = true } ammonia = { version = "3", optional = true } [dev-dependencies] @@ -53,7 +53,7 @@ assert_cmd = "1" predicates = "2" select = "0.5" semver = "1.0" -pretty_assertions = "0.6" +pretty_assertions = "1.2.1" walkdir = "2.0" [features] diff --git a/examples/nop-preprocessor.rs b/examples/nop-preprocessor.rs index 486fd86d..ace40093 100644 --- a/examples/nop-preprocessor.rs +++ b/examples/nop-preprocessor.rs @@ -1,5 +1,5 @@ use crate::nop_lib::Nop; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, Arg, ArgMatches}; use mdbook::book::Book; use mdbook::errors::Error; use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext}; @@ -7,12 +7,12 @@ use semver::{Version, VersionReq}; use std::io; use std::process; -pub fn make_app() -> App<'static, 'static> { +pub fn make_app() -> App<'static> { App::new("nop-preprocessor") .about("A mdbook preprocessor which does precisely nothing") .subcommand( - SubCommand::with_name("supports") - .arg(Arg::with_name("renderer").required(true)) + App::new("supports") + .arg(Arg::new("renderer").required(true)) .about("Check whether a renderer is supported by this preprocessor"), ) } diff --git a/guide/src/cli/test.md b/guide/src/cli/test.md index e134dc9b..a542f3ce 100644 --- a/guide/src/cli/test.md +++ b/guide/src/cli/test.md @@ -43,7 +43,7 @@ mdbook test path/to/book The `--library-path` (`-L`) option allows you to add directories to the library search path used by `rustdoc` when it builds and tests the examples. Multiple directories can be specified with multiple options (`-L foo -L bar`) or with a -comma-delimited list (`-L foo,bar`). 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 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`: @@ -61,3 +61,8 @@ The `--dest-dir` (`-d`) option allows you to change the output directory for the book. Relative paths are interpreted relative to the book's root directory. If not specified it will default to the value of the `build.build-dir` key in `book.toml`, or to `./book`. + +#### --chapter + +The `--chapter` (`-c`) option allows you to test a specific chapter of the +book using the chapter name or the relative path to the chapter. \ No newline at end of file diff --git a/guide/src/continuous-integration.md b/guide/src/continuous-integration.md index 3503445f..c39cb010 100644 --- a/guide/src/continuous-integration.md +++ b/guide/src/continuous-integration.md @@ -21,7 +21,7 @@ A simple approach would be to use the popular `curl` CLI tool to download the ex ```sh mkdir bin -curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.15/mdbook-v0.4.15-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin +curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.21/mdbook-v0.4.21-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin bin/mdbook build ``` diff --git a/guide/src/format/configuration/general.md b/guide/src/format/configuration/general.md index b1fe49e4..a00247ec 100644 --- a/guide/src/format/configuration/general.md +++ b/guide/src/format/configuration/general.md @@ -7,7 +7,7 @@ Here is an example of what a ***book.toml*** file might look like: ```toml [book] title = "Example book" -author = "John Doe" +authors = ["John Doe"] description = "The example book covers examples." [rust] diff --git a/guide/src/format/configuration/renderers.md b/guide/src/format/configuration/renderers.md index 1aa1eef9..b9c30861 100644 --- a/guide/src/format/configuration/renderers.md +++ b/guide/src/format/configuration/renderers.md @@ -157,7 +157,8 @@ The following configuration options are available: Defaults to `404.md`. - **site-url:** The url where the book will be hosted. This is required to ensure navigation links and script/css imports in the 404 file work correctly, even when accessing - urls in subdirectories. Defaults to `/`. + urls in subdirectories. Defaults to `/`. If `site-url` is set, + make sure to use document relative links for your assets, meaning they should not start with `/`. - **cname:** The DNS subdomain or apex domain at which your book will be hosted. This string will be written to a file named CNAME in the root of your site, as required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages @@ -207,6 +208,7 @@ editable = false # allows editing the source code copyable = true # include the copy button for copying code snippets copy-js = true # includes the JavaScript for the code editor line-numbers = false # displays line numbers for editable code +runnable = true # displays a run button for rust code ``` - **editable:** Allow editing the source code. Defaults to `false`. @@ -214,6 +216,7 @@ line-numbers = false # displays line numbers for editable code - **copy-js:** Copy JavaScript files for the editor to the output directory. Defaults to `true`. - **line-numbers** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`. +- **runnable** Displays a run button for rust code snippets. Changing this to `false` will disable the run in playground feature globally. Defaults to `true`. [Ace]: https://ace.c9.io/ diff --git a/guide/src/format/mdbook.md b/guide/src/format/mdbook.md index e4c76f1b..62e89843 100644 --- a/guide/src/format/mdbook.md +++ b/guide/src/format/mdbook.md @@ -41,7 +41,7 @@ println!("Hello, World!"); If there is no `main` function, then the code is automatically wrapped inside one. -If you wish to disable the play button, you can include the `noplayground` option on the code block like this: +If you wish to disable the play button for a code block, you can include the `noplayground` option on the code block like this: ~~~markdown ```rust,noplayground @@ -51,6 +51,13 @@ println!("Hello {}!", name); ``` ~~~ +Or, if you wish to disable the play button for all code blocks in your book, you can write the config to the `book.toml` like this. + +```toml +[output.html.playground] +runnable = false +``` + ## Rust code block attributes Additional attributes can be included in Rust code blocks with comma, space, or tab-separated terms just after the language term. For example: diff --git a/guide/src/guide/creating.md b/guide/src/guide/creating.md index 6e0df0a8..f68a8c60 100644 --- a/guide/src/guide/creating.md +++ b/guide/src/guide/creating.md @@ -97,7 +97,7 @@ So if you have images or other static files, just include them somewhere in the Once you've written your book, you may want to host it somewhere for others to view. The first step is to build the output of the book. -This can be done with the `mbdook build` command in the same directory where the `book.toml` file is located: +This can be done with the `mdbook build` command in the same directory where the `book.toml` file is located: ```sh mdbook build @@ -106,4 +106,4 @@ mdbook build 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. -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. \ No newline at end of file diff --git a/guide/src/guide/installation.md b/guide/src/guide/installation.md index b68f807a..128b1047 100644 --- a/guide/src/guide/installation.md +++ b/guide/src/guide/installation.md @@ -20,7 +20,7 @@ To make it easier to run, put the path to the binary into your `PATH`. To build the `mdbook` executable from source, you will first need to install Rust and Cargo. Follow the instructions on the [Rust installation page]. -mdBook currently requires at least Rust version 1.46. +mdBook currently requires at least Rust version 1.54. Once you have installed Rust, the following command can be used to build and install mdBook: @@ -30,6 +30,8 @@ cargo install mdbook This will automatically download mdBook from [crates.io], build it, and install it in Cargo's global binary directory (`~/.cargo/bin/` by default). +To uninstall, run the command `cargo uninstall mdbook`. + [Rust installation page]: https://www.rust-lang.org/tools/install [crates.io]: https://crates.io/ diff --git a/src/book/book.rs b/src/book/book.rs index da2a0a3c..b46843df 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -7,6 +7,9 @@ use std::path::{Path, PathBuf}; use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; use crate::config::BuildConfig; use crate::errors::*; +use crate::utils::bracket_escape; +use log::debug; +use serde::{Deserialize, Serialize}; /// Load a book into memory from its `src/` directory. pub fn load_book>(src_dir: P, cfg: &BuildConfig) -> Result { @@ -53,7 +56,7 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> { let mut f = File::create(&filename).with_context(|| { format!("Unable to create missing file: {}", filename.display()) })?; - writeln!(f, "# {}", link.name)?; + writeln!(f, "# {}", bracket_escape(&link.name))?; } } diff --git a/src/book/init.rs b/src/book/init.rs index 264c113d..dd3fa8b0 100644 --- a/src/book/init.rs +++ b/src/book/init.rs @@ -6,6 +6,7 @@ use super::MDBook; use crate::config::Config; use crate::errors::*; use crate::theme; +use log::{debug, error, info, trace}; /// A helper for setting up a new book and its directory structure. #[derive(Debug, Clone, PartialEq)] diff --git a/src/book/mod.rs b/src/book/mod.rs index 3370d92c..5ec64d7c 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -14,6 +14,7 @@ pub use self::book::{load_book, Book, BookItem, BookItems, Chapter}; pub use self::init::BookBuilder; pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; +use log::{debug, error, info, log_enabled, trace, warn}; use std::io::Write; use std::path::PathBuf; use std::process::Command; @@ -246,6 +247,13 @@ impl MDBook { /// Run `rustdoc` tests on the book, linking against the provided libraries. pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> { + // test_chapter with chapter:None will run all tests. + self.test_chapter(library_paths, None) + } + + /// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries. + /// If `chapter` is `None`, all tests will be run. + pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> { let library_args: Vec<&str> = (0..library_paths.len()) .map(|_| "-L") .zip(library_paths.into_iter()) @@ -254,6 +262,8 @@ impl MDBook { let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?; + let mut chapter_found = false; + // FIXME: Is "test" the proper renderer name to use here? let preprocess_context = PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string()); @@ -270,8 +280,16 @@ impl MDBook { _ => continue, }; - let path = self.source_dir().join(&chapter_path); - info!("Testing file: {:?}", path); + if let Some(chapter) = chapter { + if ch.name != chapter && chapter_path.to_str() != Some(chapter) { + if chapter == "?" { + info!("Skipping chapter '{}'...", ch.name); + } + continue; + } + } + chapter_found = true; + info!("Testing chapter '{}': {:?}", ch.name, chapter_path); // write preprocessed file to tempdir let path = temp_dir.path().join(&chapter_path); @@ -311,6 +329,11 @@ impl MDBook { if failed { bail!("One or more tests failed"); } + if let Some(chapter) = chapter { + if !chapter_found { + bail!("Chapter not found: {}", chapter); + } + } Ok(()) } @@ -386,7 +409,7 @@ fn determine_renderers(config: &Config) -> Vec> { renderers } -const DEFAULT_PREPROCESSORS: &[&'static str] = &["links", "index"]; +const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"]; fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool { let name = pre.name(); @@ -756,10 +779,9 @@ mod tests { let preprocessors = determine_preprocessors(&cfg).unwrap(); - assert!(preprocessors + assert!(!preprocessors .iter() - .find(|preprocessor| preprocessor.name() == "random") - .is_none()); + .any(|preprocessor| preprocessor.name() == "random")); } #[test] @@ -776,10 +798,9 @@ mod tests { let preprocessors = determine_preprocessors(&cfg).unwrap(); - assert!(preprocessors + assert!(!preprocessors .iter() - .find(|preprocessor| preprocessor.name() == "links") - .is_none()); + .any(|preprocessor| preprocessor.name() == "links")); } #[test] diff --git a/src/book/summary.rs b/src/book/summary.rs index 1ade05ec..02f67c6f 100644 --- a/src/book/summary.rs +++ b/src/book/summary.rs @@ -1,6 +1,8 @@ use crate::errors::*; +use log::{debug, trace, warn}; use memchr::{self, Memchr}; use pulldown_cmark::{self, Event, HeadingLevel, Tag}; +use serde::{Deserialize, Serialize}; use std::fmt::{self, Display, Formatter}; use std::iter::FromIterator; use std::ops::{Deref, DerefMut}; @@ -536,6 +538,10 @@ impl<'a> SummaryParser<'a> { // Skip a HTML element such as a comment line. Some(Event::Html(_)) => {} // Otherwise, no title. + Some(ev) => { + self.back(ev); + return None; + } _ => return None, } } @@ -647,6 +653,18 @@ mod tests { assert_eq!(got, should_be); } + #[test] + fn no_initial_title() { + let src = "[Link]()"; + let mut parser = SummaryParser::new(src); + + assert!(parser.parse_title().is_none()); + assert!(matches!( + parser.next_event(), + Some(Event::Start(Tag::Paragraph)) + )); + } + #[test] fn parse_title_with_styling() { let src = "# My **Awesome** Summary"; diff --git a/src/cmd/build.rs b/src/cmd/build.rs index d1c66302..5fe73236 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -1,22 +1,28 @@ use crate::{get_book_dir, open}; -use clap::{App, ArgMatches, SubCommand}; +use clap::{arg, App, Arg, ArgMatches}; use mdbook::errors::Result; use mdbook::MDBook; // Create clap subcommand arguments -pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { - SubCommand::with_name("build") +pub fn make_subcommand<'help>() -> App<'help> { + App::new("build") .about("Builds a book from its markdown files") - .arg_from_usage( - "-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ - Relative paths are interpreted relative to the book's root directory.{n}\ - If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", + .arg( + Arg::new("dest-dir") + .short('d') + .long("dest-dir") + .value_name("dest-dir") + .help( + "Output directory for the book{n}\ + Relative paths are interpreted relative to the book's root directory.{n}\ + If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.", + ), ) - .arg_from_usage( - "[dir] 'Root directory for the book{n}\ - (Defaults to the Current Directory when omitted)'", - ) - .arg_from_usage("-o, --open 'Opens the compiled book in a web browser'") + .arg(arg!([dir] + "Root directory for the book{n}\ + (Defaults to the Current Directory when omitted)" + )) + .arg(arg!(-o --open "Opens the compiled book in a web browser")) } // Build command implementation @@ -32,7 +38,12 @@ pub fn execute(args: &ArgMatches) -> Result<()> { if args.is_present("open") { // FIXME: What's the right behaviour if we don't use the HTML renderer? - open(book.build_dir_for("html").join("index.html")); + let path = book.build_dir_for("html").join("index.html"); + if !path.exists() { + error!("No chapter available to open"); + std::process::exit(1) + } + open(path); } Ok(()) diff --git a/src/cmd/clean.rs b/src/cmd/clean.rs index b58f937e..0569726e 100644 --- a/src/cmd/clean.rs +++ b/src/cmd/clean.rs @@ -1,23 +1,28 @@ use crate::get_book_dir; use anyhow::Context; -use clap::{App, ArgMatches, SubCommand}; +use clap::{arg, App, Arg, ArgMatches}; use mdbook::MDBook; use std::fs; // Create clap subcommand arguments -pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { - SubCommand::with_name("clean") +pub fn make_subcommand<'help>() -> App<'help> { + App::new("clean") .about("Deletes a built book") - .arg_from_usage( - "-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ - Relative paths are interpreted relative to the book's root directory.{n}\ - Running this command deletes this directory.{n}\ - If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", - ) - .arg_from_usage( - "[dir] 'Root directory for the book{n}\ - (Defaults to the Current Directory when omitted)'", + .arg( + Arg::new("dest-dir") + .short('d') + .long("dest-dir") + .value_name("dest-dir") + .help( + "Output directory for the book{n}\ + Relative paths are interpreted relative to the book's root directory.{n}\ + If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.", + ), ) + .arg(arg!([dir] + "Root directory for the book{n}\ + (Defaults to the Current Directory when omitted)" + )) } // Clean command implementation diff --git a/src/cmd/init.rs b/src/cmd/init.rs index ed0aa17d..c964dcc1 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -1,5 +1,5 @@ use crate::get_book_dir; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{arg, App, Arg, ArgMatches}; use mdbook::config; use mdbook::errors::Result; use mdbook::MDBook; @@ -8,25 +8,25 @@ use std::io::Write; use std::process::Command; // Create clap subcommand arguments -pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { - SubCommand::with_name("init") +pub fn make_subcommand<'help>() -> App<'help> { + App::new("init") .about("Creates the boilerplate structure and files for a new book") // the {n} denotes a newline which will properly aligned in all help messages - .arg_from_usage( - "[dir] 'Directory to create the book in{n}\ - (Defaults to the Current Directory when omitted)'", - ) - .arg_from_usage("--theme 'Copies the default theme into your source folder'") - .arg_from_usage("--force 'Skips confirmation prompts'") + .arg(arg!([dir] + "Directory to create the book in{n}\ + (Defaults to the Current Directory when omitted)" + )) + .arg(arg!(--theme "Copies the default theme into your source folder")) + .arg(arg!(--force "Skips confirmation prompts")) .arg( - Arg::with_name("title") + Arg::new("title") .long("title") .takes_value(true) .help("Sets the book title") .required(false), ) .arg( - Arg::with_name("ignore") + Arg::new("ignore") .long("ignore") .takes_value(true) .possible_values(&["none", "git"]) @@ -122,8 +122,5 @@ fn confirm() -> bool { io::stdout().flush().unwrap(); let mut s = String::new(); io::stdin().read_line(&mut s).ok(); - match &*s.trim() { - "Y" | "y" | "yes" | "Yes" => true, - _ => false, - } + matches!(&*s.trim(), "Y" | "y" | "yes" | "Yes") } diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index c5394f8a..00eaa46b 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -1,7 +1,7 @@ #[cfg(feature = "watch")] use super::watch; use crate::{get_book_dir, open}; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{arg, App, Arg, ArgMatches}; use futures_util::sink::SinkExt; use futures_util::StreamExt; use mdbook::errors::*; @@ -18,37 +18,43 @@ use warp::Filter; const LIVE_RELOAD_ENDPOINT: &str = "__livereload"; // Create clap subcommand arguments -pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { - SubCommand::with_name("serve") +pub fn make_subcommand<'help>() -> App<'help> { + App::new("serve") .about("Serves a book at http://localhost:3000, and rebuilds it on changes") - .arg_from_usage( - "-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ - Relative paths are interpreted relative to the book's root directory.{n}\ - If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", - ) - .arg_from_usage( - "[dir] 'Root directory for the book{n}\ - (Defaults to the Current Directory when omitted)'", - ) .arg( - Arg::with_name("hostname") - .short("n") + Arg::new("dest-dir") + .short('d') + .long("dest-dir") + .value_name("dest-dir") + .help( + "Output directory for the book{n}\ + Relative paths are interpreted relative to the book's root directory.{n}\ + If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.", + ), + ) + .arg(arg!([dir] + "Root directory for the book{n}\ + (Defaults to the Current Directory when omitted)" + )) + .arg( + Arg::new("hostname") + .short('n') .long("hostname") .takes_value(true) .default_value("localhost") - .empty_values(false) + .forbid_empty_values(true) .help("Hostname to listen on for HTTP connections"), ) .arg( - Arg::with_name("port") - .short("p") + Arg::new("port") + .short('p') .long("port") .takes_value(true) .default_value("3000") - .empty_values(false) + .forbid_empty_values(true) .help("Port to use for HTTP connections"), ) - .arg_from_usage("-o, --open 'Opens the book server in a web browser'") + .arg(arg!(-o --open "Opens the compiled book in a web browser")) } // Serve command implementation @@ -62,11 +68,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> { let address = format!("{}:{}", hostname, port); - let livereload_url = format!("ws://{}/{}", address, LIVE_RELOAD_ENDPOINT); let update_config = |book: &mut MDBook| { book.config - .set("output.html.livereload-url", &livereload_url) - .expect("livereload-url update failed"); + .set("output.html.live-reload-endpoint", &LIVE_RELOAD_ENDPOINT) + .expect("live-reload-endpoint update failed"); if let Some(dest_dir) = args.value_of("dest-dir") { book.config.build.build_dir = dest_dir.into(); } @@ -84,8 +89,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { let input_404 = book .config .get("output.html.input-404") - .map(toml::Value::as_str) - .and_then(std::convert::identity) // flatten + .and_then(toml::Value::as_str) .map(ToString::to_string); let file_404 = get_404_output_file(&input_404); diff --git a/src/cmd/test.rs b/src/cmd/test.rs index f6d97aa6..f5ca3ee4 100644 --- a/src/cmd/test.rs +++ b/src/cmd/test.rs @@ -1,29 +1,47 @@ use crate::get_book_dir; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{arg, App, Arg, ArgMatches}; use mdbook::errors::Result; use mdbook::MDBook; // Create clap subcommand arguments -pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { - SubCommand::with_name("test") +pub fn make_subcommand<'help>() -> App<'help> { + App::new("test") .about("Tests that a book's Rust code samples compile") - .arg_from_usage( - "-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ - Relative paths are interpreted relative to the book's root directory.{n}\ - If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", + .arg( + Arg::new("dest-dir") + .short('d') + .long("dest-dir") + .value_name("dest-dir") + .help( + "Output directory for the book{n}\ + Relative paths are interpreted relative to the book's root directory.{n}\ + If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.", + ), + ).arg( + Arg::new("chapter") + .short('c') + .long("chapter") + .value_name("chapter") + .help( + "Only test the specified chapter{n}\ + Where the name of the chapter is defined in the SUMMARY.md file.{n}\ + Use the special name \"?\" to the list of chapter names." + ) ) - .arg_from_usage( - "[dir] 'Root directory for the book{n}\ - (Defaults to the Current Directory when omitted)'", - ) - .arg(Arg::with_name("library-path") - .short("L") + .arg(arg!([dir] + "Root directory for the book{n}\ + (Defaults to the Current Directory when omitted)" + )) + .arg(Arg::new("library-path") + .short('L') .long("library-path") .value_name("dir") .takes_value(true) + .use_delimiter(true) .require_delimiter(true) - .multiple(true) - .empty_values(false) + .multiple_values(true) + .multiple_occurrences(true) + .forbid_empty_values(true) .help("A comma-separated list of directories to add to {n}the crate search path when building tests")) } @@ -33,14 +51,18 @@ pub fn execute(args: &ArgMatches) -> Result<()> { .values_of("library-path") .map(std::iter::Iterator::collect) .unwrap_or_default(); + let chapter: Option<&str> = args.value_of("chapter"); + let book_dir = get_book_dir(args); let mut book = MDBook::load(&book_dir)?; if let Some(dest_dir) = args.value_of("dest-dir") { book.config.build.build_dir = dest_dir.into(); } - - book.test(library_paths)?; + match chapter { + Some(_) => book.test_chapter(library_paths, chapter), + None => book.test(library_paths), + }?; Ok(()) } diff --git a/src/cmd/watch.rs b/src/cmd/watch.rs index b27516b0..9336af77 100644 --- a/src/cmd/watch.rs +++ b/src/cmd/watch.rs @@ -1,5 +1,5 @@ use crate::{get_book_dir, open}; -use clap::{App, ArgMatches, SubCommand}; +use clap::{arg, App, Arg, ArgMatches}; use mdbook::errors::Result; use mdbook::utils; use mdbook::MDBook; @@ -10,19 +10,25 @@ use std::thread::sleep; use std::time::Duration; // Create clap subcommand arguments -pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { - SubCommand::with_name("watch") +pub fn make_subcommand<'help>() -> App<'help> { + App::new("watch") .about("Watches a book's files and rebuilds it on changes") - .arg_from_usage( - "-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ - Relative paths are interpreted relative to the book's root directory.{n}\ - If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", + .arg( + Arg::new("dest-dir") + .short('d') + .long("dest-dir") + .value_name("dest-dir") + .help( + "Output directory for the book{n}\ + Relative paths are interpreted relative to the book's root directory.{n}\ + If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.", + ), ) - .arg_from_usage( - "[dir] 'Root directory for the book{n}\ - (Defaults to the Current Directory when omitted)'", - ) - .arg_from_usage("-o, --open 'Open the compiled book in a web browser'") + .arg(arg!([dir] + "Root directory for the book{n}\ + (Defaults to the Current Directory when omitted)" + )) + .arg(arg!(-o --open "Opens the compiled book in a web browser")) } // Watch command implementation @@ -39,7 +45,12 @@ pub fn execute(args: &ArgMatches) -> Result<()> { if args.is_present("open") { book.build()?; - open(book.build_dir_for("html").join("index.html")); + let path = book.build_dir_for("html").join("index.html"); + if !path.exists() { + error!("No chapter available to open"); + std::process::exit(1) + } + open(path); } trigger_on_change(&book, |paths, book_dir| { diff --git a/src/config.rs b/src/config.rs index 810d420e..20ac561a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -49,6 +49,7 @@ #![deny(missing_docs)] +use log::{debug, trace, warn}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::collections::HashMap; use std::env; @@ -227,10 +228,10 @@ impl Config { let value = Value::try_from(value) .with_context(|| "Unable to represent the item as a JSON Value")?; - if index.starts_with("book.") { - self.book.update_value(&index[5..], value); - } else if index.starts_with("build.") { - self.build.update_value(&index[6..], value); + if let Some(key) = index.strip_prefix("book.") { + self.book.update_value(key, value); + } else if let Some(key) = index.strip_prefix("build.") { + self.build.update_value(key, value); } else { self.rest.insert(index, value); } @@ -295,7 +296,7 @@ impl Default for Config { } } -impl<'de> Deserialize<'de> for Config { +impl<'de> serde::Deserialize<'de> for Config { fn deserialize>(de: D) -> std::result::Result { let raw = Value::deserialize(de)?; @@ -371,15 +372,8 @@ impl Serialize for Config { } fn parse_env(key: &str) -> Option { - const PREFIX: &str = "MDBOOK_"; - - if key.starts_with(PREFIX) { - let key = &key[PREFIX.len()..]; - - Some(key.to_lowercase().replace("__", ".").replace("_", "-")) - } else { - None - } + key.strip_prefix("MDBOOK_") + .map(|key| key.to_lowercase().replace("__", ".").replace('_', "-")) } fn is_legacy_format(table: &Value) -> bool { @@ -533,14 +527,14 @@ pub struct HtmlConfig { /// directly jumping to editing the currently viewed page. /// Contains {path} that is replaced with chapter source file path pub edit_url_template: Option, - /// This is used as a bit of a workaround for the `mdbook serve` command. - /// Basically, because you set the websocket port from the command line, the - /// `mdbook serve` command needs a way to let the HTML renderer know where - /// to point livereloading at, if it has been enabled. + /// Endpoint of websocket, for livereload usage. Value loaded from .toml file + /// is ignored, because our code overrides this field with the value [`LIVE_RELOAD_ENDPOINT`] + /// + /// [`LIVE_RELOAD_ENDPOINT`]: cmd::serve::LIVE_RELOAD_ENDPOINT /// /// This config item *should not be edited* by the end user. #[doc(hidden)] - pub livereload_url: Option, + pub live_reload_endpoint: Option, /// The mapping from old pages to new pages/URLs to use when generating /// redirects. pub redirect: HashMap, @@ -569,7 +563,7 @@ impl Default for HtmlConfig { input_404: None, site_url: None, cname: None, - livereload_url: None, + live_reload_endpoint: None, redirect: HashMap::new(), } } @@ -588,7 +582,7 @@ impl HtmlConfig { /// Configuration for how to render the print icon, print.html, and print.css. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] +#[serde(default, rename_all = "kebab-case")] pub struct Print { /// Whether print support is enabled. pub enable: bool, @@ -633,6 +627,8 @@ pub struct Playground { /// Set's the language the playground will work with /// TODO: Use an array when there's support for multiple languages simultaneously pub language: String, + /// Display the run button. Default: `true` + pub runnable: bool, } impl Default for Playground { @@ -643,6 +639,7 @@ impl Default for Playground { copy_js: true, line_numbers: false, language: "rust".to_string(), + runnable: true, } } } @@ -725,6 +722,7 @@ impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {} mod tests { use super::*; use crate::utils::fs::get_404_output_file; + use serde_json::json; const COMPLEX_CONFIG: &str = r#" [book] @@ -787,6 +785,7 @@ mod tests { copy_js: true, line_numbers: false, language: "rust".to_string(), + runnable: true, }; let html_should_be = HtmlConfig { curly_quotes: true, @@ -817,6 +816,22 @@ mod tests { assert_eq!(got.html_config().unwrap(), html_should_be); } + #[test] + fn disable_runnable() { + let src = r#" + [book] + title = "Some Book" + description = "book book book" + authors = ["Shogo Takata"] + + [output.html.playground] + runnable = false + "#; + + let got = Config::from_str(src).unwrap(); + assert!(!got.html_config().unwrap().playground.runnable); + } + #[test] fn edition_2015() { let src = r#" @@ -1023,7 +1038,7 @@ mod tests { fn encode_env_var(key: &str) -> String { format!( "MDBOOK_{}", - key.to_uppercase().replace('.', "__").replace("-", "_") + key.to_uppercase().replace('.', "__").replace('-', "_") ) } @@ -1047,11 +1062,10 @@ mod tests { } #[test] - #[allow(clippy::approx_constant)] fn update_config_using_env_var_and_complex_value() { let mut cfg = Config::default(); let key = "foo-bar.baz"; - let value = json!({"array": [1, 2, 3], "number": 3.14}); + let value = json!({"array": [1, 2, 3], "number": 13.37}); let value_str = serde_json::to_string(&value).unwrap(); assert!(cfg.get(key).is_none()); @@ -1161,4 +1175,24 @@ mod tests { Config::from_str(src).unwrap(); } + + #[test] + fn print_config() { + let src = r#" + [output.html.print] + enable = false + "#; + let got = Config::from_str(src).unwrap(); + let html_config = got.html_config().unwrap(); + assert!(!html_config.print.enable); + assert!(html_config.print.page_break); + let src = r#" + [output.html.print] + page-break = false + "#; + let got = Config::from_str(src).unwrap(); + let html_config = got.html_config().unwrap(); + assert!(html_config.print.enable); + assert!(!html_config.print.page_break); + } } diff --git a/src/lib.rs b/src/lib.rs index 82d9b6f7..14cd94d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,20 +82,6 @@ #![deny(missing_docs)] #![deny(rust_2018_idioms)] -#![allow(clippy::comparison_chain)] - -#[macro_use] -extern crate lazy_static; -#[macro_use] -extern crate log; -#[macro_use] -extern crate serde_derive; -#[macro_use] -extern crate serde_json; - -#[cfg(test)] -#[macro_use] -extern crate pretty_assertions; pub mod book; pub mod config; diff --git a/src/main.rs b/src/main.rs index 1f286d2d..35562e64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,8 @@ extern crate log; use anyhow::anyhow; use chrono::Local; -use clap::{App, AppSettings, Arg, ArgMatches, Shell, SubCommand}; +use clap::{App, AppSettings, Arg, ArgMatches}; +use clap_complete::Shell; use env_logger::Builder; use log::LevelFilter; use mdbook::utils; @@ -25,25 +26,31 @@ fn main() { // Check which subcomamnd the user ran... let res = match app.get_matches().subcommand() { - ("init", Some(sub_matches)) => cmd::init::execute(sub_matches), - ("build", Some(sub_matches)) => cmd::build::execute(sub_matches), - ("clean", Some(sub_matches)) => cmd::clean::execute(sub_matches), + Some(("init", sub_matches)) => cmd::init::execute(sub_matches), + Some(("build", sub_matches)) => cmd::build::execute(sub_matches), + Some(("clean", sub_matches)) => cmd::clean::execute(sub_matches), #[cfg(feature = "watch")] - ("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches), + Some(("watch", sub_matches)) => cmd::watch::execute(sub_matches), #[cfg(feature = "serve")] - ("serve", Some(sub_matches)) => cmd::serve::execute(sub_matches), - ("test", Some(sub_matches)) => cmd::test::execute(sub_matches), - ("completions", Some(sub_matches)) => (|| { + Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches), + Some(("test", sub_matches)) => cmd::test::execute(sub_matches), + Some(("completions", sub_matches)) => (|| { let shell: Shell = sub_matches .value_of("shell") .ok_or_else(|| anyhow!("Shell name missing."))? .parse() .map_err(|s| anyhow!("Invalid shell: {}", s))?; - create_clap_app().gen_completions_to("mdbook", shell, &mut std::io::stdout().lock()); + let mut complete_app = create_clap_app(); + clap_complete::generate( + shell, + &mut complete_app, + "mdbook", + &mut std::io::stdout().lock(), + ); Ok(()) })(), - (_, _) => unreachable!(), + _ => unreachable!(), }; if let Err(e) = res { @@ -54,14 +61,13 @@ fn main() { } /// Create a list of valid arguments and sub-commands -fn create_clap_app<'a, 'b>() -> App<'a, 'b> { +fn create_clap_app() -> App<'static> { let app = App::new(crate_name!()) .about(crate_description!()) .author("Mathieu David ") .version(VERSION) - .setting(AppSettings::GlobalVersion) + .setting(AppSettings::PropagateVersion) .setting(AppSettings::ArgRequiredElseHelp) - .setting(AppSettings::ColoredHelp) .after_help( "For more information about a specific command, try `mdbook --help`\n\ The source code for mdBook is available at: https://github.com/rust-lang/mdBook", @@ -71,12 +77,12 @@ fn create_clap_app<'a, 'b>() -> App<'a, 'b> { .subcommand(cmd::test::make_subcommand()) .subcommand(cmd::clean::make_subcommand()) .subcommand( - SubCommand::with_name("completions") + App::new("completions") .about("Generate shell completions for your shell to stdout") .arg( - Arg::with_name("shell") + Arg::new("shell") .takes_value(true) - .possible_values(&Shell::variants()) + .possible_values(Shell::possible_values()) .help("the shell to generate completions for") .value_name("SHELL") .required(true), @@ -137,3 +143,8 @@ fn open>(path: P) { error!("Error opening web browser: {}", e); } } + +#[test] +fn verify_app() { + create_clap_app().debug_assert(); +} diff --git a/src/preprocess/cmd.rs b/src/preprocess/cmd.rs index c47fd5d2..149dabda 100644 --- a/src/preprocess/cmd.rs +++ b/src/preprocess/cmd.rs @@ -1,6 +1,7 @@ use super::{Preprocessor, PreprocessorContext}; use crate::book::Book; use crate::errors::*; +use log::{debug, trace, warn}; use shlex::Shlex; use std::io::{self, Read, Write}; use std::process::{Child, Command, Stdio}; diff --git a/src/preprocess/index.rs b/src/preprocess/index.rs index fd60ad4d..d8a4e375 100644 --- a/src/preprocess/index.rs +++ b/src/preprocess/index.rs @@ -1,10 +1,11 @@ use regex::Regex; use std::path::Path; -use crate::errors::*; - use super::{Preprocessor, PreprocessorContext}; use crate::book::{Book, BookItem}; +use crate::errors::*; +use lazy_static::lazy_static; +use log::warn; /// A preprocessor for converting file name `README.md` to `index.md` since /// `README.md` is the de facto index file in markdown-based documentation. diff --git a/src/preprocess/links.rs b/src/preprocess/links.rs index edd97ba9..81575e86 100644 --- a/src/preprocess/links.rs +++ b/src/preprocess/links.rs @@ -10,6 +10,8 @@ use std::path::{Path, PathBuf}; use super::{Preprocessor, PreprocessorContext}; use crate::book::{Book, BookItem}; +use lazy_static::lazy_static; +use log::{error, warn}; const ESCAPE_CHAR: char = '\\'; const MAX_LINK_NESTED_DEPTH: usize = 10; @@ -146,6 +148,7 @@ enum RangeOrAnchor { } // A range of lines specified with some include directive. +#[allow(clippy::enum_variant_names)] // The prefix can't be removed, and is meant to mirror the contained type #[derive(PartialEq, Debug, Clone)] enum LineRange { Range(Range), diff --git a/src/preprocess/mod.rs b/src/preprocess/mod.rs index ee660636..df01a3db 100644 --- a/src/preprocess/mod.rs +++ b/src/preprocess/mod.rs @@ -12,6 +12,7 @@ use crate::book::Book; use crate::config::Config; use crate::errors::*; +use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::HashMap; use std::path::PathBuf; diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index f9f8be89..060aee68 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -14,7 +14,10 @@ use std::path::{Path, PathBuf}; use crate::utils::fs::get_404_output_file; use handlebars::Handlebars; +use lazy_static::lazy_static; +use log::{debug, trace, warn}; use regex::{Captures, Regex}; +use serde_json::json; #[derive(Default)] pub struct HtmlHandlebars; @@ -116,7 +119,7 @@ impl HtmlHandlebars { if ctx.is_index { ctx.data.insert("path".to_owned(), json!("index.md")); ctx.data.insert("path_to_root".to_owned(), json!("")); - ctx.data.insert("is_index".to_owned(), json!("true")); + ctx.data.insert("is_index".to_owned(), json!(true)); let rendered_index = ctx.handlebars.render("index", &ctx.data)?; let rendered_index = self.post_process(rendered_index, &ctx.html_config.playground, ctx.edition); @@ -170,6 +173,13 @@ impl HtmlHandlebars { // Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly data_404.insert("path".to_owned(), json!("404.md")); data_404.insert("content".to_owned(), json!(html_content_404)); + + let mut title = String::from("Page not found"); + if let Some(book_title) = &ctx.config.book.title { + title.push_str(" - "); + title.push_str(book_title); + } + data_404.insert("title".to_owned(), json!(title)); let rendered = handlebars.render("index", &data_404)?; let rendered = @@ -474,7 +484,13 @@ impl Renderer for HtmlHandlebars { let mut handlebars = Handlebars::new(); let theme_dir = match html_config.theme { - Some(ref theme) => ctx.root.join(theme), + Some(ref theme) => { + let dir = ctx.root.join(theme); + if !dir.is_dir() { + bail!("theme dir {} does not exist", dir.display()); + } + dir + } None => ctx.root.join("theme"), }; @@ -527,7 +543,8 @@ impl Renderer for HtmlHandlebars { chapter_titles: &ctx.chapter_titles, }; self.render_item(item, ctx, &mut print_content)?; - is_index = false; + // Only the first non-draft chapter item should be treated as the "index" + is_index &= !matches!(item, BookItem::Chapter(ch) if !ch.is_draft_chapter()); } // Render 404 page @@ -606,8 +623,11 @@ fn make_data( if theme.favicon_svg.is_some() { data.insert("favicon_svg".to_owned(), json!("favicon.svg")); } - if let Some(ref livereload) = html_config.livereload_url { - data.insert("livereload".to_owned(), json!(livereload)); + if let Some(ref live_reload_endpoint) = html_config.live_reload_endpoint { + data.insert( + "live_reload_endpoint".to_owned(), + json!(live_reload_endpoint), + ); } let default_theme = match html_config.default_theme { @@ -747,10 +767,13 @@ fn make_data( /// Goes through the rendered HTML, making sure all header tags have /// an anchor respectively so people can link to sections directly. fn build_header_links(html: &str) -> String { - let regex = Regex::new(r"(.*?)").unwrap(); + lazy_static! { + static ref BUILD_HEADER_LINKS: Regex = Regex::new(r"(.*?)").unwrap(); + } + let mut id_counter = HashMap::new(); - regex + BUILD_HEADER_LINKS .replace_all(html, |caps: &Captures<'_>| { let level = caps[1] .parse() @@ -768,16 +791,7 @@ fn insert_link_into_header( content: &str, id_counter: &mut HashMap, ) -> String { - let raw_id = utils::id_from_content(content); - - let id_count = id_counter.entry(raw_id.clone()).or_insert(0); - - let id = match *id_count { - 0 => raw_id, - other => format!("{}-{}", raw_id, other), - }; - - *id_count += 1; + let id = utils::unique_id_from_content(content, id_counter); format!( r##"{text}"##, @@ -796,11 +810,15 @@ fn insert_link_into_header( // ``` // This function replaces all commas by spaces in the code block classes fn fix_code_blocks(html: &str) -> String { - let regex = Regex::new(r##"]+)class="([^"]+)"([^>]*)>"##).unwrap(); - regex + lazy_static! { + static ref FIX_CODE_BLOCKS: Regex = + Regex::new(r##"]+)class="([^"]+)"([^>]*)>"##).unwrap(); + } + + FIX_CODE_BLOCKS .replace_all(html, |caps: &Captures<'_>| { let before = &caps[1]; - let classes = &caps[2].replace(",", " "); + let classes = &caps[2].replace(',', " "); let after = &caps[3]; format!( @@ -818,8 +836,11 @@ fn add_playground_pre( playground_config: &Playground, edition: Option, ) -> String { - let regex = Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?))"##).unwrap(); - regex + lazy_static! { + static ref ADD_PLAYGROUND_PRE: Regex = + Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?))"##).unwrap(); + } + ADD_PLAYGROUND_PRE .replace_all(html, |caps: &Captures<'_>| { let text = &caps[1]; let classes = &caps[2]; @@ -853,7 +874,8 @@ fn add_playground_pre_rust( ) -> String { if (!classes.contains("ignore") && !classes.contains("noplayground") - && !classes.contains("noplaypen")) + && !classes.contains("noplaypen") + && playground_config.runnable) || classes.contains("mdbook-runnable") { let contains_e2015 = classes.contains("edition2015"); @@ -870,12 +892,12 @@ fn add_playground_pre_rust( None => "", } }; - let all_classes = format!("{}{}", classes, edition_class); // wrap the contents in an external pre block format!( - "
{}
", - all_classes, + "
{}
", + classes, + edition_class, { let content: Cow<'_, str> = if playground_config.editable && classes.contains("editable") @@ -887,7 +909,7 @@ fn add_playground_pre_rust( // we need to inject our own main let (attrs, code) = partition_source(code); - format!("\n# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code).into() + format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code).into() }; hide_lines(&content) } @@ -902,14 +924,21 @@ lazy_static! { } fn hide_lines(content: &str) -> String { + lazy_static! { + static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap(); + } + let mut result = String::with_capacity(content.len()); - for line in content.lines() { + let mut lines = content.lines().peekable(); + while let Some(line) = lines.next() { + // Don't include newline on the last line. + let newline = if lines.peek().is_none() { "" } else { "\n" }; if let Some(caps) = BORING_LINES_REGEX.captures(line) { if &caps[2] == "#" { result += &caps[1]; result += &caps[2]; result += &caps[3]; - result += "\n"; + result += newline; continue; } else if &caps[2] != "!" && &caps[2] != "[" { result += ""; @@ -918,13 +947,13 @@ fn hide_lines(content: &str) -> String { result += &caps[2]; } result += &caps[3]; - result += "\n"; + result += newline; result += ""; continue; } } result += line; - result += "\n"; + result += newline; } result } @@ -1004,19 +1033,19 @@ mod tests { fn add_playground() { let inputs = [ ("x()", - "
\n#![allow(unused)]\nfn main() {\nx()\n}\n
"), + "
#![allow(unused)]\nfn main() {\nx()\n}
"), ("fn main() {}", - "
fn main() {}\n
"), + "
fn main() {}
"), ("let s = \"foo\n # bar\n\";", - "
let s = \"foo\n bar\n\";\n
"), + "
let s = \"foo\n bar\n\";
"), ("let s = \"foo\n ## bar\n\";", - "
let s = \"foo\n # bar\n\";\n
"), + "
let s = \"foo\n # bar\n\";
"), ("let s = \"foo\n # bar\n#\n\";", - "
let s = \"foo\n bar\n\n\";\n
"), + "
let s = \"foo\n bar\n\n\";
"), ("let s = \"foo\n # bar\n\";", - "let s = \"foo\n bar\n\";\n"), + "let s = \"foo\n bar\n\";"), ("#![no_std]\nlet s = \"foo\";\n #[some_attr]", - "
#![no_std]\nlet s = \"foo\";\n #[some_attr]\n
"), + "
#![no_std]\nlet s = \"foo\";\n #[some_attr]
"), ]; for (src, should_be) in &inputs { let got = add_playground_pre( @@ -1034,13 +1063,13 @@ mod tests { fn add_playground_edition2015() { let inputs = [ ("x()", - "
\n#![allow(unused)]\nfn main() {\nx()\n}\n
"), + "
#![allow(unused)]\nfn main() {\nx()\n}
"), ("fn main() {}", - "
fn main() {}\n
"), + "
fn main() {}
"), ("fn main() {}", - "
fn main() {}\n
"), + "
fn main() {}
"), ("fn main() {}", - "
fn main() {}\n
"), + "
fn main() {}
"), ]; for (src, should_be) in &inputs { let got = add_playground_pre( @@ -1058,13 +1087,13 @@ mod tests { fn add_playground_edition2018() { let inputs = [ ("x()", - "
\n#![allow(unused)]\nfn main() {\nx()\n}\n
"), + "
#![allow(unused)]\nfn main() {\nx()\n}
"), ("fn main() {}", - "
fn main() {}\n
"), + "
fn main() {}
"), ("fn main() {}", - "
fn main() {}\n
"), + "
fn main() {}
"), ("fn main() {}", - "
fn main() {}\n
"), + "
fn main() {}
"), ]; for (src, should_be) in &inputs { let got = add_playground_pre( @@ -1082,13 +1111,13 @@ mod tests { fn add_playground_edition2021() { let inputs = [ ("x()", - "
\n#![allow(unused)]\nfn main() {\nx()\n}\n
"), + "
#![allow(unused)]\nfn main() {\nx()\n}
"), ("fn main() {}", - "
fn main() {}\n
"), + "
fn main() {}
"), ("fn main() {}", - "
fn main() {}\n
"), + "
fn main() {}
"), ("fn main() {}", - "
fn main() {}\n
"), + "
fn main() {}
"), ]; for (src, should_be) in &inputs { let got = add_playground_pre( diff --git a/src/renderer/html_handlebars/helpers/navigation.rs b/src/renderer/html_handlebars/helpers/navigation.rs index 83bdadb3..b184c441 100644 --- a/src/renderer/html_handlebars/helpers/navigation.rs +++ b/src/renderer/html_handlebars/helpers/navigation.rs @@ -4,6 +4,8 @@ use std::path::Path; use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable}; use crate::utils; +use log::{debug, trace}; +use serde_json::json; type StringMap = BTreeMap; @@ -61,7 +63,7 @@ fn find_chapter( .as_json() .as_str() .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? - .replace("\"", ""); + .replace('\"', ""); if !rc.evaluate(ctx, "@root/is_index")?.is_missing() { // Special case for index.md which may be a synthetic page. @@ -121,7 +123,7 @@ fn render( .as_json() .as_str() .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? - .replace("\"", ""); + .replace('\"', ""); context.insert( "path_to_root".to_owned(), @@ -141,20 +143,17 @@ fn render( .with_extension("html") .to_str() .ok_or_else(|| RenderError::new("Link could not be converted to str")) - .map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/")))) + .map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/")))) })?; trace!("Render template"); - _h.template() - .ok_or_else(|| RenderError::new("Error with the handlebars template")) - .and_then(|t| { - let mut local_rc = rc.clone(); - let local_ctx = Context::wraps(&context)?; - t.render(r, &local_ctx, &mut local_rc, out) - })?; - - Ok(()) + let t = _h + .template() + .ok_or_else(|| RenderError::new("Error with the handlebars template"))?; + let local_ctx = Context::wraps(&context)?; + let mut local_rc = rc.clone(); + t.render(r, &local_ctx, &mut local_rc, out) } pub fn previous( diff --git a/src/renderer/html_handlebars/helpers/theme.rs b/src/renderer/html_handlebars/helpers/theme.rs index 809ee117..83aba677 100644 --- a/src/renderer/html_handlebars/helpers/theme.rs +++ b/src/renderer/html_handlebars/helpers/theme.rs @@ -1,4 +1,5 @@ use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError}; +use log::trace; pub fn theme_option( h: &Helper<'_, '_>, diff --git a/src/renderer/html_handlebars/helpers/toc.rs b/src/renderer/html_handlebars/helpers/toc.rs index a2ea501d..e96e6ef6 100644 --- a/src/renderer/html_handlebars/helpers/toc.rs +++ b/src/renderer/html_handlebars/helpers/toc.rs @@ -1,11 +1,10 @@ -use std::collections::BTreeMap; -use std::io; use std::path::Path; +use std::{cmp::Ordering, collections::BTreeMap}; use crate::utils; +use crate::utils::bracket_escape; use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError}; -use pulldown_cmark::{html, Event, Parser}; // Handlebars helper to construct TOC #[derive(Clone, Copy)] @@ -34,7 +33,7 @@ impl HelperDef for RenderToc { .as_json() .as_str() .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? - .replace("\"", ""); + .replace('\"', ""); let current_section = rc .evaluate(ctx, "@root/section")? @@ -58,6 +57,11 @@ impl HelperDef for RenderToc { out.write("
    ")?; let mut current_level = 1; + // The "index" page, which has this attribute set, is supposed to alias the first chapter in + // the book, i.e. the first link. There seems to be no easy way to determine which chapter + // the "index" is aliasing from within the renderer, so this is used instead to force the + // first link to be active. See further below. + let mut is_first_chapter = ctx.data().get("is_index").is_some(); for item in chapters { // Spacer @@ -82,61 +86,66 @@ impl HelperDef for RenderToc { level - 1 < fold_level as usize }; - if level > current_level { - while level > current_level { - out.write("
  1. ")?; - out.write("
      ")?; - current_level += 1; + match level.cmp(¤t_level) { + Ordering::Greater => { + while level > current_level { + out.write("
    1. ")?; + out.write("
        ")?; + current_level += 1; + } + write_li_open_tag(out, is_expanded, false)?; } - write_li_open_tag(out, is_expanded, false)?; - } else if level < current_level { - while level < current_level { - out.write("
      ")?; - out.write("
    2. ")?; - current_level -= 1; + Ordering::Less => { + while level < current_level { + out.write("
    ")?; + out.write("
  2. ")?; + current_level -= 1; + } + write_li_open_tag(out, is_expanded, false)?; + } + Ordering::Equal => { + write_li_open_tag(out, is_expanded, item.get("section").is_none())?; } - write_li_open_tag(out, is_expanded, false)?; - } else { - write_li_open_tag(out, is_expanded, item.get("section").is_none())?; } // Part title if let Some(title) = item.get("part") { out.write("
  3. ")?; - write_escaped(out, title)?; + out.write(&bracket_escape(title))?; out.write("
  4. ")?; continue; } // Link - let path_exists = if let Some(path) = - item.get("path") - .and_then(|p| if p.is_empty() { None } else { Some(p) }) - { - out.write(" { + out.write("")?; + path_exists = true; } - - out.write(">")?; - true - } else { - out.write("
    ")?; - false - }; + _ => { + out.write("
    ")?; + path_exists = false; + } + } if !self.no_section_label { // Section does not necessarily exist @@ -148,20 +157,7 @@ impl HelperDef for RenderToc { } if let Some(name) = item.get("name") { - // Render only inline code blocks - - // filter all events that are not inline code blocks - let parser = Parser::new(name).filter(|event| match *event { - Event::Code(_) | Event::Html(_) | Event::Text(_) => true, - _ => false, - }); - - // render markdown to html - let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2); - html::push_html(&mut markdown_parsed_name, parser); - - // write to the handlebars template - write_escaped(out, &markdown_parsed_name)?; + out.write(&bracket_escape(name))? } if path_exists { @@ -205,18 +201,3 @@ fn write_li_open_tag( li.push_str("\">"); out.write(&li) } - -fn write_escaped(out: &mut dyn Output, mut title: &str) -> io::Result<()> { - let needs_escape: &[char] = &['<', '>']; - while let Some(next) = title.find(needs_escape) { - out.write(&title[..next])?; - match title.as_bytes()[next] { - b'<' => out.write("<")?, - b'>' => out.write(">")?, - _ => unreachable!(), - } - title = &title[next + 1..]; - } - out.write(title)?; - Ok(()) -} diff --git a/src/renderer/html_handlebars/search.rs b/src/renderer/html_handlebars/search.rs index 39b59800..f88893a0 100644 --- a/src/renderer/html_handlebars/search.rs +++ b/src/renderer/html_handlebars/search.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::path::Path; -use elasticlunr::Index; +use elasticlunr::{Index, IndexBuilder}; use pulldown_cmark::*; use crate::book::{Book, BookItem}; @@ -10,10 +10,29 @@ use crate::config::Search; use crate::errors::*; use crate::theme::searcher; use crate::utils; +use lazy_static::lazy_static; +use log::{debug, warn}; +use serde::Serialize; + +const MAX_WORD_LENGTH_TO_INDEX: usize = 80; + +/// Tokenizes in the same way as elasticlunr-rs (for English), but also drops long tokens. +fn tokenize(text: &str) -> Vec { + text.split(|c: char| c.is_whitespace() || c == '-') + .filter(|s| !s.is_empty()) + .map(|s| s.trim().to_lowercase()) + .filter(|s| s.len() <= MAX_WORD_LENGTH_TO_INDEX) + .collect() +} /// Creates all files required for search. pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> { - let mut index = Index::new(&["title", "body", "breadcrumbs"]); + let mut index = IndexBuilder::new() + .add_field_with_tokenizer("title", Box::new(&tokenize)) + .add_field_with_tokenizer("body", Box::new(&tokenize)) + .add_field_with_tokenizer("breadcrumbs", Box::new(&tokenize)) + .build(); + let mut doc_urls = Vec::with_capacity(book.sections.len()); for item in book.iter() { @@ -97,6 +116,7 @@ fn render_item( breadcrumbs.push(chapter.name.clone()); + let mut id_counter = HashMap::new(); while let Some(event) = p.next() { match event { Event::Start(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => { @@ -120,7 +140,7 @@ fn render_item( } Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => { in_heading = false; - section_id = Some(utils::id_from_content(&heading)); + section_id = Some(utils::unique_id_from_content(&heading, &mut id_counter)); breadcrumbs.push(heading.clone()); } Event::Start(Tag::FootnoteDefinition(name)) => { @@ -208,12 +228,13 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec) -> let mut fields = BTreeMap::new(); let mut opt = SearchOptionsField::default(); - opt.boost = Some(search_config.boost_title); - fields.insert("title".into(), opt); - opt.boost = Some(search_config.boost_paragraph); - fields.insert("body".into(), opt); - opt.boost = Some(search_config.boost_hierarchy); - fields.insert("breadcrumbs".into(), opt); + let mut insert_boost = |key: &str, boost| { + opt.boost = Some(boost); + fields.insert(key.into(), opt); + }; + insert_boost("title", search_config.boost_title); + insert_boost("body", search_config.boost_paragraph); + insert_boost("breadcrumbs", search_config.boost_hierarchy); let search_options = SearchOptions { bool: if search_config.use_boolean_and { diff --git a/src/renderer/markdown_renderer.rs b/src/renderer/markdown_renderer.rs index bd5def1f..13bd05cc 100644 --- a/src/renderer/markdown_renderer.rs +++ b/src/renderer/markdown_renderer.rs @@ -2,7 +2,7 @@ use crate::book::BookItem; use crate::errors::*; use crate::renderer::{RenderContext, Renderer}; use crate::utils; - +use log::trace; use std::fs; #[derive(Default)] diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 9d2952c1..1c97f8f2 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -27,8 +27,11 @@ use std::process::{Command, Stdio}; use crate::book::Book; use crate::config::Config; use crate::errors::*; +use log::{error, info, trace, warn}; use toml::Value; +use serde::{Deserialize, Serialize}; + /// An arbitrary `mdbook` backend. /// /// Although it's quite possible for you to import `mdbook` as a library and diff --git a/src/theme/ayu-highlight.css b/src/theme/ayu-highlight.css index 0c45c6f1..32c94322 100644 --- a/src/theme/ayu-highlight.css +++ b/src/theme/ayu-highlight.css @@ -8,7 +8,6 @@ Original by Dempfi (https://github.com/dempfi/ayu) overflow-x: auto; background: #191f26; color: #e6e1cf; - padding: 0.5em; } .hljs-comment, diff --git a/src/theme/css/chrome.css b/src/theme/css/chrome.css index 21c08b93..10fa4b36 100644 --- a/src/theme/css/chrome.css +++ b/src/theme/css/chrome.css @@ -208,24 +208,63 @@ pre { pre > .buttons { position: absolute; z-index: 100; - right: 5px; - top: 5px; + right: 0px; + top: 2px; + margin: 0px; + padding: 2px 0px; color: var(--sidebar-fg); cursor: pointer; + visibility: hidden; + opacity: 0; + transition: visibility 0.1s linear, opacity 0.1s linear; +} +pre:hover > .buttons { + visibility: visible; + opacity: 1 } pre > .buttons :hover { color: var(--sidebar-active); + border-color: var(--icons-hover); + background-color: var(--theme-hover); } pre > .buttons i { margin-left: 8px; } pre > .buttons button { - color: inherit; - background: transparent; - border: none; cursor: inherit; + margin: 0px 5px; + padding: 3px 5px; + font-size: 14px; + + border-style: solid; + border-width: 1px; + border-radius: 4px; + border-color: var(--icons); + background-color: var(--theme-popup-bg); + transition: 100ms; + transition-property: color,border-color,background-color; + color: var(--icons); } +@media (pointer: coarse) { + pre > .buttons button { + /* On mobile, make it easier to tap buttons. */ + padding: 0.3rem 1rem; + } +} +pre > code { + padding: 1rem; +} + +/* FIXME: ACE editors overlap their buttons because ACE does absolute + positioning within the code block which breaks padding. The only solution I + can think of is to move the padding to the outer pre tag (or insert a div + wrapper), but that would require fixing a whole bunch of CSS rules. +*/ +.hljs.ace_editor { + padding: 0rem 0rem; +} + pre > .result { margin-top: 10px; } diff --git a/src/theme/css/general.css b/src/theme/css/general.css index ef2ba504..0e4f07a5 100644 --- a/src/theme/css/general.css +++ b/src/theme/css/general.css @@ -26,6 +26,16 @@ code { font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */ } +/* make long words/inline code not x overflow */ +main { + overflow-wrap: break-word; +} + +/* make wide tables scroll if they overflow */ +.table-wrapper { + overflow-x: auto; +} + /* Don't change font size in headers. */ h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { font-size: unset; @@ -80,8 +90,7 @@ h6:target::before { .content { overflow-y: auto; - padding: 0 15px; - padding-bottom: 50px; + padding: 0 5px 50px 5px; } .content main { margin-left: auto; diff --git a/src/theme/highlight.css b/src/theme/highlight.css index c2343227..ba57b82b 100644 --- a/src/theme/highlight.css +++ b/src/theme/highlight.css @@ -61,7 +61,6 @@ overflow-x: auto; background: #f6f7f6; color: #000; - padding: 0.5em; } .hljs-emphasis { diff --git a/src/theme/index.hbs b/src/theme/index.hbs index 966eedbc..0321e141 100644 --- a/src/theme/index.hbs +++ b/src/theme/index.hbs @@ -51,18 +51,18 @@ {{#if mathjax_support}} - + {{/if}} - - - - {{/if}} {{#if playground_copyable}} - {{/if}} {{#if playground_js}} - - - - - + + + + + {{/if}} {{#if search_js}} - - - + + + {{/if}} - - - + + + {{#each additional_js}} - + {{/each}} {{#if is_print}} {{#if mathjax_support}} - {{else}} -