From b2ad669c619849fb664e5015486382c91e3048f5 Mon Sep 17 00:00:00 2001 From: Matt Ickstadt Date: Wed, 7 Mar 2018 07:02:06 -0600 Subject: [PATCH] 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 --- .gitattributes | 4 + Cargo.lock | 92 + Cargo.toml | 7 +- book-example/book.toml | 9 + book-example/src/format/config.md | 59 +- book-example/src/misc/contributors.md | 2 + ci/script.sh | 2 + src/bin/mdbook.rs | 2 + src/book/book.rs | 43 +- src/config.rs | 90 +- src/lib.rs | 17 +- src/renderer/html_handlebars/hbs_renderer.rs | 199 +- src/renderer/html_handlebars/helpers/mod.rs | 2 +- src/renderer/html_handlebars/mod.rs | 3 + src/renderer/html_handlebars/search.rs | 241 ++ src/theme/book.css | 197 ++ src/theme/book.js | 1 + src/theme/index.hbs | 40 +- src/theme/mod.rs | 8 +- src/theme/playpen_editor/mod.rs | 65 +- src/theme/searcher/elasticlunr.min.js | 10 + src/theme/searcher/mark.min.js | 7 + src/theme/searcher/mod.rs | 6 + src/theme/searcher/searcher.js | 457 ++++ src/theme/stylus/book.styl | 1 + src/theme/stylus/general.styl | 10 + src/theme/stylus/searchbar.styl | 67 + src/theme/stylus/themes/ayu.styl | 9 + src/theme/stylus/themes/base.styl | 33 +- src/theme/stylus/themes/coal.styl | 9 + src/theme/stylus/themes/light.styl | 11 +- src/theme/stylus/themes/navy.styl | 9 + src/theme/stylus/themes/rust.styl | 9 + src/utils/fs.rs | 29 +- src/utils/mod.rs | 81 + tests/alternate_backends.rs | 7 +- tests/dummy_book/src/conclusion.md | 21 +- tests/rendered_output.rs | 100 + tests/searchindex_fixture.json | 2158 ++++++++++++++++++ 39 files changed, 3861 insertions(+), 256 deletions(-) create mode 100644 src/renderer/html_handlebars/search.rs create mode 100644 src/theme/searcher/elasticlunr.min.js create mode 100644 src/theme/searcher/mark.min.js create mode 100644 src/theme/searcher/mod.rs create mode 100644 src/theme/searcher/searcher.js create mode 100644 src/theme/stylus/searchbar.styl create mode 100644 tests/searchindex_fixture.json diff --git a/.gitattributes b/.gitattributes index 45bca848..81c7df5e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,7 @@ * text=auto eol=lf *.rs rust +*.woff -text +*.ttf -text +*.otf -text +*.png -text diff --git a/Cargo.lock b/Cargo.lock index 5a2bdce7..408045e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index fb5a8734..d6d524b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/book-example/book.toml b/book-example/book.toml index df1ed5c5..4f953590 100644 --- a/book-example/book.toml +++ b/book-example/book.toml @@ -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 diff --git a/book-example/src/format/config.md b/book-example/src/format/config.md index db5b0e88..899caaae 100644 --- a/book-example/src/format/config.md +++ b/book-example/src/format/config.md @@ -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 ``` @@ -145,4 +194,4 @@ override the book's title without needing to touch your `book.toml`. The latter case may be useful in situations where `mdbook` is invoked from a script or CI, where it sometimes isn't possible to update the -`book.toml` before building. \ No newline at end of file +`book.toml` before building. diff --git a/book-example/src/misc/contributors.md b/book-example/src/misc/contributors.md index 94191db4..ef03ecba 100644 --- a/book-example/src/misc/contributors.md +++ b/book-example/src/misc/contributors.md @@ -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) diff --git a/ci/script.sh b/ci/script.sh index f07c4451..cccf6a31 100644 --- a/ci/script.sh +++ b/ci/script.sh @@ -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 } diff --git a/src/bin/mdbook.rs b/src/bin/mdbook.rs index 16f2c796..7f619bb6 100644 --- a/src/bin/mdbook.rs +++ b/src/bin/mdbook.rs @@ -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(); diff --git a/src/book/book.rs b/src/book/book.rs index adb49ade..b8e61478 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -152,15 +152,20 @@ pub struct Chapter { pub sub_items: Vec, /// 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, } impl Chapter { /// Create a new chapter with the provided content. - pub fn new>(name: &str, content: String, path: P) -> Chapter { + pub fn new>(name: &str, content: String, path: P, parent_names: Vec) + -> 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>(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>(item: &SummaryItem, src_dir: P) -> Result { +fn load_summary_item>(item: &SummaryItem, src_dir: P, parent_names: Vec) + -> Result +{ 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>(link: &Link, src_dir: P) -> Result { +fn load_chapter>(link: &Link, src_dir: P, parent_names: Vec) + -> Result +{ debug!("Loading {} ({})", link.name, link.location.display()); let src_dir = src_dir.as_ref(); @@ -218,12 +229,14 @@ fn load_chapter>(link: &Link, src_dir: P) -> Result { .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::>>()?; 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(), )), ], }), diff --git a/src/config.rs b/src/config.rs index 5d252b17..253c2fd1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,13 +1,13 @@ //! Mdbook's configuration system. -//! +//! //! The main entrypoint of the `config` module is the `Config` struct. This acts //! essentially as a bag of configuration information, with a couple -//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support +//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support //! for arbitrary data which is exposed to plugins and alternate backends. -//! -//! +//! +//! //! # Examples -//! +//! //! ```rust //! # extern crate mdbook; //! # use mdbook::errors::*; @@ -15,31 +15,31 @@ //! use std::path::PathBuf; //! use mdbook::Config; //! use toml::Value; -//! +//! //! # fn run() -> Result<()> { //! let src = r#" //! [book] //! title = "My Book" //! authors = ["Michael-F-Bryan"] -//! +//! //! [build] //! src = "out" -//! +//! //! [other-table.foo] //! bar = 123 //! "#; -//! +//! //! // load the `Config` from a toml string //! let mut cfg = Config::from_str(src)?; -//! +//! //! // retrieve a nested value //! let bar = cfg.get("other-table.foo.bar").cloned(); //! assert_eq!(bar, Some(Value::Integer(123))); -//! +//! //! // Set the `output.html.theme` directory //! assert!(cfg.get("output.html").is_none()); //! cfg.set("output.html.theme", "./themes"); -//! +//! //! // then load it again, automatically deserializing to a `PathBuf`. //! let got: PathBuf = cfg.get_deserialized("output.html.theme")?; //! assert_eq!(got, PathBuf::from("./themes")); @@ -410,7 +410,7 @@ pub struct HtmlConfig { pub google_analytics: Option, /// Additional CSS stylesheets to include in the rendered page's ``. pub additional_css: Vec, - /// Additional JS scripts to include at the bottom of the rendered page's + /// Additional JS scripts to include at the bottom of the rendered page's /// ``. pub additional_js: Vec, /// Playpen settings. @@ -425,29 +425,79 @@ pub struct HtmlConfig { pub livereload_url: Option, /// Should section labels be rendered? pub no_section_label: bool, + /// Search settings. If `None`, the default will be used. + pub search: Option, } /// 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, diff --git a/src/lib.rs b/src/lib.rs index 559cec30..61040e6d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,20 +52,20 @@ //! //! ## Implementing a new Backend //! -//! `mdbook` has a fairly flexible mechanism for creating additional backends +//! `mdbook` has a fairly flexible mechanism for creating additional backends //! for your book. The general idea is you'll add an extra table in the book's //! `book.toml` which specifies an executable to be invoked by `mdbook`. This -//! executable will then be called during a build, with an in-memory +//! executable will then be called during a build, with an in-memory //! representation ([`RenderContext`]) of the book being passed to the -//! subprocess via `stdin`. -//! -//! The [`RenderContext`] gives the backend access to the contents of +//! subprocess via `stdin`. +//! +//! The [`RenderContext`] gives the backend access to the contents of //! `book.toml` and lets it know which directory all generated artefacts should //! be placed in. For a much more in-depth explanation, consult the [relevant //! chapter] in the *For Developers* section of the user guide. -//! -//! To make creating a backend easier, the `mdbook` crate can be imported -//! directly, making deserializing the `RenderContext` easy and giving you +//! +//! To make creating a backend easier, the `mdbook` crate can be imported +//! directly, making deserializing the `RenderContext` easy and giving you //! access to the various methods for working with the [`Config`]. //! //! [user guide]: https://rust-lang-nursery.github.io/mdBook/ @@ -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 { diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 6535a4a3..8e554ac3 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -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>( - &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::>() .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,16 +401,22 @@ 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![]; for item in book.iter() { @@ -469,7 +471,7 @@ fn wrap_header_with_link(level: usize, id_counter: &mut HashMap, 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] = &["", - "", - "", - "", - "", - "", - "<", - ">", - "&", - "'", - """]; - 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 // 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::() -} - -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::() -} - - #[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"); - } } diff --git a/src/renderer/html_handlebars/helpers/mod.rs b/src/renderer/html_handlebars/helpers/mod.rs index 62fc6149..ce122e9f 100644 --- a/src/renderer/html_handlebars/helpers/mod.rs +++ b/src/renderer/html_handlebars/helpers/mod.rs @@ -1,2 +1,2 @@ -pub mod navigation; pub mod toc; +pub mod navigation; diff --git a/src/renderer/html_handlebars/mod.rs b/src/renderer/html_handlebars/mod.rs index aca09dbe..f1155ed7 100644 --- a/src/renderer/html_handlebars/mod.rs +++ b/src/renderer/html_handlebars/mod.rs @@ -4,3 +4,6 @@ pub use self::hbs_renderer::HtmlHandlebars; mod hbs_renderer; mod helpers; + +#[cfg(feature = "search")] +mod search; diff --git a/src/renderer/html_handlebars/search.rs b/src/renderer/html_handlebars/search.rs new file mode 100644 index 00000000..88125449 --- /dev/null +++ b/src/renderer/html_handlebars/search.rs @@ -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, + 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 { + // 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() +} diff --git a/src/theme/book.css b/src/theme/book.css index dd9d8e7f..3f0d0f58 100644 --- a/src/theme/book.css +++ b/src/theme/book.css @@ -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; +} diff --git a/src/theme/book.js b/src/theme/book.js index df2c1251..4da984c7 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -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': diff --git a/src/theme/index.hbs b/src/theme/index.hbs index 64e1898f..a1949b4f 100644 --- a/src/theme/index.hbs +++ b/src/theme/index.hbs @@ -1,6 +1,7 @@ + {{ title }} @@ -112,9 +113,14 @@
  • + {{#if search_enabled}} + + {{/if}} -

    {{ book_title }}

    +

    {{ book_title }}

    + {{#if search_enabled}} +
    + +
    +
    +
    +
      +
    +
    + {{/if}} + {{/if}} - {{#if playpens_editable}} - - - - - + {{#if playpen_js}} + + + + + + {{/if}} + + {{#if search_enabled}} + + {{/if}} + {{#if search_js}} + + + {{/if}} {{#if is_print}} diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 0633be34..fc7c809f 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -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>(theme_dir: P) -> Self { let theme_dir = theme_dir.as_ref(); let mut theme = Theme::default(); diff --git a/src/theme/playpen_editor/mod.rs b/src/theme/playpen_editor/mod.rs index cfef26ed..447e81ed 100644 --- a/src/theme/playpen_editor/mod.rs +++ b/src/theme/playpen_editor/mod.rs @@ -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, - pub ace_js: Vec, - pub mode_rust_js: Vec, - pub theme_dawn_js: Vec, - pub theme_tomorrow_night_js: Vec, -} - -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 - } -} diff --git a/src/theme/searcher/elasticlunr.min.js b/src/theme/searcher/elasticlunr.min.js new file mode 100644 index 00000000..94b20dd2 --- /dev/null +++ b/src/theme/searcher/elasticlunr.min.js @@ -0,0 +1,10 @@ +/** + * elasticlunr - http://weixsong.github.io + * Lightweight full-text search engine in Javascript for browser search and offline search. - 0.9.5 + * + * Copyright (C) 2017 Oliver Nightingale + * Copyright (C) 2017 Wei Song + * MIT Licensed + * @license + */ +!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o1&&void 0!==arguments[1])||arguments[1],i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:5e3;t(this,e),this.ctx=n,this.iframes=r,this.exclude=i,this.iframesTimeout=o}return n(e,[{key:"getContexts",value:function(){var e=[];return(void 0!==this.ctx&&this.ctx?NodeList.prototype.isPrototypeOf(this.ctx)?Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?this.ctx:"string"==typeof this.ctx?Array.prototype.slice.call(document.querySelectorAll(this.ctx)):[this.ctx]:[]).forEach(function(t){var n=e.filter(function(e){return e.contains(t)}).length>0;-1!==e.indexOf(t)||n||e.push(t)}),e}},{key:"getIframeContents",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},r=void 0;try{var i=e.contentWindow;if(r=i.document,!i||!r)throw new Error("iframe inaccessible")}catch(e){n()}r&&t(r)}},{key:"isIframeBlank",value:function(e){var t="about:blank",n=e.getAttribute("src").trim();return e.contentWindow.location.href===t&&n!==t&&n}},{key:"observeIframeLoad",value:function(e,t,n){var r=this,i=!1,o=null,a=function a(){if(!i){i=!0,clearTimeout(o);try{r.isIframeBlank(e)||(e.removeEventListener("load",a),r.getIframeContents(e,t,n))}catch(e){n()}}};e.addEventListener("load",a),o=setTimeout(a,this.iframesTimeout)}},{key:"onIframeReady",value:function(e,t,n){try{"complete"===e.contentWindow.document.readyState?this.isIframeBlank(e)?this.observeIframeLoad(e,t,n):this.getIframeContents(e,t,n):this.observeIframeLoad(e,t,n)}catch(e){n()}}},{key:"waitForIframes",value:function(e,t){var n=this,r=0;this.forEachIframe(e,function(){return!0},function(e){r++,n.waitForIframes(e.querySelector("html"),function(){--r||t()})},function(e){e||t()})}},{key:"forEachIframe",value:function(t,n,r){var i=this,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},a=t.querySelectorAll("iframe"),s=a.length,c=0;a=Array.prototype.slice.call(a);var u=function(){--s<=0&&o(c)};s||u(),a.forEach(function(t){e.matches(t,i.exclude)?u():i.onIframeReady(t,function(e){n(t)&&(c++,r(e)),u()},u)})}},{key:"createIterator",value:function(e,t,n){return document.createNodeIterator(e,t,n,!1)}},{key:"createInstanceOnIframe",value:function(t){return new e(t.querySelector("html"),this.iframes)}},{key:"compareNodeIframe",value:function(e,t,n){if(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_PRECEDING){if(null===t)return!0;if(t.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)return!0}return!1}},{key:"getIteratorNode",value:function(e){var t=e.previousNode();return{prevNode:t,node:null===t?e.nextNode():e.nextNode()&&e.nextNode()}}},{key:"checkIframeFilter",value:function(e,t,n,r){var i=!1,o=!1;return r.forEach(function(e,t){e.val===n&&(i=t,o=e.handled)}),this.compareNodeIframe(e,t,n)?(!1!==i||o?!1===i||o||(r[i].handled=!0):r.push({val:n,handled:!0}),!0):(!1===i&&r.push({val:n,handled:!1}),!1)}},{key:"handleOpenIframes",value:function(e,t,n,r){var i=this;e.forEach(function(e){e.handled||i.getIframeContents(e.val,function(e){i.createInstanceOnIframe(e).forEachNode(t,n,r)})})}},{key:"iterateThroughNodes",value:function(e,t,n,r,i){for(var o,a=this,s=this.createIterator(t,e,r),c=[],u=[],l=void 0,h=void 0;void 0,o=a.getIteratorNode(s),h=o.prevNode,l=o.node;)this.iframes&&this.forEachIframe(t,function(e){return a.checkIframeFilter(l,h,e,c)},function(t){a.createInstanceOnIframe(t).forEachNode(e,function(e){return u.push(e)},r)}),u.push(l);u.forEach(function(e){n(e)}),this.iframes&&this.handleOpenIframes(c,e,n,r),i()}},{key:"forEachNode",value:function(e,t,n){var r=this,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},o=this.getContexts(),a=o.length;a||i(),o.forEach(function(o){var s=function(){r.iterateThroughNodes(e,o,t,n,function(){--a<=0&&i()})};r.iframes?r.waitForIframes(o,s):s()})}}],[{key:"matches",value:function(e,t){var n="string"==typeof t?[t]:t,r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;if(r){var i=!1;return n.every(function(t){return!r.call(e,t)||(i=!0,!1)}),i}return!1}}]),e}(),o=function(){function e(n){t(this,e),this.opt=r({},{diacritics:!0,synonyms:{},accuracy:"partially",caseSensitive:!1,ignoreJoiners:!1,ignorePunctuation:[],wildcards:"disabled"},n)}return n(e,[{key:"create",value:function(e){return"disabled"!==this.opt.wildcards&&(e=this.setupWildcardsRegExp(e)),e=this.escapeStr(e),Object.keys(this.opt.synonyms).length&&(e=this.createSynonymsRegExp(e)),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),this.opt.diacritics&&(e=this.createDiacriticsRegExp(e)),e=this.createMergedBlanksRegExp(e),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.createJoinersRegExp(e)),"disabled"!==this.opt.wildcards&&(e=this.createWildcardsRegExp(e)),e=this.createAccuracyRegExp(e),new RegExp(e,"gm"+(this.opt.caseSensitive?"":"i"))}},{key:"escapeStr",value:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}},{key:"createSynonymsRegExp",value:function(e){var t=this.opt.synonyms,n=this.opt.caseSensitive?"":"i",r=this.opt.ignoreJoiners||this.opt.ignorePunctuation.length?"\0":"";for(var i in t)if(t.hasOwnProperty(i)){var o=t[i],a="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(i):this.escapeStr(i),s="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(o):this.escapeStr(o);""!==a&&""!==s&&(e=e.replace(new RegExp("("+this.escapeStr(a)+"|"+this.escapeStr(s)+")","gm"+n),r+"("+this.processSynonyms(a)+"|"+this.processSynonyms(s)+")"+r))}return e}},{key:"processSynonyms",value:function(e){return(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),e}},{key:"setupWildcardsRegExp",value:function(e){return(e=e.replace(/(?:\\)*\?/g,function(e){return"\\"===e.charAt(0)?"?":""})).replace(/(?:\\)*\*/g,function(e){return"\\"===e.charAt(0)?"*":""})}},{key:"createWildcardsRegExp",value:function(e){var t="withSpaces"===this.opt.wildcards;return e.replace(/\u0001/g,t?"[\\S\\s]?":"\\S?").replace(/\u0002/g,t?"[\\S\\s]*?":"\\S*")}},{key:"setupIgnoreJoinersRegExp",value:function(e){return e.replace(/[^(|)\\]/g,function(e,t,n){var r=n.charAt(t+1);return/[(|)\\]/.test(r)||""===r?e:e+"\0"})}},{key:"createJoinersRegExp",value:function(e){var t=[],n=this.opt.ignorePunctuation;return Array.isArray(n)&&n.length&&t.push(this.escapeStr(n.join(""))),this.opt.ignoreJoiners&&t.push("\\u00ad\\u200b\\u200c\\u200d"),t.length?e.split(/\u0000+/).join("["+t.join("")+"]*"):e}},{key:"createDiacriticsRegExp",value:function(e){var t=this.opt.caseSensitive?"":"i",n=this.opt.caseSensitive?["aàáảãạăằắẳẵặâầấẩẫậäåāą","AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćč","CÇĆČ","dđď","DĐĎ","eèéẻẽẹêềếểễệëěēę","EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïī","IÌÍỈĨỊÎÏĪ","lł","LŁ","nñňń","NÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøō","OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rř","RŘ","sšśșş","SŠŚȘŞ","tťțţ","TŤȚŢ","uùúủũụưừứửữựûüůū","UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿ","YÝỲỶỸỴŸ","zžżź","ZŽŻŹ"]:["aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćčCÇĆČ","dđďDĐĎ","eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïīIÌÍỈĨỊÎÏĪ","lłLŁ","nñňńNÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rřRŘ","sšśșşSŠŚȘŞ","tťțţTŤȚŢ","uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿYÝỲỶỸỴŸ","zžżźZŽŻŹ"],r=[];return e.split("").forEach(function(i){n.every(function(n){if(-1!==n.indexOf(i)){if(r.indexOf(n)>-1)return!1;e=e.replace(new RegExp("["+n+"]","gm"+t),"["+n+"]"),r.push(n)}return!0})}),e}},{key:"createMergedBlanksRegExp",value:function(e){return e.replace(/[\s]+/gim,"[\\s]+")}},{key:"createAccuracyRegExp",value:function(e){var t=this,n=this.opt.accuracy,r="string"==typeof n?n:n.value,i="";switch(("string"==typeof n?[]:n.limiters).forEach(function(e){i+="|"+t.escapeStr(e)}),r){case"partially":default:return"()("+e+")";case"complementary":return"()([^"+(i="\\s"+(i||this.escapeStr("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿")))+"]*"+e+"[^"+i+"]*)";case"exactly":return"(^|\\s"+i+")("+e+")(?=$|\\s"+i+")"}}}]),e}(),a=function(){function a(e){t(this,a),this.ctx=e,this.ie=!1;var n=window.navigator.userAgent;(n.indexOf("MSIE")>-1||n.indexOf("Trident")>-1)&&(this.ie=!0)}return n(a,[{key:"log",value:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"debug",r=this.opt.log;this.opt.debug&&"object"===(void 0===r?"undefined":e(r))&&"function"==typeof r[n]&&r[n]("mark.js: "+t)}},{key:"getSeparatedKeywords",value:function(e){var t=this,n=[];return e.forEach(function(e){t.opt.separateWordSearch?e.split(" ").forEach(function(e){e.trim()&&-1===n.indexOf(e)&&n.push(e)}):e.trim()&&-1===n.indexOf(e)&&n.push(e)}),{keywords:n.sort(function(e,t){return t.length-e.length}),length:n.length}}},{key:"isNumeric",value:function(e){return Number(parseFloat(e))==e}},{key:"checkRanges",value:function(e){var t=this;if(!Array.isArray(e)||"[object Object]"!==Object.prototype.toString.call(e[0]))return this.log("markRanges() will only accept an array of objects"),this.opt.noMatch(e),[];var n=[],r=0;return e.sort(function(e,t){return e.start-t.start}).forEach(function(e){var i=t.callNoMatchOnInvalidRanges(e,r),o=i.start,a=i.end;i.valid&&(e.start=o,e.length=a-o,n.push(e),r=a)}),n}},{key:"callNoMatchOnInvalidRanges",value:function(e,t){var n=void 0,r=void 0,i=!1;return e&&void 0!==e.start?(r=(n=parseInt(e.start,10))+parseInt(e.length,10),this.isNumeric(e.start)&&this.isNumeric(e.length)&&r-t>0&&r-n>0?i=!0:(this.log("Ignoring invalid or overlapping range: "+JSON.stringify(e)),this.opt.noMatch(e))):(this.log("Ignoring invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:n,end:r,valid:i}}},{key:"checkWhitespaceRanges",value:function(e,t,n){var r=void 0,i=!0,o=n.length,a=t-o,s=parseInt(e.start,10)-a;return(r=(s=s>o?o:s)+parseInt(e.length,10))>o&&(r=o,this.log("End range automatically set to the max value of "+o)),s<0||r-s<0||s>o||r>o?(i=!1,this.log("Invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)):""===n.substring(s,r).replace(/\s+/g,"")&&(i=!1,this.log("Skipping whitespace only range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:s,end:r,valid:i}}},{key:"getTextNodes",value:function(e){var t=this,n="",r=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,function(e){r.push({start:n.length,end:(n+=e.textContent).length,node:e})},function(e){return t.matchesExclude(e.parentNode)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},function(){e({value:n,nodes:r})})}},{key:"matchesExclude",value:function(e){return i.matches(e,this.opt.exclude.concat(["script","style","title","head","html"]))}},{key:"wrapRangeInTextNode",value:function(e,t,n){var r=this.opt.element?this.opt.element:"mark",i=e.splitText(t),o=i.splitText(n-t),a=document.createElement(r);return a.setAttribute("data-markjs","true"),this.opt.className&&a.setAttribute("class",this.opt.className),a.textContent=i.textContent,i.parentNode.replaceChild(a,i),o}},{key:"wrapRangeInMappedTextNode",value:function(e,t,n,r,i){var o=this;e.nodes.every(function(a,s){var c=e.nodes[s+1];if(void 0===c||c.start>t){if(!r(a.node))return!1;var u=t-a.start,l=(n>a.end?a.end:n)-a.start,h=e.value.substr(0,a.start),f=e.value.substr(l+a.start);if(a.node=o.wrapRangeInTextNode(a.node,u,l),e.value=h+f,e.nodes.forEach(function(t,n){n>=s&&(e.nodes[n].start>0&&n!==s&&(e.nodes[n].start-=l),e.nodes[n].end-=l)}),n-=l,i(a.node.previousSibling,a.start),!(n>a.end))return!1;t=a.end}return!0})}},{key:"wrapGroups",value:function(e,t,n,r){return r((e=this.wrapRangeInTextNode(e,t,t+n)).previousSibling),e}},{key:"separateGroups",value:function(e,t,n,r,i){for(var o=t.length,a=1;a-1&&r(t[a],e)&&(e=this.wrapGroups(e,s,t[a].length,i))}return e}},{key:"wrapMatches",value:function(e,t,n,r,i){var o=this,a=0===t?0:t+1;this.getTextNodes(function(t){t.nodes.forEach(function(t){t=t.node;for(var i=void 0;null!==(i=e.exec(t.textContent))&&""!==i[a];){if(o.opt.separateGroups)t=o.separateGroups(t,i,a,n,r);else{if(!n(i[a],t))continue;var s=i.index;if(0!==a)for(var c=1;c': '>', + '"': '"', + "'": ''' + }; + 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 '
    ' + result.doc.breadcrumbs + '' + + '' + + teaser + ''; + } + + 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 . + 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 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("") + } + index = word[2] + word[0].length; + teaser_split.push(body.substring(word[2], index)); + if (word[1] == searchterm_weight) { + teaser_split.push("") + } + }; + + 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); diff --git a/src/theme/stylus/book.styl b/src/theme/stylus/book.styl index 4e88350b..a9eec8ad 100644 --- a/src/theme/stylus/book.styl +++ b/src/theme/stylus/book.styl @@ -9,3 +9,4 @@ @import 'themes' @import 'print' @import 'tooltip' +@import 'searchbar' diff --git a/src/theme/stylus/general.styl b/src/theme/stylus/general.styl index c4a4b023..ead07bf3 100644 --- a/src/theme/stylus/general.styl +++ b/src/theme/stylus/general.styl @@ -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; diff --git a/src/theme/stylus/searchbar.styl b/src/theme/stylus/searchbar.styl new file mode 100644 index 00000000..7dc680e8 --- /dev/null +++ b/src/theme/stylus/searchbar.styl @@ -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; + } +} diff --git a/src/theme/stylus/themes/ayu.styl b/src/theme/stylus/themes/ayu.styl index 7f5c79ad..1c19ff67 100644 --- a/src/theme/stylus/themes/ayu.styl +++ b/src/theme/stylus/themes/ayu.styl @@ -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' diff --git a/src/theme/stylus/themes/base.styl b/src/theme/stylus/themes/base.styl index 520015c1..86e8eafb 100644 --- a/src/theme/stylus/themes/base.styl +++ b/src/theme/stylus/themes/base.styl @@ -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; + } } diff --git a/src/theme/stylus/themes/coal.styl b/src/theme/stylus/themes/coal.styl index 68b6449e..4f9364ea 100644 --- a/src/theme/stylus/themes/coal.styl +++ b/src/theme/stylus/themes/coal.styl @@ -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' diff --git a/src/theme/stylus/themes/light.styl b/src/theme/stylus/themes/light.styl index 1fc42d3b..d2b77e21 100644 --- a/src/theme/stylus/themes/light.styl +++ b/src/theme/stylus/themes/light.styl @@ -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' diff --git a/src/theme/stylus/themes/navy.styl b/src/theme/stylus/themes/navy.styl index d9832062..d4c038af 100644 --- a/src/theme/stylus/themes/navy.styl +++ b/src/theme/stylus/themes/navy.styl @@ -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' diff --git a/src/theme/stylus/themes/rust.styl b/src/theme/stylus/themes/rust.styl index c3ba03c4..c5a2eb8f 100644 --- a/src/theme/stylus/themes/rust.styl +++ b/src/theme/stylus/themes/rust.styl @@ -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' diff --git a/src/utils/fs.rs b/src/utils/fs.rs index 9189eeb2..7ae13fd1 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -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>(path: P) -> Result { @@ -16,6 +16,27 @@ pub fn file_to_string>(path: P) -> Result { 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::() +} + +/// Write the given data to a file, creating it first if necessary +pub fn write_file>( + 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>(path: P) -> Result { /// 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>(path: P) -> String { debug!("path_to_root"); // Remove filename and add "../" for every directory @@ -61,7 +81,6 @@ pub fn path_to_root>(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 { debug!("Creating {}", path.display()); @@ -76,7 +95,6 @@ pub fn create_file(path: &Path) -> Result { } /// 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, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 56291aeb..56e0f004 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -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::(); + // 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] = &["", + "", + "", + "", + "", + "", + "<", + ">", + "&", + "'", + """]; + 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; diff --git a/tests/alternate_backends.rs b/tests/alternate_backends.rs index c45da7a4..04555f37 100644 --- a/tests/alternate_backends.rs +++ b/tests/alternate_backends.rs @@ -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>(out_file: P) -> String { let out_file = out_file.as_ref(); @@ -52,6 +52,9 @@ fn tee_command>(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); diff --git a/tests/dummy_book/src/conclusion.md b/tests/dummy_book/src/conclusion.md index b6a587e7..ba121c1f 100644 --- a/tests/dummy_book/src/conclusion.md +++ b/tests/dummy_book/src/conclusion.md @@ -1 +1,20 @@ -# Conclusion \ No newline at end of file +# Conclusion + +

    + +I put <HTML> in here!
    +

    + + diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index c5ec0b83..ff6559d6 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -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"); + } + } +} diff --git a/tests/searchindex_fixture.json b/tests/searchindex_fixture.json new file mode 100644 index 00000000..e4e73790 --- /dev/null +++ b/tests/searchindex_fixture.json @@ -0,0 +1,2158 @@ +{ + "index": { + "documentStore": { + "docInfo": { + "conclusion.html#conclusion": { + "body": 3, + "breadcrumbs": 1, + "title": 1 + }, + "first/includes.html#includes": { + "body": 0, + "breadcrumbs": 3, + "title": 1 + }, + "first/includes.html#summary": { + "body": 9, + "breadcrumbs": 3, + "title": 1 + }, + "first/index.html#first-chapter": { + "body": 2, + "breadcrumbs": 2, + "title": 2 + }, + "first/index.html#some-section": { + "body": 0, + "breadcrumbs": 1, + "title": 1 + }, + "first/nested.html#nested-chapter": { + "body": 4, + "breadcrumbs": 4, + "title": 2 + }, + "first/nested.html#some-section": { + "body": 0, + "breadcrumbs": 3, + "title": 1 + }, + "intro.html#introduction": { + "body": 3, + "breadcrumbs": 1, + "title": 1 + }, + "second.html#second-chapter": { + "body": 20, + "breadcrumbs": 2, + "title": 2 + } + }, + "docs": { + "conclusion.html#conclusion": { + "body": "I put <HTML> in here!", + "breadcrumbs": "Conclusion", + "id": "conclusion.html#conclusion", + "title": "Conclusion" + }, + "first/includes.html#includes": { + "body": "", + "breadcrumbs": "First Chapter » Includes", + "id": "first/includes.html#includes", + "title": "Includes" + }, + "first/includes.html#summary": { + "body": "Introduction First Chapter Nested Chapter Includes Second Chapter Conclusion", + "breadcrumbs": "First Chapter » Summary", + "id": "first/includes.html#summary", + "title": "Summary" + }, + "first/index.html#first-chapter": { + "body": "more text.", + "breadcrumbs": "First Chapter", + "id": "first/index.html#first-chapter", + "title": "First Chapter" + }, + "first/index.html#some-section": { + "body": "", + "breadcrumbs": "Some Section", + "id": "first/index.html#some-section", + "title": "Some Section" + }, + "first/nested.html#nested-chapter": { + "body": "This file has some testable code. assert!(true);", + "breadcrumbs": "First Chapter » Nested Chapter", + "id": "first/nested.html#nested-chapter", + "title": "Nested Chapter" + }, + "first/nested.html#some-section": { + "body": "", + "breadcrumbs": "First Chapter » Some Section", + "id": "first/nested.html#some-section", + "title": "Some Section" + }, + "intro.html#introduction": { + "body": "Here's some interesting text...", + "breadcrumbs": "Introduction", + "id": "intro.html#introduction", + "title": "Introduction" + }, + "second.html#second-chapter": { + "body": "This makes sure you can insert runnable Rust files. fn main() { println!(\"Hello World!\");\n#\n# // You can even hide lines! :D\n# println!(\"I am hidden! Expand the code snippet to see me\");\n}", + "breadcrumbs": "Second Chapter", + "id": "second.html#second-chapter", + "title": "Second Chapter" + } + }, + "length": 9, + "save": true + }, + "fields": [ + "title", + "body", + "breadcrumbs" + ], + "index": { + "body": { + "root": { + "a": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "t": { + "!": { + "(": { + "df": 0, + "docs": {}, + "t": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "u": { + "df": 1, + "docs": { + "first/nested.html#nested-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "df": 0, + "docs": {} + }, + "df": 0, + "docs": {} + } + } + } + } + } + }, + "c": { + "df": 0, + "docs": {}, + "h": { + "a": { + "df": 0, + "docs": {}, + "p": { + "df": 0, + "docs": {}, + "t": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "r": { + "df": 4, + "docs": { + "first/includes.html#summary": { + "tf": 1.7320508075688773 + }, + "first/index.html#first-chapter": { + "tf": 1.0 + }, + "first/nested.html#nested-chapter": { + "tf": 1.0 + }, + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + } + } + }, + "df": 0, + "docs": {} + }, + "o": { + "d": { + "df": 0, + "docs": {}, + "e": { + "df": 2, + "docs": { + "first/nested.html#nested-chapter": { + "tf": 1.0 + }, + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "df": 0, + "docs": {}, + "n": { + "c": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "u": { + "df": 0, + "docs": {}, + "s": { + "df": 2, + "docs": { + "conclusion.html#conclusion": { + "tf": 1.0 + }, + "first/includes.html#summary": { + "tf": 1.0 + } + } + } + } + } + }, + "df": 0, + "docs": {} + } + } + }, + "d": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + }, + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "v": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "n": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + }, + "x": { + "df": 0, + "docs": {}, + "p": { + "a": { + "df": 0, + "docs": {}, + "n": { + "d": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + }, + "df": 0, + "docs": {} + } + }, + "df": 0, + "docs": {} + } + } + }, + "f": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "e": { + "df": 2, + "docs": { + "first/nested.html#nested-chapter": { + "tf": 1.0 + }, + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "r": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "t": { + "df": 2, + "docs": { + "first/includes.html#summary": { + "tf": 1.0 + }, + "first/index.html#first-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "n": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "h": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "e": { + "'": { + "df": 1, + "docs": { + "intro.html#introduction": { + "tf": 1.0 + } + } + }, + "df": 1, + "docs": { + "conclusion.html#conclusion": { + "tf": 1.0 + } + } + } + } + }, + "i": { + "d": { + "d": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "n": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + }, + "df": 0, + "docs": {}, + "e": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "df": 0, + "docs": {} + } + }, + "i": { + "df": 0, + "docs": {}, + "n": { + "c": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "u": { + "d": { + "df": 2, + "docs": { + "first/includes.html#includes": { + "tf": 1.0 + }, + "first/includes.html#summary": { + "tf": 1.0 + } + } + }, + "df": 0, + "docs": {} + } + } + }, + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "t": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "intro.html#introduction": { + "tf": 1.0 + } + } + } + } + } + } + }, + "r": { + "df": 0, + "docs": {}, + "o": { + "d": { + "df": 0, + "docs": {}, + "u": { + "c": { + "df": 0, + "docs": {}, + "t": { + "df": 2, + "docs": { + "first/includes.html#summary": { + "tf": 1.0 + }, + "intro.html#introduction": { + "tf": 1.0 + } + } + } + }, + "df": 0, + "docs": {} + } + }, + "df": 0, + "docs": {} + } + } + } + } + }, + "l": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "n": { + "df": 0, + "docs": {}, + "e": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + }, + "t": { + ";": { + "df": 0, + "docs": {}, + "h": { + "df": 0, + "docs": {}, + "t": { + "df": 0, + "docs": {}, + "m": { + "df": 0, + "docs": {}, + "l": { + "&": { + "df": 0, + "docs": {}, + "g": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "conclusion.html#conclusion": { + "tf": 1.0 + } + } + } + } + }, + "df": 0, + "docs": {} + } + } + } + } + }, + "df": 0, + "docs": {} + } + }, + "m": { + "a": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "n": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "k": { + "df": 0, + "docs": {}, + "e": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + }, + "df": 0, + "docs": {}, + "o": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "e": { + "df": 1, + "docs": { + "first/index.html#first-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "n": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "t": { + "df": 2, + "docs": { + "first/includes.html#summary": { + "tf": 1.0 + }, + "first/nested.html#nested-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "p": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "n": { + "df": 0, + "docs": {}, + "t": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "n": { + "!": { + "(": { + "\"": { + "df": 0, + "docs": {}, + "h": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "o": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + } + } + }, + "i": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "df": 0, + "docs": {} + }, + "df": 0, + "docs": {} + }, + "df": 0, + "docs": {} + } + } + } + } + } + }, + "u": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "conclusion.html#conclusion": { + "tf": 1.0 + } + } + } + } + }, + "r": { + "df": 0, + "docs": {}, + "u": { + "df": 0, + "docs": {}, + "n": { + "df": 0, + "docs": {}, + "n": { + "a": { + "b": { + "df": 0, + "docs": {}, + "l": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "df": 0, + "docs": {} + }, + "df": 0, + "docs": {} + } + }, + "s": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "s": { + "df": 0, + "docs": {}, + "e": { + "c": { + "df": 0, + "docs": {}, + "o": { + "df": 0, + "docs": {}, + "n": { + "d": { + "df": 2, + "docs": { + "first/includes.html#summary": { + "tf": 1.0 + }, + "second.html#second-chapter": { + "tf": 1.0 + } + } + }, + "df": 0, + "docs": {} + } + }, + "t": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "o": { + "df": 0, + "docs": {}, + "n": { + "df": 2, + "docs": { + "first/index.html#some-section": { + "tf": 1.0 + }, + "first/nested.html#some-section": { + "tf": 1.0 + } + } + } + } + } + } + }, + "df": 0, + "docs": {}, + "e": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "n": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "p": { + "df": 0, + "docs": {}, + "p": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + } + } + } + }, + "u": { + "df": 0, + "docs": {}, + "m": { + "df": 0, + "docs": {}, + "m": { + "a": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "i": { + "df": 1, + "docs": { + "first/includes.html#summary": { + "tf": 1.0 + } + } + } + } + }, + "df": 0, + "docs": {} + } + }, + "r": { + "df": 0, + "docs": {}, + "e": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "t": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "t": { + "a": { + "b": { + "df": 0, + "docs": {}, + "l": { + "df": 1, + "docs": { + "first/nested.html#nested-chapter": { + "tf": 1.0 + } + } + } + }, + "df": 0, + "docs": {} + }, + "df": 0, + "docs": {} + } + }, + "x": { + "df": 0, + "docs": {}, + "t": { + "df": 2, + "docs": { + "first/index.html#first-chapter": { + "tf": 1.0 + }, + "intro.html#introduction": { + "tf": 1.0 + } + } + } + } + } + }, + "w": { + "df": 0, + "docs": {}, + "o": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "l": { + "d": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + }, + "df": 0, + "docs": {} + } + } + } + } + } + }, + "breadcrumbs": { + "root": { + "a": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "t": { + "!": { + "(": { + "df": 0, + "docs": {}, + "t": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "u": { + "df": 1, + "docs": { + "first/nested.html#nested-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "df": 0, + "docs": {} + }, + "df": 0, + "docs": {} + } + } + } + } + } + }, + "c": { + "df": 0, + "docs": {}, + "h": { + "a": { + "df": 0, + "docs": {}, + "p": { + "df": 0, + "docs": {}, + "t": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "r": { + "df": 6, + "docs": { + "first/includes.html#includes": { + "tf": 1.0 + }, + "first/includes.html#summary": { + "tf": 2.0 + }, + "first/index.html#first-chapter": { + "tf": 1.4142135623730952 + }, + "first/nested.html#nested-chapter": { + "tf": 1.7320508075688773 + }, + "first/nested.html#some-section": { + "tf": 1.0 + }, + "second.html#second-chapter": { + "tf": 1.4142135623730952 + } + } + } + } + } + } + }, + "df": 0, + "docs": {} + }, + "o": { + "d": { + "df": 0, + "docs": {}, + "e": { + "df": 2, + "docs": { + "first/nested.html#nested-chapter": { + "tf": 1.0 + }, + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "df": 0, + "docs": {}, + "n": { + "c": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "u": { + "df": 0, + "docs": {}, + "s": { + "df": 2, + "docs": { + "conclusion.html#conclusion": { + "tf": 1.4142135623730952 + }, + "first/includes.html#summary": { + "tf": 1.0 + } + } + } + } + } + }, + "df": 0, + "docs": {} + } + } + }, + "d": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + }, + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "v": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "n": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + }, + "x": { + "df": 0, + "docs": {}, + "p": { + "a": { + "df": 0, + "docs": {}, + "n": { + "d": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + }, + "df": 0, + "docs": {} + } + }, + "df": 0, + "docs": {} + } + } + }, + "f": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "e": { + "df": 2, + "docs": { + "first/nested.html#nested-chapter": { + "tf": 1.0 + }, + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "r": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "t": { + "df": 5, + "docs": { + "first/includes.html#includes": { + "tf": 1.0 + }, + "first/includes.html#summary": { + "tf": 1.4142135623730952 + }, + "first/index.html#first-chapter": { + "tf": 1.4142135623730952 + }, + "first/nested.html#nested-chapter": { + "tf": 1.0 + }, + "first/nested.html#some-section": { + "tf": 1.0 + } + } + } + } + } + }, + "n": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "h": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "e": { + "'": { + "df": 1, + "docs": { + "intro.html#introduction": { + "tf": 1.0 + } + } + }, + "df": 1, + "docs": { + "conclusion.html#conclusion": { + "tf": 1.0 + } + } + } + } + }, + "i": { + "d": { + "d": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "n": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + }, + "df": 0, + "docs": {}, + "e": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "df": 0, + "docs": {} + } + }, + "i": { + "df": 0, + "docs": {}, + "n": { + "c": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "u": { + "d": { + "df": 2, + "docs": { + "first/includes.html#includes": { + "tf": 1.4142135623730952 + }, + "first/includes.html#summary": { + "tf": 1.0 + } + } + }, + "df": 0, + "docs": {} + } + } + }, + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "t": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "intro.html#introduction": { + "tf": 1.0 + } + } + } + } + } + } + }, + "r": { + "df": 0, + "docs": {}, + "o": { + "d": { + "df": 0, + "docs": {}, + "u": { + "c": { + "df": 0, + "docs": {}, + "t": { + "df": 2, + "docs": { + "first/includes.html#summary": { + "tf": 1.0 + }, + "intro.html#introduction": { + "tf": 1.4142135623730952 + } + } + } + }, + "df": 0, + "docs": {} + } + }, + "df": 0, + "docs": {} + } + } + } + } + }, + "l": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "n": { + "df": 0, + "docs": {}, + "e": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + }, + "t": { + ";": { + "df": 0, + "docs": {}, + "h": { + "df": 0, + "docs": {}, + "t": { + "df": 0, + "docs": {}, + "m": { + "df": 0, + "docs": {}, + "l": { + "&": { + "df": 0, + "docs": {}, + "g": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "conclusion.html#conclusion": { + "tf": 1.0 + } + } + } + } + }, + "df": 0, + "docs": {} + } + } + } + } + }, + "df": 0, + "docs": {} + } + }, + "m": { + "a": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "n": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "k": { + "df": 0, + "docs": {}, + "e": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + }, + "df": 0, + "docs": {}, + "o": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "e": { + "df": 1, + "docs": { + "first/index.html#first-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "n": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "t": { + "df": 2, + "docs": { + "first/includes.html#summary": { + "tf": 1.0 + }, + "first/nested.html#nested-chapter": { + "tf": 1.4142135623730952 + } + } + } + } + } + }, + "p": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "n": { + "df": 0, + "docs": {}, + "t": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "n": { + "!": { + "(": { + "\"": { + "df": 0, + "docs": {}, + "h": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "o": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + } + } + }, + "i": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "df": 0, + "docs": {} + }, + "df": 0, + "docs": {} + }, + "df": 0, + "docs": {} + } + } + } + } + } + }, + "u": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "conclusion.html#conclusion": { + "tf": 1.0 + } + } + } + } + }, + "r": { + "df": 0, + "docs": {}, + "u": { + "df": 0, + "docs": {}, + "n": { + "df": 0, + "docs": {}, + "n": { + "a": { + "b": { + "df": 0, + "docs": {}, + "l": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "df": 0, + "docs": {} + }, + "df": 0, + "docs": {} + } + }, + "s": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "s": { + "df": 0, + "docs": {}, + "e": { + "c": { + "df": 0, + "docs": {}, + "o": { + "df": 0, + "docs": {}, + "n": { + "d": { + "df": 2, + "docs": { + "first/includes.html#summary": { + "tf": 1.0 + }, + "second.html#second-chapter": { + "tf": 1.4142135623730952 + } + } + }, + "df": 0, + "docs": {} + } + }, + "t": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "o": { + "df": 0, + "docs": {}, + "n": { + "df": 2, + "docs": { + "first/index.html#some-section": { + "tf": 1.4142135623730952 + }, + "first/nested.html#some-section": { + "tf": 1.4142135623730952 + } + } + } + } + } + } + }, + "df": 0, + "docs": {}, + "e": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + }, + "n": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "p": { + "df": 0, + "docs": {}, + "p": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + } + } + } + }, + "u": { + "df": 0, + "docs": {}, + "m": { + "df": 0, + "docs": {}, + "m": { + "a": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "i": { + "df": 1, + "docs": { + "first/includes.html#summary": { + "tf": 1.4142135623730952 + } + } + } + } + }, + "df": 0, + "docs": {} + } + }, + "r": { + "df": 0, + "docs": {}, + "e": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "t": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "t": { + "a": { + "b": { + "df": 0, + "docs": {}, + "l": { + "df": 1, + "docs": { + "first/nested.html#nested-chapter": { + "tf": 1.0 + } + } + } + }, + "df": 0, + "docs": {} + }, + "df": 0, + "docs": {} + } + }, + "x": { + "df": 0, + "docs": {}, + "t": { + "df": 2, + "docs": { + "first/index.html#first-chapter": { + "tf": 1.0 + }, + "intro.html#introduction": { + "tf": 1.0 + } + } + } + } + } + }, + "w": { + "df": 0, + "docs": {}, + "o": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "l": { + "d": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + }, + "df": 0, + "docs": {} + } + } + } + } + } + }, + "title": { + "root": { + "c": { + "df": 0, + "docs": {}, + "h": { + "a": { + "df": 0, + "docs": {}, + "p": { + "df": 0, + "docs": {}, + "t": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "r": { + "df": 3, + "docs": { + "first/index.html#first-chapter": { + "tf": 1.0 + }, + "first/nested.html#nested-chapter": { + "tf": 1.0 + }, + "second.html#second-chapter": { + "tf": 1.0 + } + } + } + } + } + } + }, + "df": 0, + "docs": {} + }, + "o": { + "df": 0, + "docs": {}, + "n": { + "c": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "u": { + "df": 0, + "docs": {}, + "s": { + "df": 1, + "docs": { + "conclusion.html#conclusion": { + "tf": 1.0 + } + } + } + } + } + }, + "df": 0, + "docs": {} + } + } + }, + "df": 0, + "docs": {}, + "f": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "first/index.html#first-chapter": { + "tf": 1.0 + } + } + } + } + } + } + }, + "i": { + "df": 0, + "docs": {}, + "n": { + "c": { + "df": 0, + "docs": {}, + "l": { + "df": 0, + "docs": {}, + "u": { + "d": { + "df": 1, + "docs": { + "first/includes.html#includes": { + "tf": 1.0 + } + } + }, + "df": 0, + "docs": {} + } + } + }, + "df": 0, + "docs": {}, + "t": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "o": { + "d": { + "df": 0, + "docs": {}, + "u": { + "c": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "intro.html#introduction": { + "tf": 1.0 + } + } + } + }, + "df": 0, + "docs": {} + } + }, + "df": 0, + "docs": {} + } + } + } + } + }, + "n": { + "df": 0, + "docs": {}, + "e": { + "df": 0, + "docs": {}, + "s": { + "df": 0, + "docs": {}, + "t": { + "df": 1, + "docs": { + "first/nested.html#nested-chapter": { + "tf": 1.0 + } + } + } + } + } + }, + "s": { + "df": 0, + "docs": {}, + "e": { + "c": { + "df": 0, + "docs": {}, + "o": { + "df": 0, + "docs": {}, + "n": { + "d": { + "df": 1, + "docs": { + "second.html#second-chapter": { + "tf": 1.0 + } + } + }, + "df": 0, + "docs": {} + } + }, + "t": { + "df": 0, + "docs": {}, + "i": { + "df": 0, + "docs": {}, + "o": { + "df": 0, + "docs": {}, + "n": { + "df": 2, + "docs": { + "first/index.html#some-section": { + "tf": 1.0 + }, + "first/nested.html#some-section": { + "tf": 1.0 + } + } + } + } + } + } + }, + "df": 0, + "docs": {} + }, + "u": { + "df": 0, + "docs": {}, + "m": { + "df": 0, + "docs": {}, + "m": { + "a": { + "df": 0, + "docs": {}, + "r": { + "df": 0, + "docs": {}, + "i": { + "df": 1, + "docs": { + "first/includes.html#summary": { + "tf": 1.0 + } + } + } + } + }, + "df": 0, + "docs": {} + } + } + } + } + } + } + }, + "pipeline": [ + "trimmer", + "stopWordFilter", + "stemmer" + ], + "ref": "id", + "version": "0.9.5" + }, + "searchoptions": { + "bool": "OR", + "expand": true, + "fields": { + "body": { + "boost": 1 + }, + "breadcrumbs": { + "boost": 1 + }, + "title": { + "boost": 2 + } + }, + "limit_results": 30, + "teaser_word_count": 30 + } +} \ No newline at end of file