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
|
||||
*.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)",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "ansi_term"
|
||||
version = "0.9.0"
|
||||
|
@ -177,6 +190,20 @@ name = "either"
|
|||
version = "1.4.0"
|
||||
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]]
|
||||
name = "env_logger"
|
||||
version = "0.5.3"
|
||||
|
@ -288,6 +315,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "httparse"
|
||||
version = "1.2.4"
|
||||
|
@ -428,6 +467,11 @@ name = "mac"
|
|||
version = "0.1.1"
|
||||
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]]
|
||||
name = "markup5ever"
|
||||
version = "0.3.2"
|
||||
|
@ -441,6 +485,21 @@ dependencies = [
|
|||
"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]]
|
||||
name = "matches"
|
||||
version = "0.1.6"
|
||||
|
@ -450,9 +509,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
name = "mdbook"
|
||||
version = "0.1.4-alpha.0"
|
||||
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)",
|
||||
"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)",
|
||||
"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)",
|
||||
"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)",
|
||||
|
@ -932,6 +993,20 @@ dependencies = [
|
|||
"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]]
|
||||
name = "string_cache_codegen"
|
||||
version = "0.4.0"
|
||||
|
@ -990,6 +1065,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "termcolor"
|
||||
version = "0.3.4"
|
||||
|
@ -1236,6 +1321,7 @@ dependencies = [
|
|||
|
||||
[metadata]
|
||||
"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.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"
|
||||
|
@ -1261,6 +1347,7 @@ dependencies = [
|
|||
"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 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 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"
|
||||
|
@ -1273,6 +1360,7 @@ dependencies = [
|
|||
"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 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 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"
|
||||
|
@ -1291,7 +1379,9 @@ dependencies = [
|
|||
"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 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.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 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"
|
||||
|
@ -1348,6 +1438,7 @@ dependencies = [
|
|||
"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 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_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"
|
||||
|
@ -1355,6 +1446,7 @@ dependencies = [
|
|||
"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 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 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"
|
||||
|
|
|
@ -50,6 +50,10 @@ iron = { version = "0.5", optional = true }
|
|||
staticfile = { version = "0.4", 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]
|
||||
error-chain = "0.11"
|
||||
|
||||
|
@ -60,12 +64,13 @@ walkdir = "2.0"
|
|||
pulldown-cmark-to-cmark = "1.1.0"
|
||||
|
||||
[features]
|
||||
default = ["output", "watch", "serve"]
|
||||
default = ["output", "watch", "serve", "search"]
|
||||
debug = []
|
||||
output = []
|
||||
regenerate-css = []
|
||||
watch = ["notify", "time", "crossbeam"]
|
||||
serve = ["iron", "staticfile", "ws"]
|
||||
search = ["elasticlunr-rs", "ammonia"]
|
||||
|
||||
[[bin]]
|
||||
doc = false
|
||||
|
|
|
@ -8,3 +8,12 @@ mathjax-support = true
|
|||
|
||||
[output.html.playpen]
|
||||
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]
|
||||
additional-css = ["custom.css"]
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 15
|
||||
```
|
||||
|
||||
## 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
|
||||
surgically change the style.
|
||||
- **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.
|
||||
- **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
|
||||
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
|
||||
[book]
|
||||
title = "Example book"
|
||||
|
@ -105,6 +142,18 @@ additional-js = ["custom.js"]
|
|||
[output.html.playpen]
|
||||
editor = "./path/to/editor"
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
- [Chris Spiegel](https://github.com/cspiegel)
|
||||
- [projektir](https://github.com/projektir)
|
||||
- [Phaiax](https://github.com/Phaiax)
|
||||
- [Matt Ickstadt](https://github.com/mattico)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
set -ex
|
||||
|
||||
main() {
|
||||
cross build --target $TARGET --all --no-default-features
|
||||
cross build --target $TARGET --all
|
||||
cross build --target $TARGET --all --release
|
||||
|
||||
|
@ -10,6 +11,7 @@ main() {
|
|||
return
|
||||
fi
|
||||
|
||||
cross test --target $TARGET --no-default-features
|
||||
cross test --target $TARGET
|
||||
cross test --target $TARGET --release
|
||||
}
|
||||
|
|
|
@ -92,6 +92,8 @@ fn init_logger() {
|
|||
} else {
|
||||
// if no RUST_LOG provided, default to logging at the Info level
|
||||
builder.filter(None, LevelFilter::Info);
|
||||
// Filter extraneous html5ever not-implemented messages
|
||||
builder.filter(Some("html5ever"), LevelFilter::Error);
|
||||
}
|
||||
|
||||
builder.init();
|
||||
|
|
|
@ -152,15 +152,20 @@ pub struct Chapter {
|
|||
pub sub_items: Vec<BookItem>,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
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 {
|
||||
/// 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 {
|
||||
name: name.to_string(),
|
||||
content: content,
|
||||
path: path.into(),
|
||||
parent_names: parent_names,
|
||||
..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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
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());
|
||||
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)
|
||||
.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();
|
||||
|
||||
sub_item_parents.push(link.name.clone());
|
||||
let sub_items = link.nested_items
|
||||
.iter()
|
||||
.map(|i| load_summary_item(i, src_dir))
|
||||
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
ch.sub_items = sub_items;
|
||||
|
@ -324,9 +337,9 @@ And here is some \
|
|||
#[test]
|
||||
fn load_a_single_chapter_from_disk() {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -334,7 +347,7 @@ And here is some \
|
|||
fn cant_load_a_nonexistent_chapter() {
|
||||
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());
|
||||
}
|
||||
|
||||
|
@ -347,6 +360,7 @@ And here is some \
|
|||
content: String::from("Hello World!"),
|
||||
number: Some(SectionNumber(vec![1, 2])),
|
||||
path: PathBuf::from("second.md"),
|
||||
parent_names: vec![String::from("Chapter 1")],
|
||||
sub_items: Vec::new(),
|
||||
};
|
||||
let should_be = BookItem::Chapter(Chapter {
|
||||
|
@ -354,6 +368,7 @@ And here is some \
|
|||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: PathBuf::from("chapter_1.md"),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(nested.clone()),
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -417,17 +432,20 @@ And here is some \
|
|||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: PathBuf::from("Chapter_1/index.md"),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Hello World",
|
||||
String::new(),
|
||||
"Chapter_1/hello.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Goodbye World",
|
||||
String::new(),
|
||||
"Chapter_1/goodbye.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
],
|
||||
}),
|
||||
|
@ -464,17 +482,20 @@ And here is some \
|
|||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: PathBuf::from("Chapter_1/index.md"),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Hello World",
|
||||
String::new(),
|
||||
"Chapter_1/hello.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Goodbye World",
|
||||
String::new(),
|
||||
"Chapter_1/goodbye.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
],
|
||||
}),
|
||||
|
|
|
@ -425,29 +425,79 @@ pub struct HtmlConfig {
|
|||
pub livereload_url: Option<String>,
|
||||
/// Should section labels be rendered?
|
||||
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.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Playpen {
|
||||
/// The path to the editor to use. Defaults to the [Ace Editor].
|
||||
///
|
||||
/// [Ace Editor]: https://ace.c9.io/
|
||||
pub editor: PathBuf,
|
||||
/// Should playpen snippets be editable? Defaults to `false`.
|
||||
/// Should playpen snippets be editable? Default: `false`.
|
||||
pub editable: bool,
|
||||
/// Copy JavaScript files for the editor to the output directory?
|
||||
/// Default: `true`.
|
||||
pub copy_js: bool,
|
||||
}
|
||||
|
||||
impl Default for Playpen {
|
||||
fn default() -> Playpen {
|
||||
Playpen {
|
||||
editor: PathBuf::from("ace"),
|
||||
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
|
||||
/// a `toml::Value`.
|
||||
///
|
||||
|
@ -525,7 +575,7 @@ mod tests {
|
|||
};
|
||||
let playpen_should_be = Playpen {
|
||||
editable: true,
|
||||
editor: PathBuf::from("ace"),
|
||||
copy_js: true,
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
|
|
|
@ -122,6 +122,7 @@ pub mod errors {
|
|||
HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"];
|
||||
HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"];
|
||||
Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
|
||||
SerdeJson(::serde_json::Error) #[doc = "JSON conversion failed"];
|
||||
}
|
||||
|
||||
links {
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
use renderer::html_handlebars::helpers;
|
||||
use renderer::{RenderContext, Renderer};
|
||||
use book::{Book, BookItem, Chapter};
|
||||
use config::{Config, HtmlConfig, Playpen};
|
||||
use {theme, utils};
|
||||
use theme::{playpen_editor, Theme};
|
||||
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;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use handlebars::Handlebars;
|
||||
|
||||
use regex::{Captures, Regex};
|
||||
use serde_json;
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -26,23 +25,10 @@ impl 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(
|
||||
&self,
|
||||
item: &BookItem,
|
||||
mut ctx: RenderItemContext,
|
||||
item: &BookItem,
|
||||
mut ctx: RenderItemContext,
|
||||
print_content: &mut String,
|
||||
) -> Result<()> {
|
||||
// FIXME: This should be made DRY-er and rely less on mutable state
|
||||
|
@ -56,6 +42,11 @@ impl HtmlHandlebars {
|
|||
let path = ch.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.
|
||||
if ch.path == Path::new("print.md") {
|
||||
|
@ -83,18 +74,15 @@ impl HtmlHandlebars {
|
|||
debug!("Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
|
||||
let filepath = Path::new(&ch.path).with_extension("html");
|
||||
let rendered = self.post_process(
|
||||
rendered,
|
||||
&normalize_path(filepath.to_str().ok_or_else(|| {
|
||||
Error::from(format!("Bad file name: {}", filepath.display()))
|
||||
})?),
|
||||
&filepathstr,
|
||||
&ctx.html_config.playpen,
|
||||
);
|
||||
|
||||
// Write to file
|
||||
debug!("Creating {} ✓", filepath.display());
|
||||
self.write_file(&ctx.destination, filepath, &rendered.into_bytes())?;
|
||||
debug!("Creating {} ✓", filepathstr);
|
||||
utils::fs::write_file(&ctx.destination, &filepath, &rendered.into_bytes())?;
|
||||
|
||||
if ctx.is_index {
|
||||
self.render_index(ch, &ctx.destination)?;
|
||||
|
@ -123,7 +111,7 @@ impl HtmlHandlebars {
|
|||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
self.write_file(destination, "index.html", content.as_bytes())?;
|
||||
utils::fs::write_file(destination, "index.html", content.as_bytes())?;
|
||||
|
||||
debug!(
|
||||
"Creating index.html from {} ✓",
|
||||
|
@ -153,45 +141,47 @@ impl HtmlHandlebars {
|
|||
theme: &Theme,
|
||||
html_config: &HtmlConfig,
|
||||
) -> Result<()> {
|
||||
self.write_file(destination, "book.js", &theme.js)?;
|
||||
self.write_file(destination, "book.css", &theme.css)?;
|
||||
self.write_file(destination, "favicon.png", &theme.favicon)?;
|
||||
self.write_file(destination, "highlight.css", &theme.highlight_css)?;
|
||||
self.write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||
self.write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||
self.write_file(destination, "highlight.js", &theme.highlight_js)?;
|
||||
self.write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
|
||||
self.write_file(
|
||||
use utils::fs::write_file;
|
||||
|
||||
write_file(destination, "book.js", &theme.js)?;
|
||||
write_file(destination, "book.css", &theme.css)?;
|
||||
write_file(destination, "favicon.png", &theme.favicon)?;
|
||||
write_file(destination, "highlight.css", &theme.highlight_css)?;
|
||||
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||
write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||
write_file(destination, "highlight.js", &theme.highlight_js)?;
|
||||
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/css/font-awesome.css",
|
||||
theme::FONT_AWESOME,
|
||||
)?;
|
||||
self.write_file(
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT,
|
||||
)?;
|
||||
self.write_file(
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG,
|
||||
)?;
|
||||
self.write_file(
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
self.write_file(
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF,
|
||||
)?;
|
||||
self.write_file(
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2,
|
||||
)?;
|
||||
self.write_file(
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
|
@ -200,16 +190,15 @@ impl HtmlHandlebars {
|
|||
let playpen_config = &html_config.playpen;
|
||||
|
||||
// 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
|
||||
let editor = playpen_editor::PlaypenEditor::new(&playpen_config.editor);
|
||||
self.write_file(destination, "editor.js", &editor.js)?;
|
||||
self.write_file(destination, "ace.js", &editor.ace_js)?;
|
||||
self.write_file(destination, "mode-rust.js", &editor.mode_rust_js)?;
|
||||
self.write_file(destination, "theme-dawn.js", &editor.theme_dawn_js)?;
|
||||
self.write_file(destination,
|
||||
write_file(destination, "editor.js", playpen_editor::JS)?;
|
||||
write_file(destination, "ace.js", playpen_editor::ACE_JS)?;
|
||||
write_file(destination, "mode-rust.js", playpen_editor::MODE_RUST_JS)?;
|
||||
write_file(destination, "theme-dawn.js", playpen_editor::THEME_DAWN_JS)?;
|
||||
write_file(destination,
|
||||
"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)
|
||||
.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 {
|
||||
handlebars: &handlebars,
|
||||
destination: destination.to_path_buf(),
|
||||
data: data.clone(),
|
||||
is_index: i == 0,
|
||||
is_index: is_index,
|
||||
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
|
||||
|
@ -326,14 +319,13 @@ impl Renderer for HtmlHandlebars {
|
|||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
|
||||
let rendered = self.post_process(rendered,
|
||||
"print.html",
|
||||
&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!("Copy static files");
|
||||
|
@ -342,6 +334,10 @@ impl Renderer for HtmlHandlebars {
|
|||
self.copy_additional_css_and_js(&html_config, &ctx.root, &destination)
|
||||
.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
|
||||
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
|
||||
|
||||
|
@ -405,14 +401,20 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
|
|||
data.insert("additional_js".to_owned(), json!(js));
|
||||
}
|
||||
|
||||
if html.playpen.editable {
|
||||
data.insert("playpens_editable".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"));
|
||||
if html.playpen.editable && html.playpen.copy_js {
|
||||
data.insert("playpen_js".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
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![];
|
||||
|
@ -469,7 +471,7 @@ fn wrap_header_with_link(level: usize,
|
|||
id_counter: &mut HashMap<String, usize>,
|
||||
filepath: &str)
|
||||
-> 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);
|
||||
|
||||
|
@ -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
|
||||
// <base href="../"> pointing to the root folder. This function *fixes*
|
||||
// that in a very inelegant way
|
||||
|
@ -555,8 +530,7 @@ fn fix_code_blocks(html: &str) -> String {
|
|||
before = before,
|
||||
classes = classes,
|
||||
after = after)
|
||||
})
|
||||
.into_owned()
|
||||
}).into_owned()
|
||||
}
|
||||
|
||||
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
|
||||
text.to_owned()
|
||||
}
|
||||
})
|
||||
.into_owned()
|
||||
}).into_owned()
|
||||
}
|
||||
|
||||
fn partition_source(s: &str) -> (String, String) {
|
||||
|
@ -624,26 +597,6 @@ struct RenderItemContext<'a> {
|
|||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -687,12 +640,4 @@ mod tests {
|
|||
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 navigation;
|
||||
|
|
|
@ -4,3 +4,6 @@ pub use self::hbs_renderer::HtmlHandlebars;
|
|||
|
||||
mod hbs_renderer;
|
||||
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 {
|
||||
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 {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
|
@ -337,6 +346,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
|||
color: #333;
|
||||
background-color: #fff;
|
||||
/* Inline code */
|
||||
/* Search */
|
||||
}
|
||||
.light .content .header:link,
|
||||
.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 {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.light #searchresults a,
|
||||
.light .content a:link,
|
||||
.light a:visited,
|
||||
.light a > .hljs {
|
||||
|
@ -499,10 +510,34 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
|||
.light ::-webkit-scrollbar-thumb {
|
||||
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 {
|
||||
color: #98a3ad;
|
||||
background-color: #141617;
|
||||
/* Inline code */
|
||||
/* Search */
|
||||
}
|
||||
.coal .content .header:link,
|
||||
.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 {
|
||||
background-color: #292c2f;
|
||||
}
|
||||
.coal #searchresults a,
|
||||
.coal .content a:link,
|
||||
.coal a:visited,
|
||||
.coal a > .hljs {
|
||||
|
@ -665,10 +701,34 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
|||
.coal ::-webkit-scrollbar-thumb {
|
||||
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 {
|
||||
color: #bcbdd0;
|
||||
background-color: #161923;
|
||||
/* Inline code */
|
||||
/* Search */
|
||||
}
|
||||
.navy .content .header:link,
|
||||
.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 {
|
||||
background-color: #282d3f;
|
||||
}
|
||||
.navy #searchresults a,
|
||||
.navy .content a:link,
|
||||
.navy a:visited,
|
||||
.navy a > .hljs {
|
||||
|
@ -831,10 +892,34 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
|||
.navy ::-webkit-scrollbar-thumb {
|
||||
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 {
|
||||
color: #262625;
|
||||
background-color: #e1e1db;
|
||||
/* Inline code */
|
||||
/* Search */
|
||||
}
|
||||
.rust .content .header:link,
|
||||
.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 {
|
||||
background-color: #3b2e2a;
|
||||
}
|
||||
.rust #searchresults a,
|
||||
.rust .content a:link,
|
||||
.rust a:visited,
|
||||
.rust a > .hljs {
|
||||
|
@ -997,10 +1083,34 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
|||
.rust ::-webkit-scrollbar-thumb {
|
||||
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 {
|
||||
color: #c5c5c5;
|
||||
background-color: #0f1419;
|
||||
/* Inline code */
|
||||
/* Search */
|
||||
}
|
||||
.ayu .content .header:link,
|
||||
.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 {
|
||||
background-color: #14191f;
|
||||
}
|
||||
.ayu #searchresults a,
|
||||
.ayu .content a:link,
|
||||
.ayu a:visited,
|
||||
.ayu a > .hljs {
|
||||
|
@ -1163,6 +1274,29 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
|||
.ayu ::-webkit-scrollbar-thumb {
|
||||
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 {
|
||||
#sidebar,
|
||||
#menu-bar,
|
||||
|
@ -1243,3 +1377,66 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
|||
.tooltipped .tooltiptext {
|
||||
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() {
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
if (window.search && window.search.hasFocus()) { return; }
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}" class="sidebar-visible">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
|
@ -112,6 +113,11 @@
|
|||
<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>
|
||||
</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>
|
||||
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
|
@ -124,6 +130,17 @@
|
|||
</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 -->
|
||||
<script type="text/javascript">
|
||||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||
|
@ -221,12 +238,21 @@
|
|||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playpens_editable}}
|
||||
<script src="{{ ace_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="{{ theme_dawn_js }}" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ theme_tomorrow_night_js }}" type="text/javascript" charset="utf-8"></script>
|
||||
{{#if playpen_js}}
|
||||
<script src="ace.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="theme-dawn.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 is_print}}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
#![allow(missing_docs)] // FIXME: Document this
|
||||
#![allow(missing_docs)]
|
||||
|
||||
pub mod playpen_editor;
|
||||
|
||||
#[cfg(feature = "search")]
|
||||
pub mod searcher;
|
||||
|
||||
use std::path::Path;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
@ -52,6 +56,8 @@ pub struct 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 {
|
||||
let theme_dir = theme_dir.as_ref();
|
||||
let mut theme = Theme::default();
|
||||
|
|
|
@ -1,70 +1,7 @@
|
|||
use std::path::Path;
|
||||
|
||||
use theme::load_file_contents;
|
||||
//! Theme dependencies for the playpen editor.
|
||||
|
||||
pub static JS: &'static [u8] = include_bytes!("editor.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 THEME_DAWN_JS: &'static [u8] = include_bytes!("theme-dawn.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 'print'
|
||||
@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 }
|
||||
|
||||
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 {
|
||||
margin: 0 auto;
|
||||
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-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'
|
||||
|
|
|
@ -84,7 +84,10 @@
|
|||
background-color: $sidebar-bg
|
||||
}
|
||||
|
||||
.content a:link, a:visited, a > .hljs {
|
||||
#searchresults a,
|
||||
.content a:link,
|
||||
a:visited,
|
||||
a > .hljs {
|
||||
color: $links
|
||||
}
|
||||
|
||||
|
@ -188,4 +191,32 @@
|
|||
::-webkit-scrollbar-thumb {
|
||||
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-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'
|
||||
|
|
|
@ -16,7 +16,7 @@ $icons-hover = #333333
|
|||
|
||||
$links = #4183c4
|
||||
|
||||
$inline-code-color = #6e6b5e;
|
||||
$inline-code-color = #6e6b5e
|
||||
|
||||
$theme-popup-bg = #fafafa
|
||||
$theme-popup-border = #cccccc
|
||||
|
@ -29,4 +29,13 @@ $table-border-color = darken($bg, 5%)
|
|||
$table-header-bg = darken($bg, 20%)
|
||||
$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'
|
||||
|
|
|
@ -29,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
|
|||
$table-header-bg = lighten($bg, 20%)
|
||||
$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'
|
||||
|
|
|
@ -29,4 +29,13 @@ $table-border-color = darken($bg, 5%)
|
|||
$table-header-bg = #b3a497
|
||||
$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'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::path::{Component, Path, PathBuf};
|
||||
use errors::*;
|
||||
use std::io::Read;
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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.
|
||||
/// 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.
|
||||
|
||||
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||
debug!("path_to_root");
|
||||
// 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
|
||||
/// it checks every directory in the path to see if it exists,
|
||||
/// and if it does not it will be created.
|
||||
|
||||
pub fn create_file(path: &Path) -> Result<File> {
|
||||
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
|
||||
|
||||
pub fn remove_dir_content(dir: &Path) -> Result<()> {
|
||||
for item in fs::read_dir(dir)? {
|
||||
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
|
||||
/// with the extensions given in the `ext_blacklist` array
|
||||
|
||||
pub fn copy_files_except_ext(
|
||||
from: &Path,
|
||||
to: &Path,
|
||||
|
|
|
@ -3,13 +3,71 @@
|
|||
pub mod fs;
|
||||
mod string;
|
||||
use errors::Error;
|
||||
use regex::Regex;
|
||||
|
||||
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
|
||||
OPTION_ENABLE_TABLES};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
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.
|
||||
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
||||
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 {
|
||||
use super::super::convert_quotes_to_curly;
|
||||
|
||||
|
|
|
@ -3,12 +3,11 @@
|
|||
extern crate mdbook;
|
||||
extern crate tempdir;
|
||||
|
||||
use std::fs::File;
|
||||
#[cfg(not(windows))]
|
||||
use std::path::Path;
|
||||
use tempdir::TempDir;
|
||||
use mdbook::config::Config;
|
||||
use mdbook::MDBook;
|
||||
use mdbook::renderer::RenderContext;
|
||||
|
||||
#[test]
|
||||
fn passing_alternate_backend() {
|
||||
|
@ -39,6 +38,7 @@ fn alternate_backend_with_arguments() {
|
|||
}
|
||||
|
||||
/// Get a command which will pipe `stdin` to the provided file.
|
||||
#[cfg(not(windows))]
|
||||
fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
|
||||
let out_file = out_file.as_ref();
|
||||
|
||||
|
@ -52,6 +52,9 @@ fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
|
|||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn backends_receive_render_context_via_stdin() {
|
||||
use std::fs::File;
|
||||
use mdbook::renderer::RenderContext;
|
||||
|
||||
let temp = TempDir::new("output").unwrap();
|
||||
let out_file = temp.path().join("out.txt");
|
||||
let cmd = tee_command(&out_file);
|
||||
|
|
|
@ -1 +1,20 @@
|
|||
# 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();
|
||||
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