Search with Elasticlunr, updated (#604)
* Add search with elasticlunr.js This commit adds search functionality to mdBook, based on work done by @phaiax. The in-browser search code uses elasticlunr.js to execute the search, using an index generated at book build time by elasticlunr-rs. * Add generator comment Someone on Reddit was wondering how the rust book was generated and said they checked the source. Thought I'd put this here. Might be a good idea to have a little footer "made with mdBook", but this'll do for now. * Remove search/editor file override behavior * Use for loop for book iterator * Improve HTML regex * Fix search CORS in file URIs * Use ammonia to sanitize HTML * Filter html5ever log messages
This commit is contained in:
parent
bb043ef660
commit
b2ad669c61
|
@ -2,3 +2,7 @@
|
||||||
|
|
||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
*.rs rust
|
*.rs rust
|
||||||
|
*.woff -text
|
||||||
|
*.ttf -text
|
||||||
|
*.otf -text
|
||||||
|
*.png -text
|
||||||
|
|
|
@ -6,6 +6,19 @@ dependencies = [
|
||||||
"memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ammonia"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"html5ever 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"tendril 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ansi_term"
|
name = "ansi_term"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -177,6 +190,20 @@ name = "either"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "elasticlunr-rs"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"regex 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
@ -288,6 +315,18 @@ dependencies = [
|
||||||
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html5ever"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"markup5ever 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.2.4"
|
version = "1.2.4"
|
||||||
|
@ -428,6 +467,11 @@ name = "mac"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "maplit"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markup5ever"
|
name = "markup5ever"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -441,6 +485,21 @@ dependencies = [
|
||||||
"tendril 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"tendril 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markup5ever"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"string_cache 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"tendril 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matches"
|
name = "matches"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
@ -450,9 +509,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
name = "mdbook"
|
name = "mdbook"
|
||||||
version = "0.1.4-alpha.0"
|
version = "0.1.4-alpha.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"clap 2.29.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
"clap 2.29.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"crossbeam 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"crossbeam 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"elasticlunr-rs 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"env_logger 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"env_logger 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"handlebars 0.29.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"handlebars 0.29.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -932,6 +993,20 @@ dependencies = [
|
||||||
"string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "string_cache"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"debug_unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "string_cache_codegen"
|
name = "string_cache_codegen"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -990,6 +1065,16 @@ dependencies = [
|
||||||
"utf-8 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"utf-8 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tendril"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"futf 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"utf-8 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
|
@ -1236,6 +1321,7 @@ dependencies = [
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
"checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4"
|
"checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4"
|
||||||
|
"checksum ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fd4c682378117e4186a492b2252b9537990e1617f44aed9788b9a1149de45477"
|
||||||
"checksum ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b3568b48b7cefa6b8ce125f9bb4989e52fbcc29ebea88df04cc7c5f12f70455"
|
"checksum ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b3568b48b7cefa6b8ce125f9bb4989e52fbcc29ebea88df04cc7c5f12f70455"
|
||||||
"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"
|
"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"
|
||||||
"checksum atty 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8352656fd42c30a0c3c89d26dea01e3b77c0ab2af18230835c15e2e13cd51859"
|
"checksum atty 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8352656fd42c30a0c3c89d26dea01e3b77c0ab2af18230835c15e2e13cd51859"
|
||||||
|
@ -1261,6 +1347,7 @@ dependencies = [
|
||||||
"checksum difference 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3304d19798a8e067e48d8e69b2c37f0b5e9b4e462504ad9e27e9f3fce02bba8"
|
"checksum difference 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3304d19798a8e067e48d8e69b2c37f0b5e9b4e462504ad9e27e9f3fce02bba8"
|
||||||
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
|
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
|
||||||
"checksum either 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "740178ddf48b1a9e878e6d6509a1442a2d42fd2928aae8e7a6f8a36fb01981b3"
|
"checksum either 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "740178ddf48b1a9e878e6d6509a1442a2d42fd2928aae8e7a6f8a36fb01981b3"
|
||||||
|
"checksum elasticlunr-rs 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4e7e7e61115293401a5ec119cf8b6fa28708906569e234af336c8bc0ea0033f4"
|
||||||
"checksum env_logger 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f15f0b172cb4f52ed5dbf47f774a387cd2315d1bf7894ab5af9b083ae27efa5a"
|
"checksum env_logger 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f15f0b172cb4f52ed5dbf47f774a387cd2315d1bf7894ab5af9b083ae27efa5a"
|
||||||
"checksum error 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "a6e606f14042bb87cc02ef6a14db6c90ab92ed6f62d87e69377bc759fd7987cc"
|
"checksum error 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "a6e606f14042bb87cc02ef6a14db6c90ab92ed6f62d87e69377bc759fd7987cc"
|
||||||
"checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
|
"checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
|
||||||
|
@ -1273,6 +1360,7 @@ dependencies = [
|
||||||
"checksum getopts 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)" = "b900c08c1939860ce8b54dc6a89e26e00c04c380fd0e09796799bd7f12861e05"
|
"checksum getopts 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)" = "b900c08c1939860ce8b54dc6a89e26e00c04c380fd0e09796799bd7f12861e05"
|
||||||
"checksum handlebars 0.29.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fb04af2006ea09d985fef82b81e0eb25337e51b691c76403332378a53d521edc"
|
"checksum handlebars 0.29.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fb04af2006ea09d985fef82b81e0eb25337e51b691c76403332378a53d521edc"
|
||||||
"checksum html5ever 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a49d5001dd1bddf042ea41ed4e0a671d50b1bf187e66b349d7ec613bdce4ad90"
|
"checksum html5ever 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a49d5001dd1bddf042ea41ed4e0a671d50b1bf187e66b349d7ec613bdce4ad90"
|
||||||
|
"checksum html5ever 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e579ac8647178ab915d400d7d22938bda5cd351c6c62e1c294d56884ccfc75fe"
|
||||||
"checksum httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2f407128745b78abc95c0ffbe4e5d37427fdc0d45470710cfef8c44522a2e37"
|
"checksum httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2f407128745b78abc95c0ffbe4e5d37427fdc0d45470710cfef8c44522a2e37"
|
||||||
"checksum hyper 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)" = "368cb56b2740ebf4230520e2b90ebb0461e69034d85d1945febd9b3971426db2"
|
"checksum hyper 0.10.13 (registry+https://github.com/rust-lang/crates.io-index)" = "368cb56b2740ebf4230520e2b90ebb0461e69034d85d1945febd9b3971426db2"
|
||||||
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
|
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
|
||||||
|
@ -1291,7 +1379,9 @@ dependencies = [
|
||||||
"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
|
"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
|
||||||
"checksum log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "89f010e843f2b1a31dbd316b3b8d443758bc634bed37aabade59c686d644e0a2"
|
"checksum log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "89f010e843f2b1a31dbd316b3b8d443758bc634bed37aabade59c686d644e0a2"
|
||||||
"checksum mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
"checksum mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
"checksum maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08cbb6b4fef96b6d77bfc40ec491b1690c779e77b05cd9f07f787ed376fd4c43"
|
||||||
"checksum markup5ever 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ff834ac7123c6a37826747e5ca09db41fd7a83126792021c2e636ad174bb77d3"
|
"checksum markup5ever 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ff834ac7123c6a37826747e5ca09db41fd7a83126792021c2e636ad174bb77d3"
|
||||||
|
"checksum markup5ever 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfedc97d5a503e96816d10fedcd5b42f760b2e525ce2f7ec71f6a41780548475"
|
||||||
"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376"
|
"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376"
|
||||||
"checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d"
|
"checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d"
|
||||||
"checksum mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0"
|
"checksum mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0"
|
||||||
|
@ -1348,6 +1438,7 @@ dependencies = [
|
||||||
"checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
|
"checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
|
||||||
"checksum staticfile 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "31493480e073d52522a94cdf56269dd8eb05f99549effd1826b0271690608878"
|
"checksum staticfile 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "31493480e073d52522a94cdf56269dd8eb05f99549effd1826b0271690608878"
|
||||||
"checksum string_cache 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "413fc7852aeeb5472f1986ef755f561ddf0c789d3d796e65f0b6fe293ecd4ef8"
|
"checksum string_cache 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "413fc7852aeeb5472f1986ef755f561ddf0c789d3d796e65f0b6fe293ecd4ef8"
|
||||||
|
"checksum string_cache 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39cb4173bcbd1319da31faa5468a7e3870683d7a237150b0b0aaafd546f6ad12"
|
||||||
"checksum string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "479cde50c3539481f33906a387f2bd17c8e87cb848c35b6021d41fb81ff9b4d7"
|
"checksum string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "479cde50c3539481f33906a387f2bd17c8e87cb848c35b6021d41fb81ff9b4d7"
|
||||||
"checksum string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc"
|
"checksum string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc"
|
||||||
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
|
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
|
||||||
|
@ -1355,6 +1446,7 @@ dependencies = [
|
||||||
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
|
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
|
||||||
"checksum tempdir 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f73eebdb68c14bcb24aef74ea96079830e7fa7b31a6106e42ea7ee887c1e134e"
|
"checksum tempdir 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f73eebdb68c14bcb24aef74ea96079830e7fa7b31a6106e42ea7ee887c1e134e"
|
||||||
"checksum tendril 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1b72f8e2f5b73b65c315b1a70c730f24b9d7a25f39e98de8acbe2bb795caea"
|
"checksum tendril 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1b72f8e2f5b73b65c315b1a70c730f24b9d7a25f39e98de8acbe2bb795caea"
|
||||||
|
"checksum tendril 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9de21546595a0873061940d994bbbc5c35f024ae4fd61ec5c5b159115684f508"
|
||||||
"checksum termcolor 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "73e83896da740a4541a6f21606b35f2aa4bada5b65d89dc61114bf9d6ff2dc7e"
|
"checksum termcolor 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "73e83896da740a4541a6f21606b35f2aa4bada5b65d89dc61114bf9d6ff2dc7e"
|
||||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||||
"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693"
|
"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693"
|
||||||
|
|
|
@ -50,6 +50,10 @@ iron = { version = "0.5", optional = true }
|
||||||
staticfile = { version = "0.4", optional = true }
|
staticfile = { version = "0.4", optional = true }
|
||||||
ws = { version = "0.7", optional = true}
|
ws = { version = "0.7", optional = true}
|
||||||
|
|
||||||
|
# Search feature
|
||||||
|
elasticlunr-rs = { version = "0.2", optional = true }
|
||||||
|
ammonia = { version = "1.1", optional = true }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
error-chain = "0.11"
|
error-chain = "0.11"
|
||||||
|
|
||||||
|
@ -60,12 +64,13 @@ walkdir = "2.0"
|
||||||
pulldown-cmark-to-cmark = "1.1.0"
|
pulldown-cmark-to-cmark = "1.1.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["output", "watch", "serve"]
|
default = ["output", "watch", "serve", "search"]
|
||||||
debug = []
|
debug = []
|
||||||
output = []
|
output = []
|
||||||
regenerate-css = []
|
regenerate-css = []
|
||||||
watch = ["notify", "time", "crossbeam"]
|
watch = ["notify", "time", "crossbeam"]
|
||||||
serve = ["iron", "staticfile", "ws"]
|
serve = ["iron", "staticfile", "ws"]
|
||||||
|
search = ["elasticlunr-rs", "ammonia"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
doc = false
|
doc = false
|
||||||
|
|
|
@ -8,3 +8,12 @@ mathjax-support = true
|
||||||
|
|
||||||
[output.html.playpen]
|
[output.html.playpen]
|
||||||
editable = true
|
editable = true
|
||||||
|
|
||||||
|
[output.html.search]
|
||||||
|
limit-results = 20
|
||||||
|
use-boolean-and = true
|
||||||
|
boost-title = 2
|
||||||
|
boost-hierarchy = 2
|
||||||
|
boost-paragraph = 1
|
||||||
|
expand = true
|
||||||
|
heading-split-level = 2
|
||||||
|
|
|
@ -16,6 +16,9 @@ create-missing = false
|
||||||
|
|
||||||
[output.html]
|
[output.html]
|
||||||
additional-css = ["custom.css"]
|
additional-css = ["custom.css"]
|
||||||
|
|
||||||
|
[output.html.search]
|
||||||
|
limit-results = 15
|
||||||
```
|
```
|
||||||
|
|
||||||
## Supported configuration options
|
## Supported configuration options
|
||||||
|
@ -81,14 +84,48 @@ The following configuration options are available:
|
||||||
stylesheets that will be loaded after the default ones where you can
|
stylesheets that will be loaded after the default ones where you can
|
||||||
surgically change the style.
|
surgically change the style.
|
||||||
- **additional-js:** If you need to add some behaviour to your book without
|
- **additional-js:** If you need to add some behaviour to your book without
|
||||||
removing the current behaviour, you can specify a set of javascript files
|
removing the current behaviour, you can specify a set of JavaScript files
|
||||||
that will be loaded alongside the default one.
|
that will be loaded alongside the default one.
|
||||||
- **playpen:** A subtable for configuring various playpen settings.
|
- **no-section-label:** mdBook by defaults adds section label in table of
|
||||||
- **no-section-label**: mdBook by defaults adds section label in table of
|
|
||||||
contents column. For example, "1.", "2.1". Set this option to true to
|
contents column. For example, "1.", "2.1". Set this option to true to
|
||||||
disable those labels. Defaults to `false`.
|
disable those labels. Defaults to `false`.
|
||||||
|
- **playpen:** A subtable for configuring various playpen settings.
|
||||||
|
- **search:** A subtable for configuring the in-browser search
|
||||||
|
functionality. mdBook must be compiled with the `search` feature enabled
|
||||||
|
(on by default).
|
||||||
|
|
||||||
**book.toml**
|
Available configuration options for the `[output.html.playpen]` table:
|
||||||
|
|
||||||
|
- **editable:** Allow editing the source code. Defaults to `false`.
|
||||||
|
- **copy-js:** Copy JavaScript files for the editor to the output directory.
|
||||||
|
Defaults to `true`.
|
||||||
|
|
||||||
|
[Ace]: https://ace.c9.io/
|
||||||
|
|
||||||
|
Available configuration options for the `[output.html.search]` table:
|
||||||
|
|
||||||
|
- **limit-results:** The maximum number of search results. Defaults to `30`.
|
||||||
|
- **teaser-word-count:** The number of words used for a search result teaser.
|
||||||
|
Defaults to `30`.
|
||||||
|
- **use-boolean-and:** Define the logical link between multiple search words.
|
||||||
|
If true, all search words must appear in each result. Defaults to `true`.
|
||||||
|
- **boost-title:** Boost factor for the search result score if a search word
|
||||||
|
appears in the header. Defaults to `2`.
|
||||||
|
- **boost-hierarchy:** Boost factor for the search result score if a search
|
||||||
|
word appears in the hierarchy. The hierarchy contains all titles of the
|
||||||
|
parent documents and all parent headings. Defaults to `1`.
|
||||||
|
- **boost-paragraph:** Boost factor for the search result score if a search
|
||||||
|
word appears in the text. Defaults to `1`.
|
||||||
|
- **expand:** True if search should match longer results e.g. search `micro`
|
||||||
|
should match `microwave`. Defaults to `true`.
|
||||||
|
- **heading-split-level:** Search results will link to a section of the document
|
||||||
|
which contains the result. Documents are split into sections by headings
|
||||||
|
this level or less.
|
||||||
|
Defaults to `3`. (`### This is a level 3 heading`)
|
||||||
|
- **copy-js:** Copy JavaScript files for the search implementation to the
|
||||||
|
output directory. Defaults to `true`.
|
||||||
|
|
||||||
|
This shows all available options in the **book.toml**:
|
||||||
```toml
|
```toml
|
||||||
[book]
|
[book]
|
||||||
title = "Example book"
|
title = "Example book"
|
||||||
|
@ -105,6 +142,18 @@ additional-js = ["custom.js"]
|
||||||
[output.html.playpen]
|
[output.html.playpen]
|
||||||
editor = "./path/to/editor"
|
editor = "./path/to/editor"
|
||||||
editable = false
|
editable = false
|
||||||
|
|
||||||
|
[output.html.search]
|
||||||
|
enable = true
|
||||||
|
searcher = "./path/to/searcher"
|
||||||
|
limit-results = 30
|
||||||
|
teaser-word-count = 30
|
||||||
|
use-boolean-and = true
|
||||||
|
boost-title = 2
|
||||||
|
boost-hierarchy = 1
|
||||||
|
boost-paragraph = 1
|
||||||
|
expand = true
|
||||||
|
heading-split-level = 3
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -145,4 +194,4 @@ override the book's title without needing to touch your `book.toml`.
|
||||||
|
|
||||||
The latter case may be useful in situations where `mdbook` is invoked
|
The latter case may be useful in situations where `mdbook` is invoked
|
||||||
from a script or CI, where it sometimes isn't possible to update the
|
from a script or CI, where it sometimes isn't possible to update the
|
||||||
`book.toml` before building.
|
`book.toml` before building.
|
||||||
|
|
|
@ -14,3 +14,5 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add
|
||||||
- [Michael-F-Bryan](https://github.com/Michael-F-Bryan)
|
- [Michael-F-Bryan](https://github.com/Michael-F-Bryan)
|
||||||
- [Chris Spiegel](https://github.com/cspiegel)
|
- [Chris Spiegel](https://github.com/cspiegel)
|
||||||
- [projektir](https://github.com/projektir)
|
- [projektir](https://github.com/projektir)
|
||||||
|
- [Phaiax](https://github.com/Phaiax)
|
||||||
|
- [Matt Ickstadt](https://github.com/mattico)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
cross build --target $TARGET --all --no-default-features
|
||||||
cross build --target $TARGET --all
|
cross build --target $TARGET --all
|
||||||
cross build --target $TARGET --all --release
|
cross build --target $TARGET --all --release
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ main() {
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cross test --target $TARGET --no-default-features
|
||||||
cross test --target $TARGET
|
cross test --target $TARGET
|
||||||
cross test --target $TARGET --release
|
cross test --target $TARGET --release
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,6 +92,8 @@ fn init_logger() {
|
||||||
} else {
|
} else {
|
||||||
// if no RUST_LOG provided, default to logging at the Info level
|
// if no RUST_LOG provided, default to logging at the Info level
|
||||||
builder.filter(None, LevelFilter::Info);
|
builder.filter(None, LevelFilter::Info);
|
||||||
|
// Filter extraneous html5ever not-implemented messages
|
||||||
|
builder.filter(Some("html5ever"), LevelFilter::Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.init();
|
builder.init();
|
||||||
|
|
|
@ -152,15 +152,20 @@ pub struct Chapter {
|
||||||
pub sub_items: Vec<BookItem>,
|
pub sub_items: Vec<BookItem>,
|
||||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
/// An ordered list of the names of each chapter above this one, in the hierarchy.
|
||||||
|
pub parent_names: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chapter {
|
impl Chapter {
|
||||||
/// Create a new chapter with the provided content.
|
/// Create a new chapter with the provided content.
|
||||||
pub fn new<P: Into<PathBuf>>(name: &str, content: String, path: P) -> Chapter {
|
pub fn new<P: Into<PathBuf>>(name: &str, content: String, path: P, parent_names: Vec<String>)
|
||||||
|
-> Chapter
|
||||||
|
{
|
||||||
Chapter {
|
Chapter {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
content: content,
|
content: content,
|
||||||
path: path.into(),
|
path: path.into(),
|
||||||
|
parent_names: parent_names,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,21 +188,27 @@ fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<
|
||||||
let mut chapters = Vec::new();
|
let mut chapters = Vec::new();
|
||||||
|
|
||||||
for summary_item in summary_items {
|
for summary_item in summary_items {
|
||||||
let chapter = load_summary_item(summary_item, src_dir)?;
|
let chapter = load_summary_item(summary_item, src_dir, Vec::new())?;
|
||||||
chapters.push(chapter);
|
chapters.push(chapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Book { sections: chapters })
|
Ok(Book { sections: chapters })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_summary_item<P: AsRef<Path>>(item: &SummaryItem, src_dir: P) -> Result<BookItem> {
|
fn load_summary_item<P: AsRef<Path>>(item: &SummaryItem, src_dir: P, parent_names: Vec<String>)
|
||||||
|
-> Result<BookItem>
|
||||||
|
{
|
||||||
match *item {
|
match *item {
|
||||||
SummaryItem::Separator => Ok(BookItem::Separator),
|
SummaryItem::Separator => Ok(BookItem::Separator),
|
||||||
SummaryItem::Link(ref link) => load_chapter(link, src_dir).map(|c| BookItem::Chapter(c)),
|
SummaryItem::Link(ref link) => {
|
||||||
|
load_chapter(link, src_dir, parent_names).map(|c| BookItem::Chapter(c))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_chapter<P: AsRef<Path>>(link: &Link, src_dir: P) -> Result<Chapter> {
|
fn load_chapter<P: AsRef<Path>>(link: &Link, src_dir: P, parent_names: Vec<String>)
|
||||||
|
-> Result<Chapter>
|
||||||
|
{
|
||||||
debug!("Loading {} ({})", link.name, link.location.display());
|
debug!("Loading {} ({})", link.name, link.location.display());
|
||||||
let src_dir = src_dir.as_ref();
|
let src_dir = src_dir.as_ref();
|
||||||
|
|
||||||
|
@ -218,12 +229,14 @@ fn load_chapter<P: AsRef<Path>>(link: &Link, src_dir: P) -> Result<Chapter> {
|
||||||
.strip_prefix(&src_dir)
|
.strip_prefix(&src_dir)
|
||||||
.expect("Chapters are always inside a book");
|
.expect("Chapters are always inside a book");
|
||||||
|
|
||||||
let mut ch = Chapter::new(&link.name, content, stripped);
|
let mut sub_item_parents = parent_names.clone();
|
||||||
|
let mut ch = Chapter::new(&link.name, content, stripped, parent_names);
|
||||||
ch.number = link.number.clone();
|
ch.number = link.number.clone();
|
||||||
|
|
||||||
|
sub_item_parents.push(link.name.clone());
|
||||||
let sub_items = link.nested_items
|
let sub_items = link.nested_items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|i| load_summary_item(i, src_dir))
|
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
|
||||||
.collect::<Result<Vec<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
ch.sub_items = sub_items;
|
ch.sub_items = sub_items;
|
||||||
|
@ -324,9 +337,9 @@ And here is some \
|
||||||
#[test]
|
#[test]
|
||||||
fn load_a_single_chapter_from_disk() {
|
fn load_a_single_chapter_from_disk() {
|
||||||
let (link, temp_dir) = dummy_link();
|
let (link, temp_dir) = dummy_link();
|
||||||
let should_be = Chapter::new("Chapter 1", DUMMY_SRC.to_string(), "chapter_1.md");
|
let should_be = Chapter::new("Chapter 1", DUMMY_SRC.to_string(), "chapter_1.md", Vec::new());
|
||||||
|
|
||||||
let got = load_chapter(&link, temp_dir.path()).unwrap();
|
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
|
||||||
assert_eq!(got, should_be);
|
assert_eq!(got, should_be);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,7 +347,7 @@ And here is some \
|
||||||
fn cant_load_a_nonexistent_chapter() {
|
fn cant_load_a_nonexistent_chapter() {
|
||||||
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
|
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
|
||||||
|
|
||||||
let got = load_chapter(&link, "");
|
let got = load_chapter(&link, "", Vec::new());
|
||||||
assert!(got.is_err());
|
assert!(got.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,6 +360,7 @@ And here is some \
|
||||||
content: String::from("Hello World!"),
|
content: String::from("Hello World!"),
|
||||||
number: Some(SectionNumber(vec![1, 2])),
|
number: Some(SectionNumber(vec![1, 2])),
|
||||||
path: PathBuf::from("second.md"),
|
path: PathBuf::from("second.md"),
|
||||||
|
parent_names: vec![String::from("Chapter 1")],
|
||||||
sub_items: Vec::new(),
|
sub_items: Vec::new(),
|
||||||
};
|
};
|
||||||
let should_be = BookItem::Chapter(Chapter {
|
let should_be = BookItem::Chapter(Chapter {
|
||||||
|
@ -354,6 +368,7 @@ And here is some \
|
||||||
content: String::from(DUMMY_SRC),
|
content: String::from(DUMMY_SRC),
|
||||||
number: None,
|
number: None,
|
||||||
path: PathBuf::from("chapter_1.md"),
|
path: PathBuf::from("chapter_1.md"),
|
||||||
|
parent_names: Vec::new(),
|
||||||
sub_items: vec![
|
sub_items: vec![
|
||||||
BookItem::Chapter(nested.clone()),
|
BookItem::Chapter(nested.clone()),
|
||||||
BookItem::Separator,
|
BookItem::Separator,
|
||||||
|
@ -361,7 +376,7 @@ And here is some \
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
let got = load_summary_item(&SummaryItem::Link(root), temp.path()).unwrap();
|
let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
|
||||||
assert_eq!(got, should_be);
|
assert_eq!(got, should_be);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -417,17 +432,20 @@ And here is some \
|
||||||
content: String::from(DUMMY_SRC),
|
content: String::from(DUMMY_SRC),
|
||||||
number: None,
|
number: None,
|
||||||
path: PathBuf::from("Chapter_1/index.md"),
|
path: PathBuf::from("Chapter_1/index.md"),
|
||||||
|
parent_names: Vec::new(),
|
||||||
sub_items: vec![
|
sub_items: vec![
|
||||||
BookItem::Chapter(Chapter::new(
|
BookItem::Chapter(Chapter::new(
|
||||||
"Hello World",
|
"Hello World",
|
||||||
String::new(),
|
String::new(),
|
||||||
"Chapter_1/hello.md",
|
"Chapter_1/hello.md",
|
||||||
|
Vec::new(),
|
||||||
)),
|
)),
|
||||||
BookItem::Separator,
|
BookItem::Separator,
|
||||||
BookItem::Chapter(Chapter::new(
|
BookItem::Chapter(Chapter::new(
|
||||||
"Goodbye World",
|
"Goodbye World",
|
||||||
String::new(),
|
String::new(),
|
||||||
"Chapter_1/goodbye.md",
|
"Chapter_1/goodbye.md",
|
||||||
|
Vec::new(),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
@ -464,17 +482,20 @@ And here is some \
|
||||||
content: String::from(DUMMY_SRC),
|
content: String::from(DUMMY_SRC),
|
||||||
number: None,
|
number: None,
|
||||||
path: PathBuf::from("Chapter_1/index.md"),
|
path: PathBuf::from("Chapter_1/index.md"),
|
||||||
|
parent_names: Vec::new(),
|
||||||
sub_items: vec![
|
sub_items: vec![
|
||||||
BookItem::Chapter(Chapter::new(
|
BookItem::Chapter(Chapter::new(
|
||||||
"Hello World",
|
"Hello World",
|
||||||
String::new(),
|
String::new(),
|
||||||
"Chapter_1/hello.md",
|
"Chapter_1/hello.md",
|
||||||
|
Vec::new(),
|
||||||
)),
|
)),
|
||||||
BookItem::Separator,
|
BookItem::Separator,
|
||||||
BookItem::Chapter(Chapter::new(
|
BookItem::Chapter(Chapter::new(
|
||||||
"Goodbye World",
|
"Goodbye World",
|
||||||
String::new(),
|
String::new(),
|
||||||
"Chapter_1/goodbye.md",
|
"Chapter_1/goodbye.md",
|
||||||
|
Vec::new(),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
//! Mdbook's configuration system.
|
//! Mdbook's configuration system.
|
||||||
//!
|
//!
|
||||||
//! The main entrypoint of the `config` module is the `Config` struct. This acts
|
//! The main entrypoint of the `config` module is the `Config` struct. This acts
|
||||||
//! essentially as a bag of configuration information, with a couple
|
//! essentially as a bag of configuration information, with a couple
|
||||||
//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support
|
//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support
|
||||||
//! for arbitrary data which is exposed to plugins and alternate backends.
|
//! for arbitrary data which is exposed to plugins and alternate backends.
|
||||||
//!
|
//!
|
||||||
//!
|
//!
|
||||||
//! # Examples
|
//! # Examples
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! # extern crate mdbook;
|
//! # extern crate mdbook;
|
||||||
//! # use mdbook::errors::*;
|
//! # use mdbook::errors::*;
|
||||||
|
@ -15,31 +15,31 @@
|
||||||
//! use std::path::PathBuf;
|
//! use std::path::PathBuf;
|
||||||
//! use mdbook::Config;
|
//! use mdbook::Config;
|
||||||
//! use toml::Value;
|
//! use toml::Value;
|
||||||
//!
|
//!
|
||||||
//! # fn run() -> Result<()> {
|
//! # fn run() -> Result<()> {
|
||||||
//! let src = r#"
|
//! let src = r#"
|
||||||
//! [book]
|
//! [book]
|
||||||
//! title = "My Book"
|
//! title = "My Book"
|
||||||
//! authors = ["Michael-F-Bryan"]
|
//! authors = ["Michael-F-Bryan"]
|
||||||
//!
|
//!
|
||||||
//! [build]
|
//! [build]
|
||||||
//! src = "out"
|
//! src = "out"
|
||||||
//!
|
//!
|
||||||
//! [other-table.foo]
|
//! [other-table.foo]
|
||||||
//! bar = 123
|
//! bar = 123
|
||||||
//! "#;
|
//! "#;
|
||||||
//!
|
//!
|
||||||
//! // load the `Config` from a toml string
|
//! // load the `Config` from a toml string
|
||||||
//! let mut cfg = Config::from_str(src)?;
|
//! let mut cfg = Config::from_str(src)?;
|
||||||
//!
|
//!
|
||||||
//! // retrieve a nested value
|
//! // retrieve a nested value
|
||||||
//! let bar = cfg.get("other-table.foo.bar").cloned();
|
//! let bar = cfg.get("other-table.foo.bar").cloned();
|
||||||
//! assert_eq!(bar, Some(Value::Integer(123)));
|
//! assert_eq!(bar, Some(Value::Integer(123)));
|
||||||
//!
|
//!
|
||||||
//! // Set the `output.html.theme` directory
|
//! // Set the `output.html.theme` directory
|
||||||
//! assert!(cfg.get("output.html").is_none());
|
//! assert!(cfg.get("output.html").is_none());
|
||||||
//! cfg.set("output.html.theme", "./themes");
|
//! cfg.set("output.html.theme", "./themes");
|
||||||
//!
|
//!
|
||||||
//! // then load it again, automatically deserializing to a `PathBuf`.
|
//! // then load it again, automatically deserializing to a `PathBuf`.
|
||||||
//! let got: PathBuf = cfg.get_deserialized("output.html.theme")?;
|
//! let got: PathBuf = cfg.get_deserialized("output.html.theme")?;
|
||||||
//! assert_eq!(got, PathBuf::from("./themes"));
|
//! assert_eq!(got, PathBuf::from("./themes"));
|
||||||
|
@ -410,7 +410,7 @@ pub struct HtmlConfig {
|
||||||
pub google_analytics: Option<String>,
|
pub google_analytics: Option<String>,
|
||||||
/// Additional CSS stylesheets to include in the rendered page's `<head>`.
|
/// Additional CSS stylesheets to include in the rendered page's `<head>`.
|
||||||
pub additional_css: Vec<PathBuf>,
|
pub additional_css: Vec<PathBuf>,
|
||||||
/// Additional JS scripts to include at the bottom of the rendered page's
|
/// Additional JS scripts to include at the bottom of the rendered page's
|
||||||
/// `<body>`.
|
/// `<body>`.
|
||||||
pub additional_js: Vec<PathBuf>,
|
pub additional_js: Vec<PathBuf>,
|
||||||
/// Playpen settings.
|
/// Playpen settings.
|
||||||
|
@ -425,29 +425,79 @@ pub struct HtmlConfig {
|
||||||
pub livereload_url: Option<String>,
|
pub livereload_url: Option<String>,
|
||||||
/// Should section labels be rendered?
|
/// Should section labels be rendered?
|
||||||
pub no_section_label: bool,
|
pub no_section_label: bool,
|
||||||
|
/// Search settings. If `None`, the default will be used.
|
||||||
|
pub search: Option<Search>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for tweaking how the the HTML renderer handles the playpen.
|
/// Configuration for tweaking how the the HTML renderer handles the playpen.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "kebab-case")]
|
#[serde(default, rename_all = "kebab-case")]
|
||||||
pub struct Playpen {
|
pub struct Playpen {
|
||||||
/// The path to the editor to use. Defaults to the [Ace Editor].
|
/// Should playpen snippets be editable? Default: `false`.
|
||||||
///
|
|
||||||
/// [Ace Editor]: https://ace.c9.io/
|
|
||||||
pub editor: PathBuf,
|
|
||||||
/// Should playpen snippets be editable? Defaults to `false`.
|
|
||||||
pub editable: bool,
|
pub editable: bool,
|
||||||
|
/// Copy JavaScript files for the editor to the output directory?
|
||||||
|
/// Default: `true`.
|
||||||
|
pub copy_js: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Playpen {
|
impl Default for Playpen {
|
||||||
fn default() -> Playpen {
|
fn default() -> Playpen {
|
||||||
Playpen {
|
Playpen {
|
||||||
editor: PathBuf::from("ace"),
|
|
||||||
editable: false,
|
editable: false,
|
||||||
|
copy_js: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configuration of the search functionality of the HTML renderer.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(default, rename_all = "kebab-case")]
|
||||||
|
pub struct Search {
|
||||||
|
/// Maximum number of visible results. Default: `30`.
|
||||||
|
pub limit_results: u32,
|
||||||
|
/// The number of words used for a search result teaser. Default: `30`,
|
||||||
|
pub teaser_word_count: u32,
|
||||||
|
/// Define the logical link between multiple search words.
|
||||||
|
/// If true, all search words must appear in each result. Default: `true`.
|
||||||
|
pub use_boolean_and: bool,
|
||||||
|
/// Boost factor for the search result score if a search word appears in the header.
|
||||||
|
/// Default: `2`.
|
||||||
|
pub boost_title: u8,
|
||||||
|
/// Boost factor for the search result score if a search word appears in the hierarchy.
|
||||||
|
/// The hierarchy contains all titles of the parent documents and all parent headings.
|
||||||
|
/// Default: `1`.
|
||||||
|
pub boost_hierarchy: u8,
|
||||||
|
/// Boost factor for the search result score if a search word appears in the text.
|
||||||
|
/// Default: `1`.
|
||||||
|
pub boost_paragraph: u8,
|
||||||
|
/// True if the searchword `micro` should match `microwave`. Default: `true`.
|
||||||
|
pub expand : bool,
|
||||||
|
/// Documents are split into smaller parts, seperated by headings. This defines, until which
|
||||||
|
/// level of heading documents should be split. Default: `3`. (`### This is a level 3 heading`)
|
||||||
|
pub heading_split_level: u8,
|
||||||
|
/// Copy JavaScript files for the search functionality to the output directory?
|
||||||
|
/// Default: `true`.
|
||||||
|
pub copy_js: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Search {
|
||||||
|
fn default() -> Search {
|
||||||
|
// Please update the documentation of `Search` when changing values!
|
||||||
|
Search {
|
||||||
|
limit_results: 30,
|
||||||
|
teaser_word_count: 30,
|
||||||
|
use_boolean_and: false,
|
||||||
|
boost_title: 2,
|
||||||
|
boost_hierarchy: 1,
|
||||||
|
boost_paragraph: 1,
|
||||||
|
expand: true,
|
||||||
|
heading_split_level: 3,
|
||||||
|
copy_js: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Allows you to "update" any arbitrary field in a struct by round-tripping via
|
/// Allows you to "update" any arbitrary field in a struct by round-tripping via
|
||||||
/// a `toml::Value`.
|
/// a `toml::Value`.
|
||||||
///
|
///
|
||||||
|
@ -525,7 +575,7 @@ mod tests {
|
||||||
};
|
};
|
||||||
let playpen_should_be = Playpen {
|
let playpen_should_be = Playpen {
|
||||||
editable: true,
|
editable: true,
|
||||||
editor: PathBuf::from("ace"),
|
copy_js: true,
|
||||||
};
|
};
|
||||||
let html_should_be = HtmlConfig {
|
let html_should_be = HtmlConfig {
|
||||||
curly_quotes: true,
|
curly_quotes: true,
|
||||||
|
|
17
src/lib.rs
17
src/lib.rs
|
@ -52,20 +52,20 @@
|
||||||
//!
|
//!
|
||||||
//! ## Implementing a new Backend
|
//! ## Implementing a new Backend
|
||||||
//!
|
//!
|
||||||
//! `mdbook` has a fairly flexible mechanism for creating additional backends
|
//! `mdbook` has a fairly flexible mechanism for creating additional backends
|
||||||
//! for your book. The general idea is you'll add an extra table in the book's
|
//! for your book. The general idea is you'll add an extra table in the book's
|
||||||
//! `book.toml` which specifies an executable to be invoked by `mdbook`. This
|
//! `book.toml` which specifies an executable to be invoked by `mdbook`. This
|
||||||
//! executable will then be called during a build, with an in-memory
|
//! executable will then be called during a build, with an in-memory
|
||||||
//! representation ([`RenderContext`]) of the book being passed to the
|
//! representation ([`RenderContext`]) of the book being passed to the
|
||||||
//! subprocess via `stdin`.
|
//! subprocess via `stdin`.
|
||||||
//!
|
//!
|
||||||
//! The [`RenderContext`] gives the backend access to the contents of
|
//! The [`RenderContext`] gives the backend access to the contents of
|
||||||
//! `book.toml` and lets it know which directory all generated artefacts should
|
//! `book.toml` and lets it know which directory all generated artefacts should
|
||||||
//! be placed in. For a much more in-depth explanation, consult the [relevant
|
//! be placed in. For a much more in-depth explanation, consult the [relevant
|
||||||
//! chapter] in the *For Developers* section of the user guide.
|
//! chapter] in the *For Developers* section of the user guide.
|
||||||
//!
|
//!
|
||||||
//! To make creating a backend easier, the `mdbook` crate can be imported
|
//! To make creating a backend easier, the `mdbook` crate can be imported
|
||||||
//! directly, making deserializing the `RenderContext` easy and giving you
|
//! directly, making deserializing the `RenderContext` easy and giving you
|
||||||
//! access to the various methods for working with the [`Config`].
|
//! access to the various methods for working with the [`Config`].
|
||||||
//!
|
//!
|
||||||
//! [user guide]: https://rust-lang-nursery.github.io/mdBook/
|
//! [user guide]: https://rust-lang-nursery.github.io/mdBook/
|
||||||
|
@ -122,6 +122,7 @@ pub mod errors {
|
||||||
HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"];
|
HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"];
|
||||||
HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"];
|
HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"];
|
||||||
Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
|
Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
|
||||||
|
SerdeJson(::serde_json::Error) #[doc = "JSON conversion failed"];
|
||||||
}
|
}
|
||||||
|
|
||||||
links {
|
links {
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
use renderer::html_handlebars::helpers;
|
|
||||||
use renderer::{RenderContext, Renderer};
|
|
||||||
use book::{Book, BookItem, Chapter};
|
use book::{Book, BookItem, Chapter};
|
||||||
use config::{Config, HtmlConfig, Playpen};
|
use config::{Config, HtmlConfig, Playpen};
|
||||||
use {theme, utils};
|
|
||||||
use theme::{playpen_editor, Theme};
|
|
||||||
use errors::*;
|
use errors::*;
|
||||||
use regex::{Captures, Regex};
|
use renderer::{RenderContext, Renderer};
|
||||||
|
use renderer::html_handlebars::helpers;
|
||||||
|
use theme::{self, Theme, playpen_editor};
|
||||||
|
use utils;
|
||||||
|
|
||||||
#[allow(unused_imports)] use std::ascii::AsciiExt;
|
#[allow(unused_imports)] use std::ascii::AsciiExt;
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::fs::{self, File};
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
|
use regex::{Captures, Regex};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -26,23 +25,10 @@ impl HtmlHandlebars {
|
||||||
HtmlHandlebars
|
HtmlHandlebars
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_file<P: AsRef<Path>>(
|
|
||||||
&self,
|
|
||||||
build_dir: &Path,
|
|
||||||
filename: P,
|
|
||||||
content: &[u8],
|
|
||||||
) -> Result<()> {
|
|
||||||
let path = build_dir.join(filename);
|
|
||||||
|
|
||||||
utils::fs::create_file(&path)?
|
|
||||||
.write_all(content)
|
|
||||||
.map_err(|e| e.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_item(
|
fn render_item(
|
||||||
&self,
|
&self,
|
||||||
item: &BookItem,
|
item: &BookItem,
|
||||||
mut ctx: RenderItemContext,
|
mut ctx: RenderItemContext,
|
||||||
print_content: &mut String,
|
print_content: &mut String,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// FIXME: This should be made DRY-er and rely less on mutable state
|
// FIXME: This should be made DRY-er and rely less on mutable state
|
||||||
|
@ -56,6 +42,11 @@ impl HtmlHandlebars {
|
||||||
let path = ch.path
|
let path = ch.path
|
||||||
.to_str()
|
.to_str()
|
||||||
.chain_err(|| "Could not convert path to str")?;
|
.chain_err(|| "Could not convert path to str")?;
|
||||||
|
let filepath = Path::new(&ch.path)
|
||||||
|
.with_extension("html");
|
||||||
|
let filepathstr = filepath.to_str()
|
||||||
|
.chain_err(|| "Could not convert HTML path to str")?;
|
||||||
|
let filepathstr = utils::fs::normalize_path(filepathstr);
|
||||||
|
|
||||||
// "print.html" is used for the print page.
|
// "print.html" is used for the print page.
|
||||||
if ch.path == Path::new("print.md") {
|
if ch.path == Path::new("print.md") {
|
||||||
|
@ -83,18 +74,15 @@ impl HtmlHandlebars {
|
||||||
debug!("Render template");
|
debug!("Render template");
|
||||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||||
|
|
||||||
let filepath = Path::new(&ch.path).with_extension("html");
|
|
||||||
let rendered = self.post_process(
|
let rendered = self.post_process(
|
||||||
rendered,
|
rendered,
|
||||||
&normalize_path(filepath.to_str().ok_or_else(|| {
|
&filepathstr,
|
||||||
Error::from(format!("Bad file name: {}", filepath.display()))
|
|
||||||
})?),
|
|
||||||
&ctx.html_config.playpen,
|
&ctx.html_config.playpen,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Write to file
|
// Write to file
|
||||||
debug!("Creating {} ✓", filepath.display());
|
debug!("Creating {} ✓", filepathstr);
|
||||||
self.write_file(&ctx.destination, filepath, &rendered.into_bytes())?;
|
utils::fs::write_file(&ctx.destination, &filepath, &rendered.into_bytes())?;
|
||||||
|
|
||||||
if ctx.is_index {
|
if ctx.is_index {
|
||||||
self.render_index(ch, &ctx.destination)?;
|
self.render_index(ch, &ctx.destination)?;
|
||||||
|
@ -123,7 +111,7 @@ impl HtmlHandlebars {
|
||||||
.collect::<Vec<&str>>()
|
.collect::<Vec<&str>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
self.write_file(destination, "index.html", content.as_bytes())?;
|
utils::fs::write_file(destination, "index.html", content.as_bytes())?;
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Creating index.html from {} ✓",
|
"Creating index.html from {} ✓",
|
||||||
|
@ -153,45 +141,47 @@ impl HtmlHandlebars {
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
html_config: &HtmlConfig,
|
html_config: &HtmlConfig,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.write_file(destination, "book.js", &theme.js)?;
|
use utils::fs::write_file;
|
||||||
self.write_file(destination, "book.css", &theme.css)?;
|
|
||||||
self.write_file(destination, "favicon.png", &theme.favicon)?;
|
write_file(destination, "book.js", &theme.js)?;
|
||||||
self.write_file(destination, "highlight.css", &theme.highlight_css)?;
|
write_file(destination, "book.css", &theme.css)?;
|
||||||
self.write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
|
write_file(destination, "favicon.png", &theme.favicon)?;
|
||||||
self.write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
|
write_file(destination, "highlight.css", &theme.highlight_css)?;
|
||||||
self.write_file(destination, "highlight.js", &theme.highlight_js)?;
|
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||||
self.write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
|
write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||||
self.write_file(
|
write_file(destination, "highlight.js", &theme.highlight_js)?;
|
||||||
|
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
|
||||||
|
write_file(
|
||||||
destination,
|
destination,
|
||||||
"_FontAwesome/css/font-awesome.css",
|
"_FontAwesome/css/font-awesome.css",
|
||||||
theme::FONT_AWESOME,
|
theme::FONT_AWESOME,
|
||||||
)?;
|
)?;
|
||||||
self.write_file(
|
write_file(
|
||||||
destination,
|
destination,
|
||||||
"_FontAwesome/fonts/fontawesome-webfont.eot",
|
"_FontAwesome/fonts/fontawesome-webfont.eot",
|
||||||
theme::FONT_AWESOME_EOT,
|
theme::FONT_AWESOME_EOT,
|
||||||
)?;
|
)?;
|
||||||
self.write_file(
|
write_file(
|
||||||
destination,
|
destination,
|
||||||
"_FontAwesome/fonts/fontawesome-webfont.svg",
|
"_FontAwesome/fonts/fontawesome-webfont.svg",
|
||||||
theme::FONT_AWESOME_SVG,
|
theme::FONT_AWESOME_SVG,
|
||||||
)?;
|
)?;
|
||||||
self.write_file(
|
write_file(
|
||||||
destination,
|
destination,
|
||||||
"_FontAwesome/fonts/fontawesome-webfont.ttf",
|
"_FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||||
theme::FONT_AWESOME_TTF,
|
theme::FONT_AWESOME_TTF,
|
||||||
)?;
|
)?;
|
||||||
self.write_file(
|
write_file(
|
||||||
destination,
|
destination,
|
||||||
"_FontAwesome/fonts/fontawesome-webfont.woff",
|
"_FontAwesome/fonts/fontawesome-webfont.woff",
|
||||||
theme::FONT_AWESOME_WOFF,
|
theme::FONT_AWESOME_WOFF,
|
||||||
)?;
|
)?;
|
||||||
self.write_file(
|
write_file(
|
||||||
destination,
|
destination,
|
||||||
"_FontAwesome/fonts/fontawesome-webfont.woff2",
|
"_FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||||
theme::FONT_AWESOME_WOFF2,
|
theme::FONT_AWESOME_WOFF2,
|
||||||
)?;
|
)?;
|
||||||
self.write_file(
|
write_file(
|
||||||
destination,
|
destination,
|
||||||
"_FontAwesome/fonts/FontAwesome.ttf",
|
"_FontAwesome/fonts/FontAwesome.ttf",
|
||||||
theme::FONT_AWESOME_TTF,
|
theme::FONT_AWESOME_TTF,
|
||||||
|
@ -200,16 +190,15 @@ impl HtmlHandlebars {
|
||||||
let playpen_config = &html_config.playpen;
|
let playpen_config = &html_config.playpen;
|
||||||
|
|
||||||
// Ace is a very large dependency, so only load it when requested
|
// Ace is a very large dependency, so only load it when requested
|
||||||
if playpen_config.editable {
|
if playpen_config.editable && playpen_config.copy_js {
|
||||||
// Load the editor
|
// Load the editor
|
||||||
let editor = playpen_editor::PlaypenEditor::new(&playpen_config.editor);
|
write_file(destination, "editor.js", playpen_editor::JS)?;
|
||||||
self.write_file(destination, "editor.js", &editor.js)?;
|
write_file(destination, "ace.js", playpen_editor::ACE_JS)?;
|
||||||
self.write_file(destination, "ace.js", &editor.ace_js)?;
|
write_file(destination, "mode-rust.js", playpen_editor::MODE_RUST_JS)?;
|
||||||
self.write_file(destination, "mode-rust.js", &editor.mode_rust_js)?;
|
write_file(destination, "theme-dawn.js", playpen_editor::THEME_DAWN_JS)?;
|
||||||
self.write_file(destination, "theme-dawn.js", &editor.theme_dawn_js)?;
|
write_file(destination,
|
||||||
self.write_file(destination,
|
|
||||||
"theme-tomorrow_night.js",
|
"theme-tomorrow_night.js",
|
||||||
&editor.theme_tomorrow_night_js,
|
playpen_editor::THEME_TOMORROW_NIGHT_JS,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,15 +296,19 @@ impl Renderer for HtmlHandlebars {
|
||||||
fs::create_dir_all(&destination)
|
fs::create_dir_all(&destination)
|
||||||
.chain_err(|| "Unexpected error when constructing destination path")?;
|
.chain_err(|| "Unexpected error when constructing destination path")?;
|
||||||
|
|
||||||
for (i, item) in book.iter().enumerate() {
|
let mut is_index = true;
|
||||||
|
for item in book.iter() {
|
||||||
let ctx = RenderItemContext {
|
let ctx = RenderItemContext {
|
||||||
handlebars: &handlebars,
|
handlebars: &handlebars,
|
||||||
destination: destination.to_path_buf(),
|
destination: destination.to_path_buf(),
|
||||||
data: data.clone(),
|
data: data.clone(),
|
||||||
is_index: i == 0,
|
is_index: is_index,
|
||||||
html_config: html_config.clone(),
|
html_config: html_config.clone(),
|
||||||
};
|
};
|
||||||
self.render_item(item, ctx, &mut print_content)?;
|
self.render_item(item,
|
||||||
|
ctx,
|
||||||
|
&mut print_content)?;
|
||||||
|
is_index = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print version
|
// Print version
|
||||||
|
@ -326,14 +319,13 @@ impl Renderer for HtmlHandlebars {
|
||||||
|
|
||||||
// Render the handlebars template with the data
|
// Render the handlebars template with the data
|
||||||
debug!("Render template");
|
debug!("Render template");
|
||||||
|
|
||||||
let rendered = handlebars.render("index", &data)?;
|
let rendered = handlebars.render("index", &data)?;
|
||||||
|
|
||||||
let rendered = self.post_process(rendered,
|
let rendered = self.post_process(rendered,
|
||||||
"print.html",
|
"print.html",
|
||||||
&html_config.playpen);
|
&html_config.playpen);
|
||||||
|
|
||||||
self.write_file(&destination, "print.html", &rendered.into_bytes())?;
|
utils::fs::write_file(&destination, "print.html", &rendered.into_bytes())?;
|
||||||
debug!("Creating print.html ✓");
|
debug!("Creating print.html ✓");
|
||||||
|
|
||||||
debug!("Copy static files");
|
debug!("Copy static files");
|
||||||
|
@ -342,6 +334,10 @@ impl Renderer for HtmlHandlebars {
|
||||||
self.copy_additional_css_and_js(&html_config, &ctx.root, &destination)
|
self.copy_additional_css_and_js(&html_config, &ctx.root, &destination)
|
||||||
.chain_err(|| "Unable to copy across additional CSS and JS")?;
|
.chain_err(|| "Unable to copy across additional CSS and JS")?;
|
||||||
|
|
||||||
|
// Render search index
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
super::search::create_files(&html_config.search.unwrap_or_default(), &destination, &book)?;
|
||||||
|
|
||||||
// Copy all remaining files
|
// Copy all remaining files
|
||||||
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
|
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
|
||||||
|
|
||||||
|
@ -405,16 +401,22 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
|
||||||
data.insert("additional_js".to_owned(), json!(js));
|
data.insert("additional_js".to_owned(), json!(js));
|
||||||
}
|
}
|
||||||
|
|
||||||
if html.playpen.editable {
|
if html.playpen.editable && html.playpen.copy_js {
|
||||||
data.insert("playpens_editable".to_owned(), json!(true));
|
data.insert("playpen_js".to_owned(), json!(true));
|
||||||
data.insert("editor_js".to_owned(), json!("editor.js"));
|
|
||||||
data.insert("ace_js".to_owned(), json!("ace.js"));
|
|
||||||
data.insert("mode_rust_js".to_owned(), json!("mode-rust.js"));
|
|
||||||
data.insert("theme_dawn_js".to_owned(), json!("theme-dawn.js"));
|
|
||||||
data.insert("theme_tomorrow_night_js".to_owned(),
|
|
||||||
json!("theme-tomorrow_night.js"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let search = html_config.search.clone();
|
||||||
|
if cfg!(feature = "search") {
|
||||||
|
data.insert("search_enabled".to_owned(), json!(true));
|
||||||
|
if search.unwrap_or_default().copy_js {
|
||||||
|
data.insert("search_js".to_owned(), json!(true));
|
||||||
|
}
|
||||||
|
} else if search.is_some() {
|
||||||
|
warn!("mdBook compiled without search support, ignoring `output.html.search` table");
|
||||||
|
warn!("please reinstall with `cargo install mdbook --force --features search`\
|
||||||
|
to use the search feature")
|
||||||
|
}
|
||||||
|
|
||||||
let mut chapters = vec![];
|
let mut chapters = vec![];
|
||||||
|
|
||||||
for item in book.iter() {
|
for item in book.iter() {
|
||||||
|
@ -469,7 +471,7 @@ fn wrap_header_with_link(level: usize,
|
||||||
id_counter: &mut HashMap<String, usize>,
|
id_counter: &mut HashMap<String, usize>,
|
||||||
filepath: &str)
|
filepath: &str)
|
||||||
-> String {
|
-> String {
|
||||||
let raw_id = id_from_content(content);
|
let raw_id = utils::id_from_content(content);
|
||||||
|
|
||||||
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
|
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
|
||||||
|
|
||||||
|
@ -489,33 +491,6 @@ fn wrap_header_with_link(level: usize,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate an id for use with anchors which is derived from a "normalised"
|
|
||||||
/// string.
|
|
||||||
fn id_from_content(content: &str) -> String {
|
|
||||||
let mut content = content.to_string();
|
|
||||||
|
|
||||||
// Skip any tags or html-encoded stuff
|
|
||||||
const REPL_SUB: &[&str] = &["<em>",
|
|
||||||
"</em>",
|
|
||||||
"<code>",
|
|
||||||
"</code>",
|
|
||||||
"<strong>",
|
|
||||||
"</strong>",
|
|
||||||
"<",
|
|
||||||
">",
|
|
||||||
"&",
|
|
||||||
"'",
|
|
||||||
"""];
|
|
||||||
for sub in REPL_SUB {
|
|
||||||
content = content.replace(sub, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove spaces and hastags indicating a header
|
|
||||||
let trimmed = content.trim().trim_left_matches('#').trim();
|
|
||||||
|
|
||||||
normalize_id(trimmed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// anchors to the same page (href="#anchor") do not work because of
|
// anchors to the same page (href="#anchor") do not work because of
|
||||||
// <base href="../"> pointing to the root folder. This function *fixes*
|
// <base href="../"> pointing to the root folder. This function *fixes*
|
||||||
// that in a very inelegant way
|
// that in a very inelegant way
|
||||||
|
@ -555,8 +530,7 @@ fn fix_code_blocks(html: &str) -> String {
|
||||||
before = before,
|
before = before,
|
||||||
classes = classes,
|
classes = classes,
|
||||||
after = after)
|
after = after)
|
||||||
})
|
}).into_owned()
|
||||||
.into_owned()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||||
|
@ -591,8 +565,7 @@ fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||||
// not language-rust, so no-op
|
// not language-rust, so no-op
|
||||||
text.to_owned()
|
text.to_owned()
|
||||||
}
|
}
|
||||||
})
|
}).into_owned()
|
||||||
.into_owned()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn partition_source(s: &str) -> (String, String) {
|
fn partition_source(s: &str) -> (String, String) {
|
||||||
|
@ -624,26 +597,6 @@ struct RenderItemContext<'a> {
|
||||||
html_config: HtmlConfig,
|
html_config: HtmlConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normalize_path(path: &str) -> String {
|
|
||||||
use std::path::is_separator;
|
|
||||||
path.chars()
|
|
||||||
.map(|ch| if is_separator(ch) { '/' } else { ch })
|
|
||||||
.collect::<String>()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn normalize_id(content: &str) -> String {
|
|
||||||
content.chars()
|
|
||||||
.filter_map(|ch| if ch.is_alphanumeric() || ch == '_' || ch == '-' {
|
|
||||||
Some(ch.to_ascii_lowercase())
|
|
||||||
} else if ch.is_whitespace() {
|
|
||||||
Some('-')
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
.collect::<String>()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -687,12 +640,4 @@ mod tests {
|
||||||
assert_eq!(got, should_be);
|
assert_eq!(got, should_be);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn anchor_generation() {
|
|
||||||
assert_eq!(id_from_content("## `--passes`: add more rustdoc passes"),
|
|
||||||
"--passes-add-more-rustdoc-passes");
|
|
||||||
assert_eq!(id_from_content("## Method-call expressions"),
|
|
||||||
"method-call-expressions");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
pub mod navigation;
|
|
||||||
pub mod toc;
|
pub mod toc;
|
||||||
|
pub mod navigation;
|
||||||
|
|
|
@ -4,3 +4,6 @@ pub use self::hbs_renderer::HtmlHandlebars;
|
||||||
|
|
||||||
mod hbs_renderer;
|
mod hbs_renderer;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
|
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
mod search;
|
||||||
|
|
|
@ -0,0 +1,241 @@
|
||||||
|
extern crate ammonia;
|
||||||
|
extern crate elasticlunr;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use pulldown_cmark::*;
|
||||||
|
use serde_json;
|
||||||
|
use self::elasticlunr::Index;
|
||||||
|
|
||||||
|
use book::{Book, BookItem};
|
||||||
|
use config::Search;
|
||||||
|
use errors::*;
|
||||||
|
use utils;
|
||||||
|
use theme::searcher;
|
||||||
|
|
||||||
|
/// 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"]);
|
||||||
|
|
||||||
|
for item in book.iter() {
|
||||||
|
render_item(&mut index, &search_config, item)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = write_to_js(index, &search_config)?;
|
||||||
|
debug!("Writing search index ✓");
|
||||||
|
|
||||||
|
if search_config.copy_js {
|
||||||
|
utils::fs::write_file(destination, "searchindex.js", index.as_bytes())?;
|
||||||
|
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
||||||
|
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
||||||
|
utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
|
||||||
|
debug!("Copying search files ✓");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uses the given arguments to construct a search document, then inserts it to the given index.
|
||||||
|
fn add_doc<'a>(
|
||||||
|
index: &mut Index,
|
||||||
|
anchor_base: &'a str,
|
||||||
|
section_id: &Option<String>,
|
||||||
|
items: &[&str],
|
||||||
|
) {
|
||||||
|
let doc_ref: Cow<'a, str> = if let &Some(ref id) = section_id {
|
||||||
|
format!("{}#{}", anchor_base, id).into()
|
||||||
|
} else {
|
||||||
|
anchor_base.into()
|
||||||
|
};
|
||||||
|
let doc_ref = utils::collapse_whitespace(doc_ref.trim());
|
||||||
|
let items = items.iter().map(|&x| utils::collapse_whitespace(x.trim()));
|
||||||
|
index.add_doc(&doc_ref, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders markdown into flat unformatted text and adds it to the search index.
|
||||||
|
fn render_item(
|
||||||
|
index: &mut Index,
|
||||||
|
search_config: &Search,
|
||||||
|
item: &BookItem,
|
||||||
|
) -> Result<()> {
|
||||||
|
let chapter = match item {
|
||||||
|
&BookItem::Chapter(ref ch) => ch,
|
||||||
|
_ => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let filepath = Path::new(&chapter.path).with_extension("html");
|
||||||
|
let filepath = filepath
|
||||||
|
.to_str()
|
||||||
|
.chain_err(|| "Could not convert HTML path to str")?;
|
||||||
|
let anchor_base = utils::fs::normalize_path(filepath);
|
||||||
|
|
||||||
|
let mut opts = Options::empty();
|
||||||
|
opts.insert(OPTION_ENABLE_TABLES);
|
||||||
|
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
||||||
|
let p = Parser::new_ext(&chapter.content, opts);
|
||||||
|
|
||||||
|
let mut in_header = false;
|
||||||
|
let max_section_depth = search_config.heading_split_level as i32;
|
||||||
|
let mut section_id = None;
|
||||||
|
let mut heading = String::new();
|
||||||
|
let mut body = String::new();
|
||||||
|
let mut breadcrumbs = chapter.parent_names.clone();
|
||||||
|
let mut footnote_numbers = HashMap::new();
|
||||||
|
|
||||||
|
for event in p {
|
||||||
|
match event {
|
||||||
|
Event::Start(Tag::Header(i)) if i <= max_section_depth => {
|
||||||
|
if heading.len() > 0 {
|
||||||
|
// Section finished, the next header is following now
|
||||||
|
// Write the data to the index, and clear it for the next section
|
||||||
|
add_doc(
|
||||||
|
index,
|
||||||
|
&anchor_base,
|
||||||
|
§ion_id,
|
||||||
|
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||||
|
);
|
||||||
|
section_id = None;
|
||||||
|
heading.clear();
|
||||||
|
body.clear();
|
||||||
|
breadcrumbs.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
in_header = true;
|
||||||
|
}
|
||||||
|
Event::End(Tag::Header(i)) if i <= max_section_depth => {
|
||||||
|
in_header = false;
|
||||||
|
section_id = Some(utils::id_from_content(&heading));
|
||||||
|
breadcrumbs.push(heading.clone());
|
||||||
|
}
|
||||||
|
Event::Start(Tag::FootnoteDefinition(name)) => {
|
||||||
|
let number = footnote_numbers.len() + 1;
|
||||||
|
footnote_numbers.entry(name).or_insert(number);
|
||||||
|
}
|
||||||
|
Event::Start(_) | Event::End(_) | Event::SoftBreak | Event::HardBreak => {
|
||||||
|
// Insert spaces where HTML output would usually seperate text
|
||||||
|
// to ensure words don't get merged together
|
||||||
|
if in_header {
|
||||||
|
heading.push(' ');
|
||||||
|
} else {
|
||||||
|
body.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Text(text) => {
|
||||||
|
if in_header {
|
||||||
|
heading.push_str(&text);
|
||||||
|
} else {
|
||||||
|
body.push_str(&text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Html(html) | Event::InlineHtml(html) => {
|
||||||
|
body.push_str(&clean_html(&html));
|
||||||
|
}
|
||||||
|
Event::FootnoteReference(name) => {
|
||||||
|
let len = footnote_numbers.len() + 1;
|
||||||
|
let number = footnote_numbers.entry(name).or_insert(len);
|
||||||
|
body.push_str(&format!(" [{}] ", number));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if heading.len() > 0 {
|
||||||
|
// Make sure the last section is added to the index
|
||||||
|
add_doc(
|
||||||
|
index,
|
||||||
|
&anchor_base,
|
||||||
|
§ion_id,
|
||||||
|
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exports the index and search options to a JS script which stores the index in `window.search`.
|
||||||
|
/// Using a JS script is a workaround for CORS in `file://` URIs. It also removes the need for
|
||||||
|
/// downloading/parsing JSON in JS.
|
||||||
|
fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
|
||||||
|
// These structs mirror the configuration javascript object accepted by
|
||||||
|
// http://elasticlunr.com/docs/configuration.js.html
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SearchOptionsField {
|
||||||
|
boost: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SearchOptionsFields {
|
||||||
|
title: SearchOptionsField,
|
||||||
|
body: SearchOptionsField,
|
||||||
|
breadcrumbs: SearchOptionsField,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SearchOptions {
|
||||||
|
bool: String,
|
||||||
|
expand: bool,
|
||||||
|
limit_results: u32,
|
||||||
|
teaser_word_count: u32,
|
||||||
|
fields: SearchOptionsFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SearchindexJson {
|
||||||
|
/// The searchoptions for elasticlunr.js
|
||||||
|
searchoptions: SearchOptions,
|
||||||
|
/// The index for elasticlunr.js
|
||||||
|
index: elasticlunr::Index,
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchoptions = SearchOptions {
|
||||||
|
bool: if search_config.use_boolean_and {
|
||||||
|
"AND".into()
|
||||||
|
} else {
|
||||||
|
"OR".into()
|
||||||
|
},
|
||||||
|
expand: search_config.expand,
|
||||||
|
limit_results: search_config.limit_results,
|
||||||
|
teaser_word_count: search_config.teaser_word_count,
|
||||||
|
fields: SearchOptionsFields {
|
||||||
|
title: SearchOptionsField {
|
||||||
|
boost: search_config.boost_title,
|
||||||
|
},
|
||||||
|
body: SearchOptionsField {
|
||||||
|
boost: search_config.boost_paragraph,
|
||||||
|
},
|
||||||
|
breadcrumbs: SearchOptionsField {
|
||||||
|
boost: search_config.boost_hierarchy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let json_contents = SearchindexJson {
|
||||||
|
searchoptions: searchoptions,
|
||||||
|
index: index,
|
||||||
|
};
|
||||||
|
let json_contents = serde_json::to_string(&json_contents)?;
|
||||||
|
|
||||||
|
Ok(format!("window.search = {};", json_contents))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_html(html: &str) -> String {
|
||||||
|
lazy_static! {
|
||||||
|
static ref AMMONIA: ammonia::Builder<'static> = {
|
||||||
|
let mut clean_content = HashSet::new();
|
||||||
|
clean_content.insert("script");
|
||||||
|
clean_content.insert("style");
|
||||||
|
let mut builder = ammonia::Builder::new();
|
||||||
|
builder
|
||||||
|
.tags(HashSet::new())
|
||||||
|
.tag_attributes(HashMap::new())
|
||||||
|
.generic_attributes(HashSet::new())
|
||||||
|
.link_rel(None)
|
||||||
|
.allowed_classes(HashMap::new())
|
||||||
|
.clean_content_tags(clean_content);
|
||||||
|
builder
|
||||||
|
};
|
||||||
|
}
|
||||||
|
AMMONIA.clean(html).to_string()
|
||||||
|
}
|
|
@ -36,6 +36,15 @@ h5 {
|
||||||
.header + .header h5 {
|
.header + .header h5 {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
a.header:target h1:before,
|
||||||
|
a.header:target h2:before,
|
||||||
|
a.header:target h3:before,
|
||||||
|
a.header:target h4:before {
|
||||||
|
display: inline-block;
|
||||||
|
content: "»";
|
||||||
|
margin-left: -30px;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
table {
|
table {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
@ -337,6 +346,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||||
color: #333;
|
color: #333;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
/* Inline code */
|
/* Inline code */
|
||||||
|
/* Search */
|
||||||
}
|
}
|
||||||
.light .content .header:link,
|
.light .content .header:link,
|
||||||
.light .content .header:visited {
|
.light .content .header:visited {
|
||||||
|
@ -405,6 +415,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||||
.light .mobile-nav-chapters {
|
.light .mobile-nav-chapters {
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
}
|
}
|
||||||
|
.light #searchresults a,
|
||||||
.light .content a:link,
|
.light .content a:link,
|
||||||
.light a:visited,
|
.light a:visited,
|
||||||
.light a > .hljs {
|
.light a > .hljs {
|
||||||
|
@ -499,10 +510,34 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||||
.light ::-webkit-scrollbar-thumb {
|
.light ::-webkit-scrollbar-thumb {
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
}
|
}
|
||||||
|
.light #searchbar {
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.light #searchbar:focus,
|
||||||
|
.light #searchbar.active {
|
||||||
|
-webkit-box-shadow: 0 0 3px #aaa;
|
||||||
|
box-shadow: 0 0 3px #aaa;
|
||||||
|
}
|
||||||
|
.light .searchresults-header {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.light .searchresults-outer {
|
||||||
|
border-bottom: 1px dashed #888;
|
||||||
|
}
|
||||||
|
.light ul#searchresults li.focus {
|
||||||
|
background-color: #e4f2fe;
|
||||||
|
}
|
||||||
|
.light mark {
|
||||||
|
background-color: #a2cff5;
|
||||||
|
}
|
||||||
.coal {
|
.coal {
|
||||||
color: #98a3ad;
|
color: #98a3ad;
|
||||||
background-color: #141617;
|
background-color: #141617;
|
||||||
/* Inline code */
|
/* Inline code */
|
||||||
|
/* Search */
|
||||||
}
|
}
|
||||||
.coal .content .header:link,
|
.coal .content .header:link,
|
||||||
.coal .content .header:visited {
|
.coal .content .header:visited {
|
||||||
|
@ -571,6 +606,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||||
.coal .mobile-nav-chapters {
|
.coal .mobile-nav-chapters {
|
||||||
background-color: #292c2f;
|
background-color: #292c2f;
|
||||||
}
|
}
|
||||||
|
.coal #searchresults a,
|
||||||
.coal .content a:link,
|
.coal .content a:link,
|
||||||
.coal a:visited,
|
.coal a:visited,
|
||||||
.coal a > .hljs {
|
.coal a > .hljs {
|
||||||
|
@ -665,10 +701,34 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||||
.coal ::-webkit-scrollbar-thumb {
|
.coal ::-webkit-scrollbar-thumb {
|
||||||
background: #a1adb8;
|
background: #a1adb8;
|
||||||
}
|
}
|
||||||
|
.coal #searchbar {
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #b7b7b7;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.coal #searchbar:focus,
|
||||||
|
.coal #searchbar.active {
|
||||||
|
-webkit-box-shadow: 0 0 3px #aaa;
|
||||||
|
box-shadow: 0 0 3px #aaa;
|
||||||
|
}
|
||||||
|
.coal .searchresults-header {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.coal .searchresults-outer {
|
||||||
|
border-bottom: 1px dashed #98a3ad;
|
||||||
|
}
|
||||||
|
.coal ul#searchresults li.focus {
|
||||||
|
background-color: #2b2b2f;
|
||||||
|
}
|
||||||
|
.coal mark {
|
||||||
|
background-color: #355c7d;
|
||||||
|
}
|
||||||
.navy {
|
.navy {
|
||||||
color: #bcbdd0;
|
color: #bcbdd0;
|
||||||
background-color: #161923;
|
background-color: #161923;
|
||||||
/* Inline code */
|
/* Inline code */
|
||||||
|
/* Search */
|
||||||
}
|
}
|
||||||
.navy .content .header:link,
|
.navy .content .header:link,
|
||||||
.navy .content .header:visited {
|
.navy .content .header:visited {
|
||||||
|
@ -737,6 +797,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||||
.navy .mobile-nav-chapters {
|
.navy .mobile-nav-chapters {
|
||||||
background-color: #282d3f;
|
background-color: #282d3f;
|
||||||
}
|
}
|
||||||
|
.navy #searchresults a,
|
||||||
.navy .content a:link,
|
.navy .content a:link,
|
||||||
.navy a:visited,
|
.navy a:visited,
|
||||||
.navy a > .hljs {
|
.navy a > .hljs {
|
||||||
|
@ -831,10 +892,34 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||||
.navy ::-webkit-scrollbar-thumb {
|
.navy ::-webkit-scrollbar-thumb {
|
||||||
background: #c8c9db;
|
background: #c8c9db;
|
||||||
}
|
}
|
||||||
|
.navy #searchbar {
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #aeaec6;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.navy #searchbar:focus,
|
||||||
|
.navy #searchbar.active {
|
||||||
|
-webkit-box-shadow: 0 0 3px #aaa;
|
||||||
|
box-shadow: 0 0 3px #aaa;
|
||||||
|
}
|
||||||
|
.navy .searchresults-header {
|
||||||
|
color: #5f5f71;
|
||||||
|
}
|
||||||
|
.navy .searchresults-outer {
|
||||||
|
border-bottom: 1px dashed #5c5c68;
|
||||||
|
}
|
||||||
|
.navy ul#searchresults li.focus {
|
||||||
|
background-color: #242430;
|
||||||
|
}
|
||||||
|
.navy mark {
|
||||||
|
background-color: #a2cff5;
|
||||||
|
}
|
||||||
.rust {
|
.rust {
|
||||||
color: #262625;
|
color: #262625;
|
||||||
background-color: #e1e1db;
|
background-color: #e1e1db;
|
||||||
/* Inline code */
|
/* Inline code */
|
||||||
|
/* Search */
|
||||||
}
|
}
|
||||||
.rust .content .header:link,
|
.rust .content .header:link,
|
||||||
.rust .content .header:visited {
|
.rust .content .header:visited {
|
||||||
|
@ -903,6 +988,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||||
.rust .mobile-nav-chapters {
|
.rust .mobile-nav-chapters {
|
||||||
background-color: #3b2e2a;
|
background-color: #3b2e2a;
|
||||||
}
|
}
|
||||||
|
.rust #searchresults a,
|
||||||
.rust .content a:link,
|
.rust .content a:link,
|
||||||
.rust a:visited,
|
.rust a:visited,
|
||||||
.rust a > .hljs {
|
.rust a > .hljs {
|
||||||
|
@ -997,10 +1083,34 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||||
.rust ::-webkit-scrollbar-thumb {
|
.rust ::-webkit-scrollbar-thumb {
|
||||||
background: #c8c9db;
|
background: #c8c9db;
|
||||||
}
|
}
|
||||||
|
.rust #searchbar {
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.rust #searchbar:focus,
|
||||||
|
.rust #searchbar.active {
|
||||||
|
-webkit-box-shadow: 0 0 3px #aaa;
|
||||||
|
box-shadow: 0 0 3px #aaa;
|
||||||
|
}
|
||||||
|
.rust .searchresults-header {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.rust .searchresults-outer {
|
||||||
|
border-bottom: 1px dashed #888;
|
||||||
|
}
|
||||||
|
.rust ul#searchresults li.focus {
|
||||||
|
background-color: #dec2a2;
|
||||||
|
}
|
||||||
|
.rust mark {
|
||||||
|
background-color: #e69f67;
|
||||||
|
}
|
||||||
.ayu {
|
.ayu {
|
||||||
color: #c5c5c5;
|
color: #c5c5c5;
|
||||||
background-color: #0f1419;
|
background-color: #0f1419;
|
||||||
/* Inline code */
|
/* Inline code */
|
||||||
|
/* Search */
|
||||||
}
|
}
|
||||||
.ayu .content .header:link,
|
.ayu .content .header:link,
|
||||||
.ayu .content .header:visited {
|
.ayu .content .header:visited {
|
||||||
|
@ -1069,6 +1179,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||||
.ayu .mobile-nav-chapters {
|
.ayu .mobile-nav-chapters {
|
||||||
background-color: #14191f;
|
background-color: #14191f;
|
||||||
}
|
}
|
||||||
|
.ayu #searchresults a,
|
||||||
.ayu .content a:link,
|
.ayu .content a:link,
|
||||||
.ayu a:visited,
|
.ayu a:visited,
|
||||||
.ayu a > .hljs {
|
.ayu a > .hljs {
|
||||||
|
@ -1163,6 +1274,29 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||||
.ayu ::-webkit-scrollbar-thumb {
|
.ayu ::-webkit-scrollbar-thumb {
|
||||||
background: #c8c9db;
|
background: #c8c9db;
|
||||||
}
|
}
|
||||||
|
.ayu #searchbar {
|
||||||
|
border: 1px solid #848484;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #424242;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.ayu #searchbar:focus,
|
||||||
|
.ayu #searchbar.active {
|
||||||
|
-webkit-box-shadow: 0 0 3px #d4c89f;
|
||||||
|
box-shadow: 0 0 3px #d4c89f;
|
||||||
|
}
|
||||||
|
.ayu .searchresults-header {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.ayu .searchresults-outer {
|
||||||
|
border-bottom: 1px dashed #888;
|
||||||
|
}
|
||||||
|
.ayu ul#searchresults li.focus {
|
||||||
|
background-color: #252932;
|
||||||
|
}
|
||||||
|
.ayu mark {
|
||||||
|
background-color: #e3b171;
|
||||||
|
}
|
||||||
@media only print {
|
@media only print {
|
||||||
#sidebar,
|
#sidebar,
|
||||||
#menu-bar,
|
#menu-bar,
|
||||||
|
@ -1243,3 +1377,66 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||||
.tooltipped .tooltiptext {
|
.tooltipped .tooltiptext {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
#searchresults a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
mark {
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0 3px 1px 3px;
|
||||||
|
margin: 0 -3px -1px -3px;
|
||||||
|
-webkit-transition: background-color 300ms linear;
|
||||||
|
-moz-transition: background-color 300ms linear;
|
||||||
|
-o-transition: background-color 300ms linear;
|
||||||
|
-ms-transition: background-color 300ms linear;
|
||||||
|
transition: background-color 300ms linear;
|
||||||
|
}
|
||||||
|
.fade-out {
|
||||||
|
background-color: rgba(0,0,0,0) !important;
|
||||||
|
}
|
||||||
|
.searchbar-outer {
|
||||||
|
display: none;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 750px;
|
||||||
|
}
|
||||||
|
#searchbar {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 5px auto 0px auto;
|
||||||
|
padding: 10px 16px;
|
||||||
|
-webkit-transition: box-shadow 300ms ease-in-out;
|
||||||
|
-moz-transition: box-shadow 300ms ease-in-out;
|
||||||
|
-o-transition: box-shadow 300ms ease-in-out;
|
||||||
|
-ms-transition: box-shadow 300ms ease-in-out;
|
||||||
|
transition: box-shadow 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
.searchresults-header {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 18px 0 0 5px;
|
||||||
|
}
|
||||||
|
.searchresults-outer {
|
||||||
|
display: none;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 750px;
|
||||||
|
}
|
||||||
|
ul#searchresults {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
ul#searchresults li {
|
||||||
|
margin: 10px 0px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
ul#searchresults span.teaser {
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
margin: 5px 0 0 20px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
ul#searchresults span.teaser em {
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
|
@ -499,6 +499,7 @@ function playpen_text(playpen) {
|
||||||
(function chapterNavigation() {
|
(function chapterNavigation() {
|
||||||
document.addEventListener('keydown', function (e) {
|
document.addEventListener('keydown', function (e) {
|
||||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||||
|
if (window.search && window.search.hasFocus()) { return; }
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<!DOCTYPE HTML>
|
<!DOCTYPE HTML>
|
||||||
<html lang="{{ language }}" class="sidebar-visible">
|
<html lang="{{ language }}" class="sidebar-visible">
|
||||||
<head>
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{{ title }}</title>
|
<title>{{ title }}</title>
|
||||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
@ -112,9 +113,14 @@
|
||||||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
{{#if search_enabled}}
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="menu-title">{{ book_title }}</h1>
|
<h1 class="menu-title">{{ book_title }}</h1>
|
||||||
|
|
||||||
<div class="right-buttons">
|
<div class="right-buttons">
|
||||||
<a href="print.html" title="Print this book" aria-label="Print this book">
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
@ -124,6 +130,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#if search_enabled}}
|
||||||
|
<div id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</div>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer">
|
||||||
|
<div class="searchresults-header" id="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
@ -221,12 +238,21 @@
|
||||||
</script>
|
</script>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if playpens_editable}}
|
{{#if playpen_js}}
|
||||||
<script src="{{ ace_js }}" type="text/javascript" charset="utf-8"></script>
|
<script src="ace.js" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="{{ editor_js }}" type="text/javascript" charset="utf-8"></script>
|
<script src="editor.js" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="{{ mode_rust_js }}" type="text/javascript" charset="utf-8"></script>
|
<script src="mode-rust.js" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="{{ theme_dawn_js }}" type="text/javascript" charset="utf-8"></script>
|
<script src="theme-dawn.js" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="{{ theme_tomorrow_night_js }}" type="text/javascript" charset="utf-8"></script>
|
<script src="theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if search_enabled}}
|
||||||
|
<script src="searchindex.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
{{/if}}
|
||||||
|
{{#if search_js}}
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if is_print}}
|
{{#if is_print}}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
#![allow(missing_docs)] // FIXME: Document this
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
pub mod playpen_editor;
|
pub mod playpen_editor;
|
||||||
|
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
pub mod searcher;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
@ -52,6 +56,8 @@ pub struct Theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Theme {
|
impl Theme {
|
||||||
|
/// Creates a `Theme` from the given `theme_dir`.
|
||||||
|
/// If a file is found in the theme dir, it will override the default version.
|
||||||
pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
|
pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
|
||||||
let theme_dir = theme_dir.as_ref();
|
let theme_dir = theme_dir.as_ref();
|
||||||
let mut theme = Theme::default();
|
let mut theme = Theme::default();
|
||||||
|
|
|
@ -1,70 +1,7 @@
|
||||||
use std::path::Path;
|
//! Theme dependencies for the playpen editor.
|
||||||
|
|
||||||
use theme::load_file_contents;
|
|
||||||
|
|
||||||
pub static JS: &'static [u8] = include_bytes!("editor.js");
|
pub static JS: &'static [u8] = include_bytes!("editor.js");
|
||||||
pub static ACE_JS: &'static [u8] = include_bytes!("ace.js");
|
pub static ACE_JS: &'static [u8] = include_bytes!("ace.js");
|
||||||
pub static MODE_RUST_JS: &'static [u8] = include_bytes!("mode-rust.js");
|
pub static MODE_RUST_JS: &'static [u8] = include_bytes!("mode-rust.js");
|
||||||
pub static THEME_DAWN_JS: &'static [u8] = include_bytes!("theme-dawn.js");
|
pub static THEME_DAWN_JS: &'static [u8] = include_bytes!("theme-dawn.js");
|
||||||
pub static THEME_TOMORROW_NIGHT_JS: &'static [u8] = include_bytes!("theme-tomorrow_night.js");
|
pub static THEME_TOMORROW_NIGHT_JS: &'static [u8] = include_bytes!("theme-tomorrow_night.js");
|
||||||
|
|
||||||
/// Integration of a JavaScript editor for playpens.
|
|
||||||
/// Uses the Ace editor: https://ace.c9.io/.
|
|
||||||
/// The Ace editor itself, the mode, and the theme files are the
|
|
||||||
/// generated minified no conflict versions.
|
|
||||||
///
|
|
||||||
/// The `PlaypenEditor` struct should be used instead of the static variables because
|
|
||||||
/// the `new()` method
|
|
||||||
/// will look if the user has an editor directory in his source folder and use
|
|
||||||
/// the users editor instead
|
|
||||||
/// of the default.
|
|
||||||
///
|
|
||||||
/// You should exceptionnaly use the static variables only if you need the
|
|
||||||
/// default editor even if the
|
|
||||||
/// user has specified another editor.
|
|
||||||
pub struct PlaypenEditor {
|
|
||||||
pub js: Vec<u8>,
|
|
||||||
pub ace_js: Vec<u8>,
|
|
||||||
pub mode_rust_js: Vec<u8>,
|
|
||||||
pub theme_dawn_js: Vec<u8>,
|
|
||||||
pub theme_tomorrow_night_js: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlaypenEditor {
|
|
||||||
pub fn new(src: &Path) -> Self {
|
|
||||||
let mut editor = PlaypenEditor {
|
|
||||||
js: JS.to_owned(),
|
|
||||||
ace_js: ACE_JS.to_owned(),
|
|
||||||
mode_rust_js: MODE_RUST_JS.to_owned(),
|
|
||||||
theme_dawn_js: THEME_DAWN_JS.to_owned(),
|
|
||||||
theme_tomorrow_night_js: THEME_TOMORROW_NIGHT_JS.to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if the given path exists
|
|
||||||
if !src.exists() || !src.is_dir() {
|
|
||||||
return editor;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for individual files if they exist
|
|
||||||
{
|
|
||||||
let files = vec![(src.join("editor.js"), &mut editor.js),
|
|
||||||
(src.join("ace.js"), &mut editor.ace_js),
|
|
||||||
(src.join("mode-rust.js"), &mut editor.mode_rust_js),
|
|
||||||
(src.join("theme-dawn.js"), &mut editor.theme_dawn_js),
|
|
||||||
(src.join("theme-tomorrow_night.js"),
|
|
||||||
&mut editor.theme_tomorrow_night_js)];
|
|
||||||
|
|
||||||
for (filename, dest) in files {
|
|
||||||
if !filename.exists() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = load_file_contents(&filename, dest) {
|
|
||||||
warn!("Couldn't load custom file, {}: {}", filename.display(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
editor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,6 @@
|
||||||
|
//! Theme dependencies for in-browser search. Not included in mdbook when
|
||||||
|
//! the "search" cargo feature is disabled.
|
||||||
|
|
||||||
|
pub static JS: &'static [u8] = include_bytes!("searcher.js");
|
||||||
|
pub static MARK_JS: &'static [u8] = include_bytes!("mark.min.js");
|
||||||
|
pub static ELASTICLUNR_JS: &'static [u8] = include_bytes!("elasticlunr.min.js");
|
|
@ -0,0 +1,457 @@
|
||||||
|
window.search = window.search || {};
|
||||||
|
(function search(search) {
|
||||||
|
// Search functionality
|
||||||
|
//
|
||||||
|
// You can use !hasFocus() to prevent keyhandling in your key
|
||||||
|
// event handlers while the user is typing his search.
|
||||||
|
|
||||||
|
if (!Mark || !elasticlunr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchbar = document.getElementById('searchbar'),
|
||||||
|
searchbar_outer = document.getElementById('searchbar-outer'),
|
||||||
|
searchresults = document.getElementById('searchresults'),
|
||||||
|
searchresults_outer = document.getElementById('searchresults-outer'),
|
||||||
|
searchresults_header = document.getElementById('searchresults-header'),
|
||||||
|
searchicon = document.getElementById('search-toggle'),
|
||||||
|
content = document.getElementById('content'),
|
||||||
|
|
||||||
|
searchindex = null,
|
||||||
|
searchoptions = {
|
||||||
|
bool: "AND",
|
||||||
|
expand: true,
|
||||||
|
teaser_word_count: 30,
|
||||||
|
limit_results: 30,
|
||||||
|
fields: {
|
||||||
|
title: {boost: 1},
|
||||||
|
body: {boost: 1},
|
||||||
|
breadcrumbs: {boost: 0}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mark_exclude = [],
|
||||||
|
marker = new Mark(content),
|
||||||
|
current_searchterm = "",
|
||||||
|
URL_SEARCH_PARAM = 'search',
|
||||||
|
URL_MARK_PARAM = 'highlight',
|
||||||
|
teaser_count = 0,
|
||||||
|
|
||||||
|
SEARCH_HOTKEY_KEYCODE = 83,
|
||||||
|
ESCAPE_KEYCODE = 27,
|
||||||
|
DOWN_KEYCODE = 40,
|
||||||
|
UP_KEYCODE = 38,
|
||||||
|
SELECT_KEYCODE = 13;
|
||||||
|
|
||||||
|
function hasFocus() {
|
||||||
|
return searchbar === document.activeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeChildren(elem) {
|
||||||
|
while (elem.firstChild) {
|
||||||
|
elem.removeChild(elem.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to parse a url into its building blocks.
|
||||||
|
function parseURL(url) {
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
return {
|
||||||
|
source: url,
|
||||||
|
protocol: a.protocol.replace(':',''),
|
||||||
|
host: a.hostname,
|
||||||
|
port: a.port,
|
||||||
|
params: (function(){
|
||||||
|
var ret = {};
|
||||||
|
var seg = a.search.replace(/^\?/,'').split('&');
|
||||||
|
var len = seg.length, i = 0, s;
|
||||||
|
for (;i<len;i++) {
|
||||||
|
if (!seg[i]) { continue; }
|
||||||
|
s = seg[i].split('=');
|
||||||
|
ret[s[0]] = s[1];
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
})(),
|
||||||
|
file: (a.pathname.match(/\/([^/?#]+)$/i) || [,''])[1],
|
||||||
|
hash: a.hash.replace('#',''),
|
||||||
|
path: a.pathname.replace(/^([^/])/,'/$1')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to recreate a url string from its building blocks.
|
||||||
|
function renderURL(urlobject) {
|
||||||
|
var url = urlobject.protocol + "://" + urlobject.host;
|
||||||
|
if (urlobject.port != "") {
|
||||||
|
url += ":" + urlobject.port;
|
||||||
|
}
|
||||||
|
url += urlobject.path;
|
||||||
|
var joiner = "?";
|
||||||
|
for(var prop in urlobject.params) {
|
||||||
|
if(urlobject.params.hasOwnProperty(prop)) {
|
||||||
|
url += joiner + prop + "=" + urlobject.params[prop];
|
||||||
|
joiner = "&";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (urlobject.hash != "") {
|
||||||
|
url += "#" + urlobject.hash;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to escape html special chars for displaying the teasers
|
||||||
|
var escapeHTML = (function() {
|
||||||
|
var MAP = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
var repl = function(c) { return MAP[c]; };
|
||||||
|
return function(s) {
|
||||||
|
return s.replace(/[&<>'"]/g, repl);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
function formatSearchMetric(count, searchterm) {
|
||||||
|
if (count == 1) {
|
||||||
|
return count + " search result for '" + searchterm + "':";
|
||||||
|
} else if (count == 0) {
|
||||||
|
return "No search results for '" + searchterm + "'.";
|
||||||
|
} else {
|
||||||
|
return count + " search results for '" + searchterm + "':";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSearchResult(result, searchterms) {
|
||||||
|
var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
|
||||||
|
teaser_count++;
|
||||||
|
|
||||||
|
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
|
||||||
|
var url = result.ref.split("#");
|
||||||
|
if (url.length == 1) { // no anchor found
|
||||||
|
url.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<a href="' + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
|
||||||
|
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
|
||||||
|
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
|
||||||
|
+ teaser + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTeaser(body, searchterms) {
|
||||||
|
// The strategy is as follows:
|
||||||
|
// First, assign a value to each word in the document:
|
||||||
|
// Words that correspond to search terms (stemmer aware): 40
|
||||||
|
// Normal words: 2
|
||||||
|
// First word in a sentence: 8
|
||||||
|
// Then use a sliding window with a constant number of words and count the
|
||||||
|
// sum of the values of the words within the window. Then use the window that got the
|
||||||
|
// maximum sum. If there are multiple maximas, then get the last one.
|
||||||
|
// Enclose the terms in <em>.
|
||||||
|
var stemmed_searchterms = searchterms.map(function(w) {
|
||||||
|
return elasticlunr.stemmer(w.toLowerCase());
|
||||||
|
});
|
||||||
|
var searchterm_weight = 40;
|
||||||
|
var weighted = []; // contains elements of ["word", weight, index_in_document]
|
||||||
|
// split in sentences, then words
|
||||||
|
var sentences = body.toLowerCase().split('. ');
|
||||||
|
var index = 0;
|
||||||
|
var value = 0;
|
||||||
|
var searchterm_found = false;
|
||||||
|
for (var sentenceindex in sentences) {
|
||||||
|
var words = sentences[sentenceindex].split(' ');
|
||||||
|
value = 8;
|
||||||
|
for (var wordindex in words) {
|
||||||
|
var word = words[wordindex];
|
||||||
|
if (word.length > 0) {
|
||||||
|
for (var searchtermindex in stemmed_searchterms) {
|
||||||
|
if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) {
|
||||||
|
value = searchterm_weight;
|
||||||
|
searchterm_found = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
weighted.push([word, value, index]);
|
||||||
|
value = 2;
|
||||||
|
}
|
||||||
|
index += word.length;
|
||||||
|
index += 1; // ' ' or '.' if last word in sentence
|
||||||
|
};
|
||||||
|
index += 1; // because we split at a two-char boundary '. '
|
||||||
|
};
|
||||||
|
|
||||||
|
if (weighted.length == 0) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
var window_weight = [];
|
||||||
|
var window_size = Math.min(weighted.length, searchoptions.teaser_word_count);
|
||||||
|
|
||||||
|
var cur_sum = 0;
|
||||||
|
for (var wordindex = 0; wordindex < window_size; wordindex++) {
|
||||||
|
cur_sum += weighted[wordindex][1];
|
||||||
|
};
|
||||||
|
window_weight.push(cur_sum);
|
||||||
|
for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
|
||||||
|
cur_sum -= weighted[wordindex][1];
|
||||||
|
cur_sum += weighted[wordindex + window_size][1];
|
||||||
|
window_weight.push(cur_sum);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (searchterm_found) {
|
||||||
|
var max_sum = 0;
|
||||||
|
var max_sum_window_index = 0;
|
||||||
|
// backwards
|
||||||
|
for (var i = window_weight.length - 1; i >= 0; i--) {
|
||||||
|
if (window_weight[i] > max_sum) {
|
||||||
|
max_sum = window_weight[i];
|
||||||
|
max_sum_window_index = i;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
max_sum_window_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add <em/> around searchterms
|
||||||
|
var teaser_split = [];
|
||||||
|
var index = weighted[max_sum_window_index][2];
|
||||||
|
for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) {
|
||||||
|
var word = weighted[i];
|
||||||
|
if (index < word[2]) {
|
||||||
|
// missing text from index to start of `word`
|
||||||
|
teaser_split.push(body.substring(index, word[2]));
|
||||||
|
index = word[2];
|
||||||
|
}
|
||||||
|
if (word[1] == searchterm_weight) {
|
||||||
|
teaser_split.push("<em>")
|
||||||
|
}
|
||||||
|
index = word[2] + word[0].length;
|
||||||
|
teaser_split.push(body.substring(word[2], index));
|
||||||
|
if (word[1] == searchterm_weight) {
|
||||||
|
teaser_split.push("</em>")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return teaser_split.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
searchoptions = window.search.searchoptions;
|
||||||
|
searchindex = elasticlunr.Index.load(window.search.index);
|
||||||
|
|
||||||
|
// Set up events
|
||||||
|
searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false);
|
||||||
|
searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false);
|
||||||
|
document.addEventListener('keydown', function (e) { globalKeyHandler(e); }, false);
|
||||||
|
// If the user uses the browser buttons, do the same as if a reload happened
|
||||||
|
window.onpopstate = function(e) { doSearchOrMarkFromUrl(); };
|
||||||
|
|
||||||
|
// If reloaded, do the search or mark again, depending on the current url parameters
|
||||||
|
doSearchOrMarkFromUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unfocusSearchbar() {
|
||||||
|
// hacky, but just focusing a div only works once
|
||||||
|
var tmp = document.createElement('input');
|
||||||
|
tmp.setAttribute('style', 'position: absolute; opacity: 0;');
|
||||||
|
searchicon.appendChild(tmp);
|
||||||
|
tmp.focus();
|
||||||
|
tmp.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// On reload or browser history backwards/forwards events, parse the url and do search or mark
|
||||||
|
function doSearchOrMarkFromUrl() {
|
||||||
|
// Check current URL for search request
|
||||||
|
var url = parseURL(window.location.href);
|
||||||
|
if (url.params.hasOwnProperty(URL_SEARCH_PARAM)
|
||||||
|
&& url.params[URL_SEARCH_PARAM] != "") {
|
||||||
|
showSearch(true);
|
||||||
|
searchbar.value = decodeURIComponent(
|
||||||
|
(url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20'));
|
||||||
|
searchbarKeyUpHandler(); // -> doSearch()
|
||||||
|
} else {
|
||||||
|
showSearch(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.params.hasOwnProperty(URL_MARK_PARAM)) {
|
||||||
|
var words = url.params[URL_MARK_PARAM].split(' ');
|
||||||
|
marker.mark(words, {
|
||||||
|
exclude: mark_exclude
|
||||||
|
});
|
||||||
|
|
||||||
|
var markers = document.querySelectorAll("mark");
|
||||||
|
function hide() {
|
||||||
|
for (var i = 0; i < markers.length; i++) {
|
||||||
|
markers[i].classList.add("fade-out");
|
||||||
|
window.setTimeout(function(e) { marker.unmark(); }, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var i = 0; i < markers.length; i++) {
|
||||||
|
markers[i].addEventListener('click', hide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eventhandler for keyevents on `document`
|
||||||
|
function globalKeyHandler(e) {
|
||||||
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||||
|
|
||||||
|
if (e.keyCode == ESCAPE_KEYCODE) {
|
||||||
|
e.preventDefault();
|
||||||
|
searchbar.classList.remove("active");
|
||||||
|
setSearchUrlParameters("",
|
||||||
|
(searchbar.value.trim() != "") ? "push" : "replace");
|
||||||
|
if (hasFocus()) {
|
||||||
|
unfocusSearchbar();
|
||||||
|
}
|
||||||
|
showSearch(false);
|
||||||
|
marker.unmark();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasFocus() && e.keyCode == SEARCH_HOTKEY_KEYCODE) {
|
||||||
|
e.preventDefault();
|
||||||
|
showSearch(true);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
searchbar.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasFocus() && e.keyCode == DOWN_KEYCODE) {
|
||||||
|
e.preventDefault();
|
||||||
|
unfocusSearchbar();
|
||||||
|
searchresults.children('li').first().classList.add("focus");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasFocus() && (e.keyCode == DOWN_KEYCODE
|
||||||
|
|| e.keyCode == UP_KEYCODE
|
||||||
|
|| e.keyCode == SELECT_KEYCODE)) {
|
||||||
|
// not `:focus` because browser does annoying scrolling
|
||||||
|
var current_focus = search.searchresults.find("li.focus");
|
||||||
|
if (current_focus.length == 0) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.keyCode == DOWN_KEYCODE) {
|
||||||
|
var next = current_focus.next()
|
||||||
|
if (next.length > 0) {
|
||||||
|
current_focus.classList.remove("focus");
|
||||||
|
next.classList.add("focus");
|
||||||
|
}
|
||||||
|
} else if (e.keyCode == UP_KEYCODE) {
|
||||||
|
current_focus.classList.remove("focus");
|
||||||
|
var prev = current_focus.prev();
|
||||||
|
if (prev.length == 0) {
|
||||||
|
searchbar.focus();
|
||||||
|
} else {
|
||||||
|
prev.classList.add("focus");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.location = current_focus.children('a').attr('href');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSearch(yes) {
|
||||||
|
if (yes) {
|
||||||
|
searchbar_outer.style.display = 'block';
|
||||||
|
searchicon.setAttribute('aria-expanded', 'true');
|
||||||
|
} else {
|
||||||
|
searchbar_outer.style.display = 'none';
|
||||||
|
searchresults_outer.style.display = 'none';
|
||||||
|
searchbar.value = '';
|
||||||
|
removeChildren(searchresults);
|
||||||
|
searchicon.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResults(yes) {
|
||||||
|
if (yes) {
|
||||||
|
searchbar_outer.style.display = 'block';
|
||||||
|
searchresults_outer.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
searchresults_outer.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eventhandler for search icon
|
||||||
|
function searchIconClickHandler() {
|
||||||
|
if (searchbar_outer.style.display === 'block') {
|
||||||
|
showSearch(false);
|
||||||
|
} else {
|
||||||
|
showSearch(true);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
searchbar.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eventhandler for keyevents while the searchbar is focused
|
||||||
|
function searchbarKeyUpHandler() {
|
||||||
|
var searchterm = searchbar.value.trim();
|
||||||
|
if (searchterm != "") {
|
||||||
|
searchbar.classList.add("active");
|
||||||
|
doSearch(searchterm);
|
||||||
|
} else {
|
||||||
|
searchbar.classList.remove("active");
|
||||||
|
showResults(false);
|
||||||
|
removeChildren(searchresults);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchUrlParameters(searchterm, "push_if_new_search_else_replace");
|
||||||
|
|
||||||
|
// Remove marks
|
||||||
|
marker.unmark();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor .
|
||||||
|
// `action` can be one of "push", "replace", "push_if_new_search_else_replace"
|
||||||
|
// and replaces or pushes a new browser history item.
|
||||||
|
// "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet.
|
||||||
|
function setSearchUrlParameters(searchterm, action) {
|
||||||
|
var url = parseURL(window.location.href);
|
||||||
|
var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM);
|
||||||
|
if (searchterm != "" || action == "push_if_new_search_else_replace") {
|
||||||
|
url.params[URL_SEARCH_PARAM] = searchterm;
|
||||||
|
delete url.params[URL_MARK_PARAM];
|
||||||
|
url.hash = "";
|
||||||
|
} else {
|
||||||
|
delete url.params[URL_SEARCH_PARAM];
|
||||||
|
}
|
||||||
|
// A new search will also add a new history item, so the user can go back
|
||||||
|
// to the page prior to searching. A updated search term will only replace
|
||||||
|
// the url.
|
||||||
|
if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) {
|
||||||
|
history.pushState({}, document.title, renderURL(url));
|
||||||
|
} else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) {
|
||||||
|
history.replaceState({}, document.title, renderURL(url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSearch(searchterm) {
|
||||||
|
|
||||||
|
// Don't search the same twice
|
||||||
|
if (current_searchterm == searchterm) { return; }
|
||||||
|
else { current_searchterm = searchterm; }
|
||||||
|
|
||||||
|
if (searchindex == null) { return; }
|
||||||
|
|
||||||
|
// Do the actual search
|
||||||
|
var results = searchindex.search(searchterm, searchoptions);
|
||||||
|
var resultcount = Math.min(results.length, searchoptions.limit_results);
|
||||||
|
|
||||||
|
// Display search metrics
|
||||||
|
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
|
||||||
|
|
||||||
|
// Clear and insert results
|
||||||
|
var searchterms = searchterm.split(' ');
|
||||||
|
removeChildren(searchresults);
|
||||||
|
for(var i = 0; i < resultcount ; i++){
|
||||||
|
var resultElem = document.createElement('li');
|
||||||
|
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
|
||||||
|
searchresults.appendChild(resultElem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display results
|
||||||
|
showResults(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
// Exported functions
|
||||||
|
search.hasFocus = hasFocus;
|
||||||
|
})(window.search);
|
|
@ -9,3 +9,4 @@
|
||||||
@import 'themes'
|
@import 'themes'
|
||||||
@import 'print'
|
@import 'print'
|
||||||
@import 'tooltip'
|
@import 'tooltip'
|
||||||
|
@import 'searchbar'
|
||||||
|
|
|
@ -35,6 +35,16 @@ h4, h5 { margin-top: 2em }
|
||||||
|
|
||||||
.header + .header h3, .header + .header h4, .header + .header h5 { margin-top: 1em }
|
.header + .header h3, .header + .header h4, .header + .header h5 { margin-top: 1em }
|
||||||
|
|
||||||
|
a.header:target h1:before,
|
||||||
|
a.header:target h2:before,
|
||||||
|
a.header:target h3:before,
|
||||||
|
a.header:target h4:before {
|
||||||
|
display: inline-block;
|
||||||
|
content: "»";
|
||||||
|
margin-left: -30px;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
@require 'variables'
|
||||||
|
|
||||||
|
#searchresults a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0 3px 1px 3px;
|
||||||
|
margin: 0 -3px -1px -3px;
|
||||||
|
transition: background-color 300ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-out {
|
||||||
|
background-color: rgba(0,0,0,0) !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbar-outer {
|
||||||
|
display: none;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: $content-max-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbar {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 5px auto 0px auto;
|
||||||
|
padding: 10px 16px;
|
||||||
|
transition: box-shadow 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchresults-header {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 18px 0 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchresults-outer {
|
||||||
|
display: none;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: $content-max-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul#searchresults {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 20px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 10px 0px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.teaser {
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
margin: 5px 0 0 20px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.teaser em {
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
|
||||||
$table-header-bg = lighten($bg, 20%)
|
$table-header-bg = lighten($bg, 20%)
|
||||||
$table-alternate-bg = lighten($bg, 3%)
|
$table-alternate-bg = lighten($bg, 3%)
|
||||||
|
|
||||||
|
$searchbar-border-color = #848484
|
||||||
|
$searchbar-bg = #424242
|
||||||
|
$searchbar-fg = #fff
|
||||||
|
$searchbar-shadow-color = #d4c89f
|
||||||
|
$searchresults-header-fg = #666
|
||||||
|
$searchresults-border-color = #888
|
||||||
|
$searchresults-li-bg = #252932
|
||||||
|
$search-mark-bg = #e3b171
|
||||||
|
|
||||||
@import 'base'
|
@import 'base'
|
||||||
|
|
|
@ -84,7 +84,10 @@
|
||||||
background-color: $sidebar-bg
|
background-color: $sidebar-bg
|
||||||
}
|
}
|
||||||
|
|
||||||
.content a:link, a:visited, a > .hljs {
|
#searchresults a,
|
||||||
|
.content a:link,
|
||||||
|
a:visited,
|
||||||
|
a > .hljs {
|
||||||
color: $links
|
color: $links
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,4 +191,32 @@
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: $scrollbar;
|
background: $scrollbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Search */
|
||||||
|
#searchbar {
|
||||||
|
border: 1px solid $searchbar-border-color;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: $searchbar-bg;
|
||||||
|
color: $searchbar-fg
|
||||||
|
|
||||||
|
&:focus, &.active {
|
||||||
|
box-shadow: 0 0 3px $searchbar-shadow-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchresults-header {
|
||||||
|
color: $searchresults-header-fg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchresults-outer {
|
||||||
|
border-bottom: 1px dashed $searchresults-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul#searchresults li.focus {
|
||||||
|
background-color: $searchresults-li-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
background-color: $search-mark-bg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
|
||||||
$table-header-bg = lighten($bg, 20%)
|
$table-header-bg = lighten($bg, 20%)
|
||||||
$table-alternate-bg = lighten($bg, 3%)
|
$table-alternate-bg = lighten($bg, 3%)
|
||||||
|
|
||||||
|
$searchbar-border-color = #aaa
|
||||||
|
$searchbar-bg = #b7b7b7
|
||||||
|
$searchbar-fg = #000
|
||||||
|
$searchbar-shadow-color = #aaa
|
||||||
|
$searchresults-header-fg = #666
|
||||||
|
$searchresults-border-color = #98a3ad
|
||||||
|
$searchresults-li-bg = #2b2b2f
|
||||||
|
$search-mark-bg = #355c7d
|
||||||
|
|
||||||
@import 'base'
|
@import 'base'
|
||||||
|
|
|
@ -16,7 +16,7 @@ $icons-hover = #333333
|
||||||
|
|
||||||
$links = #4183c4
|
$links = #4183c4
|
||||||
|
|
||||||
$inline-code-color = #6e6b5e;
|
$inline-code-color = #6e6b5e
|
||||||
|
|
||||||
$theme-popup-bg = #fafafa
|
$theme-popup-bg = #fafafa
|
||||||
$theme-popup-border = #cccccc
|
$theme-popup-border = #cccccc
|
||||||
|
@ -29,4 +29,13 @@ $table-border-color = darken($bg, 5%)
|
||||||
$table-header-bg = darken($bg, 20%)
|
$table-header-bg = darken($bg, 20%)
|
||||||
$table-alternate-bg = darken($bg, 3%)
|
$table-alternate-bg = darken($bg, 3%)
|
||||||
|
|
||||||
|
$searchbar-border-color = #aaa
|
||||||
|
$searchbar-bg = #fafafa
|
||||||
|
$searchbar-fg = #000
|
||||||
|
$searchbar-shadow-color = #aaa
|
||||||
|
$searchresults-header-fg = #666
|
||||||
|
$searchresults-border-color = #888
|
||||||
|
$searchresults-li-bg = #e4f2fe
|
||||||
|
$search-mark-bg = #a2cff5
|
||||||
|
|
||||||
@import 'base'
|
@import 'base'
|
||||||
|
|
|
@ -29,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
|
||||||
$table-header-bg = lighten($bg, 20%)
|
$table-header-bg = lighten($bg, 20%)
|
||||||
$table-alternate-bg = lighten($bg, 3%)
|
$table-alternate-bg = lighten($bg, 3%)
|
||||||
|
|
||||||
|
$searchbar-border-color = #aaa
|
||||||
|
$searchbar-bg = #aeaec6
|
||||||
|
$searchbar-fg = #000
|
||||||
|
$searchbar-shadow-color = #aaa
|
||||||
|
$searchresults-header-fg = #5f5f71
|
||||||
|
$searchresults-border-color = #5c5c68
|
||||||
|
$searchresults-li-bg = #242430
|
||||||
|
$search-mark-bg = #a2cff5
|
||||||
|
|
||||||
@import 'base'
|
@import 'base'
|
||||||
|
|
|
@ -29,4 +29,13 @@ $table-border-color = darken($bg, 5%)
|
||||||
$table-header-bg = #b3a497
|
$table-header-bg = #b3a497
|
||||||
$table-alternate-bg = darken($bg, 3%)
|
$table-alternate-bg = darken($bg, 3%)
|
||||||
|
|
||||||
|
$searchbar-border-color = #aaa
|
||||||
|
$searchbar-bg = #fafafa
|
||||||
|
$searchbar-fg = #000
|
||||||
|
$searchbar-shadow-color = #aaa
|
||||||
|
$searchresults-header-fg = #666
|
||||||
|
$searchresults-border-color = #888
|
||||||
|
$searchresults-li-bg = #dec2a2
|
||||||
|
$search-mark-bg = #e69f67
|
||||||
|
|
||||||
@import 'base'
|
@import 'base'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::path::{Component, Path, PathBuf};
|
|
||||||
use errors::*;
|
use errors::*;
|
||||||
use std::io::Read;
|
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
/// Takes a path to a file and try to read the file into a String
|
/// Takes a path to a file and try to read the file into a String
|
||||||
pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||||
|
@ -16,6 +16,27 @@ pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Naively replaces any path seperator with a forward-slash '/'
|
||||||
|
pub fn normalize_path(path: &str) -> String {
|
||||||
|
use std::path::is_separator;
|
||||||
|
path.chars()
|
||||||
|
.map(|ch| if is_separator(ch) { '/' } else { ch })
|
||||||
|
.collect::<String>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the given data to a file, creating it first if necessary
|
||||||
|
pub fn write_file<P: AsRef<Path>>(
|
||||||
|
build_dir: &Path,
|
||||||
|
filename: P,
|
||||||
|
content: &[u8],
|
||||||
|
) -> Result<()> {
|
||||||
|
let path = build_dir.join(filename);
|
||||||
|
|
||||||
|
create_file(&path)?
|
||||||
|
.write_all(content)
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
/// Takes a path and returns a path containing just enough `../` to point to
|
/// Takes a path and returns a path containing just enough `../` to point to
|
||||||
/// the root of the given path.
|
/// the root of the given path.
|
||||||
///
|
///
|
||||||
|
@ -38,7 +59,6 @@ pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||||
/// it doesn't return the correct path.
|
/// it doesn't return the correct path.
|
||||||
/// Consider [submitting a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
|
/// Consider [submitting a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
|
||||||
/// or a [pull-request](https://github.com/rust-lang-nursery/mdBook/pulls) to improve it.
|
/// or a [pull-request](https://github.com/rust-lang-nursery/mdBook/pulls) to improve it.
|
||||||
|
|
||||||
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||||
debug!("path_to_root");
|
debug!("path_to_root");
|
||||||
// Remove filename and add "../" for every directory
|
// Remove filename and add "../" for every directory
|
||||||
|
@ -61,7 +81,6 @@ pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||||
/// This function creates a file and returns it. But before creating the file
|
/// This function creates a file and returns it. But before creating the file
|
||||||
/// it checks every directory in the path to see if it exists,
|
/// it checks every directory in the path to see if it exists,
|
||||||
/// and if it does not it will be created.
|
/// and if it does not it will be created.
|
||||||
|
|
||||||
pub fn create_file(path: &Path) -> Result<File> {
|
pub fn create_file(path: &Path) -> Result<File> {
|
||||||
debug!("Creating {}", path.display());
|
debug!("Creating {}", path.display());
|
||||||
|
|
||||||
|
@ -76,7 +95,6 @@ pub fn create_file(path: &Path) -> Result<File> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes all the content of a directory but not the directory itself
|
/// Removes all the content of a directory but not the directory itself
|
||||||
|
|
||||||
pub fn remove_dir_content(dir: &Path) -> Result<()> {
|
pub fn remove_dir_content(dir: &Path) -> Result<()> {
|
||||||
for item in fs::read_dir(dir)? {
|
for item in fs::read_dir(dir)? {
|
||||||
if let Ok(item) = item {
|
if let Ok(item) = item {
|
||||||
|
@ -93,7 +111,6 @@ pub fn remove_dir_content(dir: &Path) -> Result<()> {
|
||||||
|
|
||||||
/// Copies all files of a directory to another one except the files
|
/// Copies all files of a directory to another one except the files
|
||||||
/// with the extensions given in the `ext_blacklist` array
|
/// with the extensions given in the `ext_blacklist` array
|
||||||
|
|
||||||
pub fn copy_files_except_ext(
|
pub fn copy_files_except_ext(
|
||||||
from: &Path,
|
from: &Path,
|
||||||
to: &Path,
|
to: &Path,
|
||||||
|
|
|
@ -3,13 +3,71 @@
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
mod string;
|
mod string;
|
||||||
use errors::Error;
|
use errors::Error;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
|
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
|
||||||
OPTION_ENABLE_TABLES};
|
OPTION_ENABLE_TABLES};
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
pub use self::string::{RangeArgument, take_lines};
|
pub use self::string::{RangeArgument, take_lines};
|
||||||
|
|
||||||
|
/// Replaces multiple consecutive whitespace characters with a single space character.
|
||||||
|
pub fn collapse_whitespace<'a>(text: &'a str) -> Cow<'a, str> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref RE: Regex = Regex::new(r"\s\s+").unwrap();
|
||||||
|
}
|
||||||
|
RE.replace_all(text, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the given string to a valid HTML element ID
|
||||||
|
pub fn normalize_id(content: &str) -> String {
|
||||||
|
let mut ret = content
|
||||||
|
.chars()
|
||||||
|
.filter_map(|ch| {
|
||||||
|
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
|
||||||
|
Some(ch.to_ascii_lowercase())
|
||||||
|
} else if ch.is_whitespace() {
|
||||||
|
Some('-')
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<String>();
|
||||||
|
// Ensure that the first character is [A-Za-z]
|
||||||
|
if ret.chars().next().map_or(false, |c| !c.is_ascii_alphabetic()) {
|
||||||
|
ret.insert(0, 'a');
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate an ID for use with anchors which is derived from a "normalised"
|
||||||
|
/// string.
|
||||||
|
pub fn id_from_content(content: &str) -> String {
|
||||||
|
let mut content = content.to_string();
|
||||||
|
|
||||||
|
// Skip any tags or html-encoded stuff
|
||||||
|
const REPL_SUB: &[&str] = &["<em>",
|
||||||
|
"</em>",
|
||||||
|
"<code>",
|
||||||
|
"</code>",
|
||||||
|
"<strong>",
|
||||||
|
"</strong>",
|
||||||
|
"<",
|
||||||
|
">",
|
||||||
|
"&",
|
||||||
|
"'",
|
||||||
|
"""];
|
||||||
|
for sub in REPL_SUB {
|
||||||
|
content = content.replace(sub, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove spaces and hashes indicating a header
|
||||||
|
let trimmed = content.trim().trim_left_matches('#').trim();
|
||||||
|
|
||||||
|
normalize_id(trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
|
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
|
||||||
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
||||||
let mut s = String::with_capacity(text.len() * 3 / 2);
|
let mut s = String::with_capacity(text.len() * 3 / 2);
|
||||||
|
@ -212,6 +270,29 @@ more text with spaces
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod html_munging {
|
||||||
|
use super::super::{id_from_content, normalize_id};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_generates_anchors() {
|
||||||
|
assert_eq!(id_from_content("## `--passes`: add more rustdoc passes"),
|
||||||
|
"a--passes-add-more-rustdoc-passes");
|
||||||
|
assert_eq!(id_from_content("## Method-call expressions"),
|
||||||
|
"method-call-expressions");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_normalizes_ids() {
|
||||||
|
assert_eq!(normalize_id("`--passes`: add more rustdoc passes"),
|
||||||
|
"a--passes-add-more-rustdoc-passes");
|
||||||
|
assert_eq!(normalize_id("Method-call 🐙 expressions \u{1f47c}"),
|
||||||
|
"method-call--expressions-");
|
||||||
|
assert_eq!(normalize_id("_-_12345"), "a_-_12345");
|
||||||
|
assert_eq!(normalize_id("12345"), "a12345");
|
||||||
|
assert_eq!(normalize_id(""), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod convert_quotes_to_curly {
|
mod convert_quotes_to_curly {
|
||||||
use super::super::convert_quotes_to_curly;
|
use super::super::convert_quotes_to_curly;
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,11 @@
|
||||||
extern crate mdbook;
|
extern crate mdbook;
|
||||||
extern crate tempdir;
|
extern crate tempdir;
|
||||||
|
|
||||||
use std::fs::File;
|
#[cfg(not(windows))]
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
use mdbook::config::Config;
|
use mdbook::config::Config;
|
||||||
use mdbook::MDBook;
|
use mdbook::MDBook;
|
||||||
use mdbook::renderer::RenderContext;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn passing_alternate_backend() {
|
fn passing_alternate_backend() {
|
||||||
|
@ -39,6 +38,7 @@ fn alternate_backend_with_arguments() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a command which will pipe `stdin` to the provided file.
|
/// Get a command which will pipe `stdin` to the provided file.
|
||||||
|
#[cfg(not(windows))]
|
||||||
fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
|
fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
|
||||||
let out_file = out_file.as_ref();
|
let out_file = out_file.as_ref();
|
||||||
|
|
||||||
|
@ -52,6 +52,9 @@ fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn backends_receive_render_context_via_stdin() {
|
fn backends_receive_render_context_via_stdin() {
|
||||||
|
use std::fs::File;
|
||||||
|
use mdbook::renderer::RenderContext;
|
||||||
|
|
||||||
let temp = TempDir::new("output").unwrap();
|
let temp = TempDir::new("output").unwrap();
|
||||||
let out_file = temp.path().join("out.txt");
|
let out_file = temp.path().join("out.txt");
|
||||||
let cmd = tee_command(&out_file);
|
let cmd = tee_command(&out_file);
|
||||||
|
|
|
@ -1 +1,20 @@
|
||||||
# Conclusion
|
# Conclusion
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<!--secret secret-->
|
||||||
|
I put <HTML> in here!<br/>
|
||||||
|
</p>
|
||||||
|
<script type="text/javascript" >
|
||||||
|
// I probably shouldn't do this
|
||||||
|
if (3 < 5 > 10)
|
||||||
|
{
|
||||||
|
alert("The sky is falling!");
|
||||||
|
}
|
||||||
|
</script >
|
||||||
|
<style >
|
||||||
|
/*
|
||||||
|
css looks, like this {
|
||||||
|
foo: < 3 <bar >
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
</style>
|
||||||
|
|
|
@ -339,3 +339,103 @@ fn book_with_a_reserved_filename_does_not_build() {
|
||||||
let got = md.build();
|
let got = md.build();
|
||||||
assert!(got.is_err());
|
assert!(got.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
mod search {
|
||||||
|
extern crate serde_json;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::path::Path;
|
||||||
|
use mdbook::utils::fs::file_to_string;
|
||||||
|
use mdbook::MDBook;
|
||||||
|
use dummy_book::DummyBook;
|
||||||
|
|
||||||
|
fn read_book_index(root: &Path) -> serde_json::Value {
|
||||||
|
let index = root.join("book/searchindex.js");
|
||||||
|
let index = file_to_string(index).unwrap();
|
||||||
|
let index = index.trim_left_matches("window.search = ");
|
||||||
|
let index = index.trim_right_matches(";");
|
||||||
|
serde_json::from_str(&index).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn book_creates_reasonable_search_index() {
|
||||||
|
let temp = DummyBook::new().build().unwrap();
|
||||||
|
let md = MDBook::load(temp.path()).unwrap();
|
||||||
|
md.build().unwrap();
|
||||||
|
|
||||||
|
let index = read_book_index(temp.path());
|
||||||
|
|
||||||
|
let bodyidx = &index["index"]["index"]["body"]["root"];
|
||||||
|
let textidx = &bodyidx["t"]["e"]["x"]["t"];
|
||||||
|
assert_eq!(textidx["df"], 2);
|
||||||
|
assert_eq!(textidx["docs"]["first/index.html#first-chapter"]["tf"], 1.0);
|
||||||
|
assert_eq!(textidx["docs"]["intro.html#introduction"]["tf"], 1.0);
|
||||||
|
|
||||||
|
let docs = &index["index"]["documentStore"]["docs"];
|
||||||
|
assert_eq!(docs["first/index.html#first-chapter"]["body"], "more text.");
|
||||||
|
assert_eq!(docs["first/index.html#some-section"]["body"], "");
|
||||||
|
assert_eq!(
|
||||||
|
docs["first/includes.html#summary"]["body"],
|
||||||
|
"Introduction First Chapter Nested Chapter Includes Second Chapter Conclusion"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
docs["first/includes.html#summary"]["breadcrumbs"],
|
||||||
|
"First Chapter » Summary"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
docs["conclusion.html#conclusion"]["body"],
|
||||||
|
"I put <HTML> in here!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting this to `true` may cause issues with `cargo watch`,
|
||||||
|
// since it may not finish writing the fixture before the tests
|
||||||
|
// are run again.
|
||||||
|
const GENERATE_FIXTURE: bool = false;
|
||||||
|
|
||||||
|
fn get_fixture() -> serde_json::Value {
|
||||||
|
if GENERATE_FIXTURE {
|
||||||
|
let temp = DummyBook::new().build().unwrap();
|
||||||
|
let md = MDBook::load(temp.path()).unwrap();
|
||||||
|
md.build().unwrap();
|
||||||
|
|
||||||
|
let src = read_book_index(temp.path());
|
||||||
|
|
||||||
|
let dest = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/searchindex_fixture.json");
|
||||||
|
let dest = File::create(&dest).unwrap();
|
||||||
|
serde_json::to_writer_pretty(dest, &src).unwrap();
|
||||||
|
|
||||||
|
src
|
||||||
|
} else {
|
||||||
|
let json = include_str!("searchindex_fixture.json");
|
||||||
|
serde_json::from_str(json).expect("Unable to deserialize the fixture")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// So you've broken the test. If you changed dummy_book, it's probably
|
||||||
|
// safe to regenerate the fixture. If you haven't then make sure that the
|
||||||
|
// search index still works. Run `cargo run -- serve tests/dummy_book`
|
||||||
|
// and try some searches. Are you getting results? Do the teasers look OK?
|
||||||
|
// Are there new errors in the JS console?
|
||||||
|
//
|
||||||
|
// If you're pretty sure you haven't broken anything, change `GENERATE_FIXTURE`
|
||||||
|
// above to `true`, and run `cargo test` to generate a new fixture. Then
|
||||||
|
// change it back to `false`. Include the changed `searchindex_fixture.json` in your commit.
|
||||||
|
#[test]
|
||||||
|
fn search_index_hasnt_changed_accidentally() {
|
||||||
|
let temp = DummyBook::new().build().unwrap();
|
||||||
|
let md = MDBook::load(temp.path()).unwrap();
|
||||||
|
md.build().unwrap();
|
||||||
|
|
||||||
|
let book_index = read_book_index(temp.path());
|
||||||
|
|
||||||
|
let fixture_index = get_fixture();
|
||||||
|
|
||||||
|
// Uncomment this if you're okay with pretty-printing 32KB of JSON
|
||||||
|
//assert_eq!(fixture_index, book_index);
|
||||||
|
|
||||||
|
if book_index != fixture_index {
|
||||||
|
panic!("The search index has changed from the fixture");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue