Merge branch 'master' of https://github.com/rust-lang/mdBook into include-ident
This commit is contained in:
commit
467314dfd9
|
@ -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.54.0
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
|
@ -48,4 +49,4 @@ jobs:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||||
- run: cargo fmt -- --check
|
- run: cargo fmt --check
|
||||||
|
|
53
CHANGELOG.md
53
CHANGELOG.md
|
@ -1,5 +1,58 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Major update to expand the documentation located at <https://rust-lang.github.io/mdBook/>.
|
||||||
|
[#1709](https://github.com/rust-lang/mdBook/pull/1709)
|
||||||
|
[#1710](https://github.com/rust-lang/mdBook/pull/1710)
|
||||||
|
- Updated the markdown parser with various fixes for common-mark compliance.
|
||||||
|
[#1712](https://github.com/rust-lang/mdBook/pull/1712)
|
||||||
|
|
||||||
## mdBook 0.4.14
|
## mdBook 0.4.14
|
||||||
[ffa8284...c9b6be8](https://github.com/rust-lang/mdBook/compare/ffa8284...c9b6be8)
|
[ffa8284...c9b6be8](https://github.com/rust-lang/mdBook/compare/ffa8284...c9b6be8)
|
||||||
|
|
||||||
|
|
|
@ -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]]
|
||||||
|
@ -261,17 +271,14 @@ 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]]
|
||||||
|
@ -473,15 +480,6 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "getopts"
|
|
||||||
version = "0.2.21"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.1.16"
|
version = "0.1.16"
|
||||||
|
@ -583,15 +581,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"
|
||||||
|
@ -839,13 +828,14 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mdbook"
|
name = "mdbook"
|
||||||
version = "0.4.14"
|
version = "0.4.18"
|
||||||
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",
|
||||||
|
@ -863,7 +853,6 @@ dependencies = [
|
||||||
"select",
|
"select",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shlex",
|
"shlex",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
@ -1062,6 +1051,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"
|
||||||
|
@ -1267,12 +1265,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark"
|
name = "pulldown-cmark"
|
||||||
version = "0.9.0"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acd16514d1af5f7a71f909a44ef253cdb712a376d7ebc8ae4a471a9be9743548"
|
checksum = "34f197a544b0c9ab3ae46c359a7ec9cbbb5c7bf97054266fecb7ead794a181d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"getopts",
|
|
||||||
"memchr",
|
"memchr",
|
||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
@ -1400,9 +1397,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.5.4"
|
version = "1.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
|
checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -1464,27 +1461,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "0.11.0"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
|
checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
|
||||||
dependencies = [
|
|
||||||
"semver-parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver-parser"
|
|
||||||
version = "0.10.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
|
|
||||||
dependencies = [
|
|
||||||
"pest",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
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"
|
||||||
|
@ -1600,27 +1588,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"
|
||||||
|
@ -1669,12 +1639,9 @@ 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"
|
||||||
dependencies = [
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
|
@ -1876,18 +1843,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"
|
||||||
|
@ -1912,12 +1867,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"
|
||||||
|
|
16
Cargo.toml
16
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "mdbook"
|
name = "mdbook"
|
||||||
version = "0.4.14"
|
version = "0.4.18"
|
||||||
authors = [
|
authors = [
|
||||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||||
|
@ -18,17 +18,17 @@ 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"] }
|
||||||
|
clap_complete = "3.0"
|
||||||
env_logger = "0.7.1"
|
env_logger = "0.7.1"
|
||||||
handlebars = "4.0"
|
handlebars = "4.0"
|
||||||
lazy_static = "1.0"
|
lazy_static = "1.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
memchr = "2.0"
|
memchr = "2.0"
|
||||||
opener = "0.5"
|
opener = "0.5"
|
||||||
pulldown-cmark = "0.9.0"
|
pulldown-cmark = { version = "0.9.1", default-features = false }
|
||||||
regex = "1.0.0"
|
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"
|
||||||
|
@ -45,14 +45,14 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"], optional = tr
|
||||||
warp = { version = "0.3.1", default-features = false, features = ["websocket"], optional = true }
|
warp = { version = "0.3.1", 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]
|
||||||
assert_cmd = "1"
|
assert_cmd = "1"
|
||||||
predicates = "2"
|
predicates = "2"
|
||||||
select = "0.5"
|
select = "0.5"
|
||||||
semver = "0.11.0"
|
semver = "1.0"
|
||||||
pretty_assertions = "0.6"
|
pretty_assertions = "0.6"
|
||||||
walkdir = "2.0"
|
walkdir = "2.0"
|
||||||
|
|
||||||
|
|
|
@ -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"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.14/mdbook-v0.4.14-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.18/mdbook-v0.4.18-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||||
bin/mdbook build
|
bin/mdbook build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ The `chapter.content` is just a string which happens to be markdown. While it's
|
||||||
entirely possible to use regular expressions or do a manual find & replace,
|
entirely possible to use regular expressions or do a manual find & replace,
|
||||||
you'll probably want to process the input into something more computer-friendly.
|
you'll probably want to process the input into something more computer-friendly.
|
||||||
The [`pulldown-cmark`][pc] crate implements a production-quality event-based
|
The [`pulldown-cmark`][pc] crate implements a production-quality event-based
|
||||||
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] allowing you to
|
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] crate allowing you to
|
||||||
translate events back into markdown text.
|
translate events back into markdown text.
|
||||||
|
|
||||||
The following code block shows how to remove all emphasis from markdown,
|
The following code block shows how to remove all emphasis from markdown,
|
||||||
|
|
|
@ -173,10 +173,12 @@ By default, mdBook will include an icon on the top right of the book (which look
|
||||||
```toml
|
```toml
|
||||||
[output.html.print]
|
[output.html.print]
|
||||||
enable = true # include support for printable output
|
enable = true # include support for printable output
|
||||||
|
page-break = true # insert page-break after each chapter
|
||||||
```
|
```
|
||||||
|
|
||||||
- **enable:** Enable print support. When `false`, all print support will not be
|
- **enable:** Enable print support. When `false`, all print support will not be
|
||||||
rendered. Defaults to `true`.
|
rendered. Defaults to `true`.
|
||||||
|
- **page-break** Insert page breaks between chapters. Defaults to `true`.
|
||||||
|
|
||||||
### `[output.html.fold]`
|
### `[output.html.fold]`
|
||||||
|
|
||||||
|
@ -205,6 +207,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`.
|
||||||
|
@ -212,6 +215,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/
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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 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))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
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 +537,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 +652,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";
|
||||||
|
|
|
@ -1,22 +1,29 @@
|
||||||
use crate::{get_book_dir, open};
|
use crate::{first_chapter, 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;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
// 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 +39,15 @@ 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"));
|
match first_chapter(&book)
|
||||||
|
.map(|path| book.build_dir_for("html").join(path).with_extension("html"))
|
||||||
|
{
|
||||||
|
Some(path) if Path::new(&path).exists() => open(path),
|
||||||
|
_ => {
|
||||||
|
error!("No chapter available to open");
|
||||||
|
std::process::exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
use super::watch;
|
use super::watch;
|
||||||
use crate::{get_book_dir, open};
|
use crate::{first_chapter, 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();
|
||||||
}
|
}
|
||||||
|
@ -97,10 +102,12 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
serve(build_dir, sockaddr, reload_tx, &file_404);
|
serve(build_dir, sockaddr, reload_tx, &file_404);
|
||||||
});
|
});
|
||||||
|
|
||||||
let serving_url = format!("http://{}", address);
|
|
||||||
info!("Serving on: {}", serving_url);
|
|
||||||
|
|
||||||
if open_browser {
|
if open_browser {
|
||||||
|
let serving_url = match first_chapter(&book).map(|path| path.with_extension("html")) {
|
||||||
|
Some(path) => format!("http://{}/{}", address, path.display()),
|
||||||
|
_ => format!("http://{}", address),
|
||||||
|
};
|
||||||
|
info!("Serving on: {}", serving_url);
|
||||||
open(serving_url);
|
open(serving_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,37 @@
|
||||||
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_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"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use crate::first_chapter;
|
||||||
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 +11,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 +46,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"));
|
match first_chapter(&book)
|
||||||
|
.map(|path| book.build_dir_for("html").join(path).with_extension("html"))
|
||||||
|
{
|
||||||
|
Some(path) if Path::new(&path).exists() => open(path),
|
||||||
|
_ => warn!("No chapter available to open"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger_on_change(&book, |paths, book_dir| {
|
trigger_on_change(&book, |paths, book_dir| {
|
||||||
|
|
|
@ -533,14 +533,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 +569,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,15 +588,20 @@ 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,
|
||||||
|
/// Insert page breaks between chapters. Default: `true`.
|
||||||
|
pub page_break: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Print {
|
impl Default for Print {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self { enable: true }
|
Self {
|
||||||
|
enable: true,
|
||||||
|
page_break: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -625,6 +630,8 @@ pub struct Playground {
|
||||||
pub copy_js: bool,
|
pub copy_js: bool,
|
||||||
/// Display line numbers on playground snippets. Default: `false`.
|
/// Display line numbers on playground snippets. Default: `false`.
|
||||||
pub line_numbers: bool,
|
pub line_numbers: bool,
|
||||||
|
/// Display the run button. Default: `true`
|
||||||
|
pub runnable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Playground {
|
impl Default for Playground {
|
||||||
|
@ -634,6 +641,7 @@ impl Default for Playground {
|
||||||
copyable: true,
|
copyable: true,
|
||||||
copy_js: true,
|
copy_js: true,
|
||||||
line_numbers: false,
|
line_numbers: false,
|
||||||
|
runnable: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -776,6 +784,7 @@ mod tests {
|
||||||
copyable: true,
|
copyable: true,
|
||||||
copy_js: true,
|
copy_js: true,
|
||||||
line_numbers: false,
|
line_numbers: false,
|
||||||
|
runnable: true,
|
||||||
};
|
};
|
||||||
let html_should_be = HtmlConfig {
|
let html_should_be = HtmlConfig {
|
||||||
curly_quotes: true,
|
curly_quotes: true,
|
||||||
|
@ -806,6 +815,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_eq!(got.html_config().unwrap().playground.runnable, false);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn edition_2015() {
|
fn edition_2015() {
|
||||||
let src = r#"
|
let src = r#"
|
||||||
|
@ -1150,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_eq!(html_config.print.enable, false);
|
||||||
|
assert_eq!(html_config.print.page_break, true);
|
||||||
|
let src = r#"
|
||||||
|
[output.html.print]
|
||||||
|
page-break = false
|
||||||
|
"#;
|
||||||
|
let got = Config::from_str(src).unwrap();
|
||||||
|
let html_config = got.html_config().unwrap();
|
||||||
|
assert_eq!(html_config.print.enable, true);
|
||||||
|
assert_eq!(html_config.print.page_break, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,8 +89,6 @@ extern crate lazy_static;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
57
src/main.rs
57
src/main.rs
|
@ -5,10 +5,14 @@ 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::book::Chapter;
|
||||||
use mdbook::utils;
|
use mdbook::utils;
|
||||||
|
use mdbook::BookItem;
|
||||||
|
use mdbook::MDBook;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
@ -25,25 +29,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 +64,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 +80,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),
|
||||||
|
@ -131,9 +140,25 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the first displayable chapter of the given book, or None if no displayable
|
||||||
|
// chapter is found (i.e. only drafts).
|
||||||
|
fn first_chapter(book: &MDBook) -> Option<&PathBuf> {
|
||||||
|
book.iter().find_map(|item| match item {
|
||||||
|
BookItem::Chapter(Chapter {
|
||||||
|
path: Some(path), ..
|
||||||
|
}) => Some(path),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn open<P: AsRef<OsStr>>(path: P) {
|
fn open<P: AsRef<OsStr>>(path: P) {
|
||||||
info!("Opening web browser");
|
info!("Opening web browser");
|
||||||
if let Err(e) = opener::open(path) {
|
if let Err(e) = opener::open(path) {
|
||||||
error!("Error opening web browser: {}", e);
|
error!("Error opening web browser: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_app() {
|
||||||
|
create_clap_app().debug_assert();
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@ use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Extra information for a `Preprocessor` to give them more context when
|
/// Extra information for a `Preprocessor` to give them more context when
|
||||||
/// processing a book.
|
/// processing a book.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
|
|
@ -56,7 +56,7 @@ impl HtmlHandlebars {
|
||||||
|
|
||||||
let fixed_content =
|
let fixed_content =
|
||||||
utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path));
|
utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path));
|
||||||
if !ctx.is_index {
|
if !ctx.is_index && ctx.html_config.print.page_break {
|
||||||
// Add page break between chapters
|
// Add page break between chapters
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
|
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
|
||||||
// Add both two CSS properties because of the compatibility issue
|
// Add both two CSS properties because of the compatibility issue
|
||||||
|
@ -170,6 +170,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 +481,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"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -606,8 +619,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 +763,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 +787,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,8 +806,12 @@ 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(",", " ");
|
||||||
|
@ -818,8 +832,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];
|
||||||
|
@ -828,7 +845,8 @@ fn add_playground_pre(
|
||||||
if classes.contains("language-rust") {
|
if classes.contains("language-rust") {
|
||||||
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");
|
||||||
|
@ -882,11 +900,11 @@ fn add_playground_pre(
|
||||||
.into_owned()
|
.into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
for line in content.lines() {
|
||||||
if let Some(caps) = BORING_LINES_REGEX.captures(line) {
|
if let Some(caps) = BORING_LINES_REGEX.captures(line) {
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::io;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
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)]
|
||||||
|
@ -103,7 +102,7 @@ impl HelperDef for RenderToc {
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
|
@ -148,20 +147,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 +191,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("<")?,
|
|
||||||
b'>' => out.write(">")?,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
title = &title[next + 1..];
|
|
||||||
}
|
|
||||||
out.write(title)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ use crate::errors::*;
|
||||||
use crate::theme::searcher;
|
use crate::theme::searcher;
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
/// 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 = Index::new(&["title", "body", "breadcrumbs"]);
|
||||||
|
@ -97,6 +99,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 +123,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)) => {
|
||||||
|
|
|
@ -29,6 +29,8 @@ use crate::config::Config;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
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
|
||||||
|
|
|
@ -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 type="text/javascript">
|
||||||
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();
|
||||||
|
|
|
@ -9,6 +9,7 @@ use regex::Regex;
|
||||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
|
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 +45,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 +62,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`.
|
||||||
|
@ -210,8 +233,26 @@ pub fn log_backtrace(e: &Error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bracket_escape(mut s: &str) -> String {
|
||||||
|
let mut escaped = String::with_capacity(s.len());
|
||||||
|
let needs_escape: &[char] = &['<', '>'];
|
||||||
|
while let Some(next) = s.find(needs_escape) {
|
||||||
|
escaped.push_str(&s[..next]);
|
||||||
|
match s.as_bytes()[next] {
|
||||||
|
b'<' => escaped.push_str("<"),
|
||||||
|
b'>' => escaped.push_str(">"),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
s = &s[next + 1..];
|
||||||
|
}
|
||||||
|
escaped.push_str(s);
|
||||||
|
escaped
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[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;
|
||||||
|
|
||||||
|
@ -332,8 +373,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 +403,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 +425,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("<"), "<");
|
||||||
|
assert_eq!(bracket_escape(">"), ">");
|
||||||
|
assert_eq!(bracket_escape("<>"), "<>");
|
||||||
|
assert_eq!(bracket_escape("<test>"), "<test>");
|
||||||
|
assert_eq!(bracket_escape("a<test>b"), "a<test>b");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,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()
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
use assert_cmd::Command;
|
||||||
|
|
||||||
|
pub(crate) fn mdbook_cmd() -> Command {
|
||||||
|
let mut cmd = Command::cargo_bin("mdbook").unwrap();
|
||||||
|
cmd.env_remove("RUST_LOG");
|
||||||
|
cmd
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
mod build;
|
mod build;
|
||||||
|
mod cmd;
|
||||||
mod test;
|
mod test;
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
|
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 file: "([^"]+)[\\/]README.md""##).unwrap())
|
||||||
|
@ -22,7 +22,7 @@ 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 file: "([^"]+)[\\/]README.md""##).unwrap())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Duplicate headers
|
||||||
|
|
||||||
|
This page validates behaviour of duplicate headers.
|
||||||
|
|
||||||
|
# Header Text
|
||||||
|
|
||||||
|
# Header Text
|
||||||
|
|
||||||
|
# header-text
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Summary formatting tests
|
||||||
|
|
||||||
|
- [*Italic* `code` \*escape\* \`escape2\`](formatted-summary.md)
|
||||||
|
- [Soft
|
||||||
|
line break](soft.md)
|
||||||
|
- [\<escaped tag\>](escaped-tag.md)
|
|
@ -17,6 +17,7 @@ use std::ffi::OsStr;
|
||||||
use std::fs;
|
use std::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();
|
||||||
|
@ -600,6 +621,93 @@ fn remove_absolute_components(path: &Path) -> impl Iterator<Item = Component> +
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks formatting of summary names with inline elements.
|
||||||
|
#[test]
|
||||||
|
fn summary_with_markdown_formatting() {
|
||||||
|
let temp = DummyBook::new().build().unwrap();
|
||||||
|
let mut cfg = Config::default();
|
||||||
|
cfg.set("book.src", "summary-formatting").unwrap();
|
||||||
|
let md = MDBook::load_with_config(temp.path(), cfg).unwrap();
|
||||||
|
md.build().unwrap();
|
||||||
|
|
||||||
|
let rendered_path = temp.path().join("book/formatted-summary.html");
|
||||||
|
assert_contains_strings(
|
||||||
|
rendered_path,
|
||||||
|
&[
|
||||||
|
r#"<a href="formatted-summary.html" class="active"><strong aria-hidden="true">1.</strong> Italic code *escape* `escape2`</a>"#,
|
||||||
|
r#"<a href="soft.html"><strong aria-hidden="true">2.</strong> Soft line break</a>"#,
|
||||||
|
r#"<a href="escaped-tag.html"><strong aria-hidden="true">3.</strong> <escaped tag></a>"#,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let generated_md = temp.path().join("summary-formatting/formatted-summary.md");
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(generated_md).unwrap(),
|
||||||
|
"# Italic code *escape* `escape2`\n"
|
||||||
|
);
|
||||||
|
let generated_md = temp.path().join("summary-formatting/soft.md");
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(generated_md).unwrap(),
|
||||||
|
"# Soft line break\n"
|
||||||
|
);
|
||||||
|
let generated_md = temp.path().join("summary-formatting/escaped-tag.md");
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(generated_md).unwrap(),
|
||||||
|
"# <escaped tag>\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure building fails if `[output.html].theme` points to a non-existent directory
|
||||||
|
#[test]
|
||||||
|
fn failure_on_missing_theme_directory() {
|
||||||
|
// 1. Using default theme should work
|
||||||
|
let temp = DummyBook::new().build().unwrap();
|
||||||
|
let book_toml = r#"
|
||||||
|
[book]
|
||||||
|
title = "implicit"
|
||||||
|
src = "src"
|
||||||
|
"#;
|
||||||
|
|
||||||
|
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
|
||||||
|
let md = MDBook::load(temp.path()).unwrap();
|
||||||
|
let got = md.build();
|
||||||
|
assert!(got.is_ok());
|
||||||
|
|
||||||
|
// 2. Pointing to a normal directory should work
|
||||||
|
let temp = DummyBook::new().build().unwrap();
|
||||||
|
let created = fs::create_dir(temp.path().join("theme-directory"));
|
||||||
|
assert!(created.is_ok());
|
||||||
|
let book_toml = r#"
|
||||||
|
[book]
|
||||||
|
title = "implicit"
|
||||||
|
src = "src"
|
||||||
|
|
||||||
|
[output.html]
|
||||||
|
theme = "./theme-directory"
|
||||||
|
"#;
|
||||||
|
|
||||||
|
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
|
||||||
|
let md = MDBook::load(temp.path()).unwrap();
|
||||||
|
let got = md.build();
|
||||||
|
assert!(got.is_ok());
|
||||||
|
|
||||||
|
// 3. Pointing to a non-existent directory should fail
|
||||||
|
let temp = DummyBook::new().build().unwrap();
|
||||||
|
let book_toml = r#"
|
||||||
|
[book]
|
||||||
|
title = "implicit"
|
||||||
|
src = "src"
|
||||||
|
|
||||||
|
[output.html]
|
||||||
|
theme = "./non-existent-directory"
|
||||||
|
"#;
|
||||||
|
|
||||||
|
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
|
||||||
|
let md = MDBook::load(temp.path()).unwrap();
|
||||||
|
let got = md.build();
|
||||||
|
assert!(got.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "search")]
|
#[cfg(feature = "search")]
|
||||||
mod search {
|
mod search {
|
||||||
use crate::dummy_book::DummyBook;
|
use crate::dummy_book::DummyBook;
|
||||||
|
@ -633,11 +741,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 +755,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,6 +766,10 @@ 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."
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue