mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 16:12:33 -05:00
Compare commits
42 Commits
v0.1.2
...
fix-travis
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe3f2ee4b1 | ||
|
|
9784d7a23b | ||
|
|
38c06f3c39 | ||
|
|
55f7ed1c37 | ||
|
|
eb0f7179ab | ||
|
|
5fb3675151 | ||
|
|
77b4f6a940 | ||
|
|
6308da699a | ||
|
|
62a727c041 | ||
|
|
3bc5d907f4 | ||
|
|
3cd12e7092 | ||
|
|
c8bbfd4bc1 | ||
|
|
d48a27f94f | ||
|
|
8c456666ff | ||
|
|
48b0f547c5 | ||
|
|
867fbfec05 | ||
|
|
951c873df6 | ||
|
|
4af155e963 | ||
|
|
07719a8e0e | ||
|
|
cc92d665ca | ||
|
|
b86533b2a1 | ||
|
|
b2ad669c61 | ||
|
|
bb043ef660 | ||
|
|
82aef1bc3f | ||
|
|
38c883e1ef | ||
|
|
8a00a004d8 | ||
|
|
6af77a7792 | ||
|
|
b5ca820345 | ||
|
|
b765023da3 | ||
|
|
d306aed587 | ||
|
|
89a5dbaf9a | ||
|
|
6961247f56 | ||
|
|
07551760c9 | ||
|
|
990daceed5 | ||
|
|
2989096188 | ||
|
|
03c6c44e5b | ||
|
|
31a370d149 | ||
|
|
0bc1030a02 | ||
|
|
43fcd00cd5 | ||
|
|
3d83b784b3 | ||
|
|
5d42738a79 | ||
|
|
1f4dab3e5c |
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -2,3 +2,7 @@
|
||||
|
||||
* text=auto eol=lf
|
||||
*.rs rust
|
||||
*.woff -text
|
||||
*.ttf -text
|
||||
*.otf -text
|
||||
*.png -text
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
Cargo.lock
|
||||
target
|
||||
|
||||
# MacOS temp file
|
||||
|
||||
75
.travis.yml
75
.travis.yml
@@ -1,87 +1,46 @@
|
||||
# Based on the "trust" template v0.1.1
|
||||
# https://github.com/japaric/trust/tree/v0.1.1
|
||||
|
||||
dist: trusty
|
||||
language: rust
|
||||
services: docker
|
||||
sudo: required
|
||||
|
||||
cache: cargo
|
||||
cache:
|
||||
- cargo
|
||||
|
||||
before_cache:
|
||||
- chmod -R a+r $HOME/.cargo
|
||||
- chmod -R a+r $HOME/.cargo
|
||||
|
||||
env:
|
||||
global:
|
||||
- CRATE_NAME=mdbook
|
||||
- secure: DPzSRXyfRIVTibv1wOKFeGekXlL8sumGEZxpeq911MpLlrndOKmOo5Ibi3JD8fbUOsE9A/5spj4B2KQNjhbplH+Cp26oEikjuNAA6cA/b2+/TMoC3i0klAYpVopBBV3FFna0gLP+q6t6fzG2v9TJrvmmVav6KVX6ylPNvD/LoReCjrkpgLIQuAQ6dSQNor9uV+EVt4plKhhkiS28DlYdgmTvNb5g4dzOhs8hoWty72J765VYWEDDC8qXn6N9GyrhsC3dhjASGn+1QDSCADYdbG9nrRlb4CZhrfcgOnHhAFva363kshg9HtCphigMgQy2oZXk4nLWK90/HuaPPkVj+N/lpIYjtiHOunToZJfIb0MWzyVI+7+I7WR6n6XbhLCPMe/sPXHHQ3HhQhZZ9xv7CDx9IkYJQBcF3LC+9kzJRi4QT0UTqrxcO3ncgXwvholP8Vg2KKPqFcbuyLPzbvr/o8zIilvLUFAEoDPfTEwSAC4BCzaGkFQVWzhWkgw8Pe1ckOEYFkZ0VLBuCpEiz+x45sbBL1SnnO5xhpjmdc572ZyW7ZmAABw1VfiWhhBWg4WGSf8lLnDHhNA36Qon34pnME/xpJQtWoo7ZZkkzvzYP/oW88/0UIMWDSOYKz7MijXlbNUggwAwUhrLzXDuB71HUKfPreFubfUxbOpu+OtTcOQ=
|
||||
|
||||
matrix:
|
||||
include:
|
||||
# Android
|
||||
- env: TARGET=arm-linux-androideabi DISABLE_TESTS=1
|
||||
|
||||
# Linux
|
||||
- env: TARGET=aarch64-unknown-linux-gnu
|
||||
- env: TARGET=arm-unknown-linux-gnueabi
|
||||
- env: TARGET=i686-unknown-linux-gnu
|
||||
- env: TARGET=x86_64-unknown-linux-gnu
|
||||
- env: TARGET=x86_64-unknown-linux-musl
|
||||
|
||||
# Mac
|
||||
- env: TARGET=i686-apple-darwin
|
||||
os: osx
|
||||
- env: TARGET=x86_64-apple-darwin
|
||||
os: osx
|
||||
|
||||
# BSD
|
||||
- env: TARGET=i686-unknown-freebsd DISABLE_TESTS=1
|
||||
- env: TARGET=x86_64-unknown-freebsd DISABLE_TESTS=1
|
||||
- env: TARGET=x86_64-unknown-netbsd DISABLE_TESTS=1
|
||||
|
||||
# Other channels
|
||||
- env: TARGET=x86_64-unknown-linux-gnu
|
||||
rust: beta
|
||||
- env: TARGET=x86_64-apple-darwin
|
||||
os: osx
|
||||
rust: beta
|
||||
- env: TARGET=x86_64-unknown-linux-gnu
|
||||
rust: nightly
|
||||
- env: TARGET=x86_64-apple-darwin
|
||||
os: osx
|
||||
rust: nightly
|
||||
|
||||
before_install:
|
||||
- set -e
|
||||
- rustup self update
|
||||
- CRATE_NAME=mdbook
|
||||
- TARGET=x86_64-unknown-linux-gnu
|
||||
|
||||
install:
|
||||
- sh ci/install.sh
|
||||
- source ~/.cargo/env || true
|
||||
- sh ci/install.sh
|
||||
- export PATH=$PATH:$HOME/.cargo/bin
|
||||
|
||||
script:
|
||||
- bash ci/script.sh
|
||||
- cargo build --all --no-default-features
|
||||
- cargo build --verbose
|
||||
- cargo test --verbose
|
||||
|
||||
after_success:
|
||||
- bash ci/github_pages.sh
|
||||
- bash ci/github_pages.sh
|
||||
|
||||
before_deploy:
|
||||
- sh ci/before_deploy.sh
|
||||
- sh ci/before_deploy.sh
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
- secure: cURRWBr034iqBz/ifD7uOunBfNR30YxIXfgLX0osWz+iafkVbhDGYYz9sBmRraqO2P7L2koEXMADVb/md1kI2+ykiq/ml+l9zuEAZPVmvSGUN7ZD+7s+lu3l5OBPG5z175T+b2q2q2m8XVR7TW20ra4QbE0bq06KAoOyjSgQVBTSCYsL9uTsGwiVRMEqqJT/BmKhKJNkpGsTKyBSKkOXvfeAAbE260vXUDEN9TYdJ3fvteRrpwLX56ee64gIZUq0RjDc4SKIEqilM6iUtNMvurqaewYNGkiXKRruV6BPCHxEHo6NNT46kOJLBJTf7gZw//dWhSoWpg9P0gdAnPWm407kSa3F7aJ1eRShAFQ4BLyfz9efTqm+jP3fOp7Mm7igSh9w6caSRuOnSsUf5+raRQ8E5Y9HsWGzzpZQk24Fx9EGZ04EeDSdpZAFz+jcbMpHf8t2p4CEx0CCNwYvKx6EydMKbMF5QteQ8SQkXNLhv7Rz2OgtXWYZPRVCMfQfOplsi2InsLCrQxTgwh+6u654SqVSgaHG+IncEAxBrdWy4rHcg7qereUcKfcY3k96vaDxdn/T2c00Ig0aNFR91YnixGMd6J6tQgDcRK9jh6fUm1CCBE9hT+pNUmtgYKuWBoLZexUZFFnfuBed0WciBot1bGDDamndqKq0jJiAzg+GMHk=
|
||||
- secure: cURRWBr034iqBz/ifD7uOunBfNR30YxIXfgLX0osWz+iafkVbhDGYYz9sBmRraqO2P7L2koEXMADVb/md1kI2+ykiq/ml+l9zuEAZPVmvSGUN7ZD+7s+lu3l5OBPG5z175T+b2q2q2m8XVR7TW20ra4QbE0bq06KAoOyjSgQVBTSCYsL9uTsGwiVRMEqqJT/BmKhKJNkpGsTKyBSKkOXvfeAAbE260vXUDEN9TYdJ3fvteRrpwLX56ee64gIZUq0RjDc4SKIEqilM6iUtNMvurqaewYNGkiXKRruV6BPCHxEHo6NNT46kOJLBJTf7gZw//dWhSoWpg9P0gdAnPWm407kSa3F7aJ1eRShAFQ4BLyfz9efTqm+jP3fOp7Mm7igSh9w6caSRuOnSsUf5+raRQ8E5Y9HsWGzzpZQk24Fx9EGZ04EeDSdpZAFz+jcbMpHf8t2p4CEx0CCNwYvKx6EydMKbMF5QteQ8SQkXNLhv7Rz2OgtXWYZPRVCMfQfOplsi2InsLCrQxTgwh+6u654SqVSgaHG+IncEAxBrdWy4rHcg7qereUcKfcY3k96vaDxdn/T2c00Ig0aNFR91YnixGMd6J6tQgDcRK9jh6fUm1CCBE9hT+pNUmtgYKuWBoLZexUZFFnfuBed0WciBot1bGDDamndqKq0jJiAzg+GMHk=
|
||||
file_glob: true
|
||||
file: "$CRATE_NAME-$TRAVIS_TAG-$TARGET.*"
|
||||
on:
|
||||
condition: "$TRAVIS_RUST_VERSION = stable"
|
||||
tags: true
|
||||
provider: releases
|
||||
skip_cleanup: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- "/^v\\d+\\.\\d+\\.\\d+.*$/"
|
||||
- master
|
||||
- "/^v\\d+\\.\\d+\\.\\d+.*$/"
|
||||
- master
|
||||
|
||||
notifications:
|
||||
email:
|
||||
|
||||
1541
Cargo.lock
generated
Normal file
1541
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.1.2"
|
||||
version = "0.1.6"
|
||||
authors = ["Mathieu David <mathieudavid@mathieudavid.org>", "Michael-F-Bryan <michaelfbryan@gmail.com>"]
|
||||
description = "create books from markdown files (like Gitbook)"
|
||||
description = "Create books from markdown files"
|
||||
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
|
||||
repository = "https://github.com/rust-lang-nursery/mdBook"
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
@@ -16,18 +16,18 @@ exclude = [
|
||||
|
||||
[package.metadata.release]
|
||||
sign-commit = true
|
||||
push-remote = "upstream"
|
||||
push-remote = "origin"
|
||||
tag-prefix = "v"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.24"
|
||||
chrono = "0.4"
|
||||
handlebars = "0.29"
|
||||
handlebars = "0.32"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
error-chain = "0.11"
|
||||
serde_json = "1.0"
|
||||
pulldown-cmark = "0.1"
|
||||
pulldown-cmark = "0.1.2"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.5.0-rc.1"
|
||||
@@ -35,7 +35,7 @@ toml = "0.4"
|
||||
memchr = "2.0"
|
||||
open = "1.1"
|
||||
regex = "0.2.1"
|
||||
tempdir = "0.3.4"
|
||||
tempfile = "3.0"
|
||||
itertools = "0.7"
|
||||
shlex = "0.1"
|
||||
toml-query = "0.6"
|
||||
@@ -46,25 +46,31 @@ time = { version = "0.1.34", optional = true }
|
||||
crossbeam = { version = "0.3", optional = true }
|
||||
|
||||
# Serve feature
|
||||
iron = { version = "0.5", optional = true }
|
||||
staticfile = { version = "0.4", optional = true }
|
||||
iron = { version = "0.6", optional = true }
|
||||
staticfile = { version = "0.5", optional = true }
|
||||
ws = { version = "0.7", optional = true}
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "2.0", optional = true }
|
||||
ammonia = { version = "1.1", optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
error-chain = "0.11"
|
||||
|
||||
[dev-dependencies]
|
||||
select = "0.4"
|
||||
pretty_assertions = "0.4"
|
||||
pretty_assertions = "0.5"
|
||||
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
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<tr>
|
||||
<td><strong>Windows</strong></td>
|
||||
<td>
|
||||
<a href="https://ci.appveyor.com/project/azerupi/mdbook/"><img src="https://ci.appveyor.com/api/projects/status/o38racsnbcospyc8/branch/master?svg=true"></a>
|
||||
<a href="https://ci.appveyor.com/project/rust-lang-libs/mdbook"><img src="https://ci.appveyor.com/api/projects/status/ysyke2rvo85sni55?svg=true"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -40,7 +40,7 @@ There are multiple ways to install mdBook.
|
||||
path to the binary into your `PATH`.
|
||||
|
||||
2. **From Crates.io**
|
||||
j
|
||||
|
||||
This requires [Rust] and Cargo to be installed. Once you have installed
|
||||
Rust, type the following in the terminal:
|
||||
|
||||
@@ -141,6 +141,10 @@ explanation, check out the [User Guide].
|
||||
`http://localhost:3000` (port is changeable) and reloads the browser when a
|
||||
change occurs.
|
||||
|
||||
- `mdbook clean`
|
||||
|
||||
Delete directory in which generated book is located.
|
||||
|
||||
|
||||
### As a library
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
- [watch](cli/watch.md)
|
||||
- [serve](cli/serve.md)
|
||||
- [test](cli/test.md)
|
||||
- [clean](cli/clean.md)
|
||||
- [Format](format/format.md)
|
||||
- [SUMMARY.md](format/summary.md)
|
||||
- [Configuration](format/config.md)
|
||||
|
||||
@@ -14,8 +14,8 @@ convenience. Large books will therefore remain structured when rendered.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
Like `init`, the `build` command can take a directory as argument to use instead of the
|
||||
current working directory.
|
||||
Like `init`, the `build` command can take a directory as an argument to use
|
||||
instead of the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook build path/to/book
|
||||
|
||||
21
book-example/src/cli/clean.md
Normal file
21
book-example/src/cli/clean.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# The clean command
|
||||
|
||||
The clean command is used to delete the generated book and any other build
|
||||
artifacts.
|
||||
|
||||
```bash
|
||||
mdbook clean
|
||||
```
|
||||
|
||||
It will try to delete the built book. If a path is provided, it will be used.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
Like `init`, the `clean` command can take a directory as an argument to use
|
||||
instead of the normal build directory.
|
||||
|
||||
```bash
|
||||
mdbook clean --dest-dir=path/to/book
|
||||
```
|
||||
|
||||
`path/to/book` could be absolute or relative.
|
||||
@@ -6,8 +6,8 @@ This preferred by many for writing books with mdbook because it allows for you t
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
Like `watch`, `serve` can take a directory as argument to use instead of the
|
||||
current working directory.
|
||||
Like `watch`, `serve` can take a directory as an argument to use instead of
|
||||
the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook serve path/to/book
|
||||
|
||||
@@ -5,8 +5,8 @@ You could repeatedly issue `mdbook build` every time a file is changed. But usin
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
Like `init` and `build`, `watch` can take a directory as argument to use instead of the
|
||||
current working directory.
|
||||
Like `init` and `build`, `watch` can take a directory as an argument to use
|
||||
instead of the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook watch path/to/book
|
||||
|
||||
@@ -5,13 +5,13 @@ book is loaded and before it gets rendered, allowing you to update and mutate
|
||||
the book. Possible use cases are:
|
||||
|
||||
- Creating custom helpers like `\{{#include /path/to/file.md}}`
|
||||
- Updating links so `[some chapter](some_chapter.md)` is automatically changed
|
||||
- Updating links so `[some chapter](some_chapter.md)` is automatically changed
|
||||
to `[some chapter](some_chapter.html)` for the HTML renderer
|
||||
- Substituting in latex-style expressions (`$$ \frac{1}{3} $$`) with their
|
||||
- Substituting in latex-style expressions (`$$ \frac{1}{3} $$`) with their
|
||||
mathjax equivalents
|
||||
|
||||
|
||||
## Implementing a Preprocessor
|
||||
## Implementing a Preprocessor
|
||||
|
||||
A preprocessor is represented by the `Preprocessor` trait.
|
||||
|
||||
@@ -29,4 +29,68 @@ pub struct PreprocessorContext {
|
||||
pub root: PathBuf,
|
||||
pub config: Config,
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## A complete Example
|
||||
|
||||
The magic happens within the `run(...)` method of the [`Preprocessor`][preprocessor-docs] trait implementation.
|
||||
|
||||
As direct access to the chapters is not possible, you will probably end up iterating
|
||||
them using `for_each_mut(...)`:
|
||||
|
||||
```rust
|
||||
book.for_each_mut(|item: &mut BookItem| {
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
|
||||
res = Some(
|
||||
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
|
||||
Ok(md) => {
|
||||
chapter.content = md;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The `chapter.content` is just a markdown formatted string, and you will have to
|
||||
process it in some way. Even though it's entirely possible to implement some sort of
|
||||
manual find & replace operation, if that feels too unsafe you can use [`pulldown-cmark`][pc]
|
||||
to parse the string into events and work on them instead.
|
||||
|
||||
Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events back to
|
||||
a string.
|
||||
|
||||
The following code block shows how to remove all emphasis from markdown, and do so
|
||||
safely.
|
||||
|
||||
```rust
|
||||
fn remove_emphasis(num_removed_items: &mut i32, chapter: &mut Chapter) -> Result<String> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
let events = Parser::new(&chapter.content).filter(|e| {
|
||||
let should_keep = match *e {
|
||||
Event::Start(Tag::Emphasis)
|
||||
| Event::Start(Tag::Strong)
|
||||
| Event::End(Tag::Emphasis)
|
||||
| Event::End(Tag::Strong) => false,
|
||||
_ => true,
|
||||
};
|
||||
if !should_keep {
|
||||
*num_removed_items += 1;
|
||||
}
|
||||
should_keep
|
||||
});
|
||||
cmark(events, &mut buf, None)
|
||||
.map(|_| buf)
|
||||
.map_err(|err| Error::from(format!("Markdown serialization failed: {}", err)))
|
||||
}
|
||||
```
|
||||
|
||||
For everything else, have a look [at the complete example][example].
|
||||
|
||||
[preprocessor-docs]: https://docs.rs/mdbook/0.1.3/mdbook/preprocess/trait.Preprocessor.html
|
||||
[pc]: https://crates.io/crates/pulldown-cmark
|
||||
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
|
||||
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs
|
||||
|
||||
@@ -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,12 +142,24 @@ 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
|
||||
```
|
||||
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All configuration values van be overridden from the command line by setting the
|
||||
All configuration values can be overridden from the command line by setting the
|
||||
corresponding environment variable. Because many operating systems restrict
|
||||
environment variables to be alphanumeric characters or `_`, the configuration
|
||||
key needs to be formatted slightly differently to the normal `foo.bar.baz` form.
|
||||
@@ -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.
|
||||
`book.toml` before building.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# mdBook specific markdown
|
||||
# mdBook-specific markdown
|
||||
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that let's you hide code lines by prepending them with a `#`.
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them with a `#`.
|
||||
|
||||
```bash
|
||||
# fn main() {
|
||||
@@ -55,7 +55,7 @@ With the following syntax, you can insert runnable Rust files into your book:
|
||||
|
||||
The path to the Rust file has to be relative from the current source file.
|
||||
|
||||
When play is clicked, the code snippet will be send to the [Rust Playpen] to be compiled and run. The result is send back and displayed directly underneath the code.
|
||||
When play is clicked, the code snippet will be sent to the [Rust Playpen] to be compiled and run. The result is sent back and displayed directly underneath the code.
|
||||
|
||||
Here is what a rendered code snippet looks like:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -15,11 +15,9 @@ main() {
|
||||
;;
|
||||
esac
|
||||
|
||||
test -f Cargo.lock || cargo generate-lockfile
|
||||
cargo rustc --bin mdbook --target $TARGET --release -- -C lto
|
||||
|
||||
cross rustc --bin mdbook --target $TARGET --release -- -C lto
|
||||
|
||||
cp target/$TARGET/release/mdbook $stage/
|
||||
cp target/release/mdbook $stage/
|
||||
|
||||
cd $stage
|
||||
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
|
||||
@@ -28,4 +26,4 @@ main() {
|
||||
rm -rf $stage
|
||||
}
|
||||
|
||||
main
|
||||
main
|
||||
|
||||
20
ci/script.sh
20
ci/script.sh
@@ -1,20 +0,0 @@
|
||||
# This script takes care of testing your crate
|
||||
|
||||
set -ex
|
||||
|
||||
main() {
|
||||
cross build --target $TARGET --all
|
||||
cross build --target $TARGET --all --release
|
||||
|
||||
if [ ! -z $DISABLE_TESTS ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
cross test --target $TARGET
|
||||
cross test --target $TARGET --release
|
||||
}
|
||||
|
||||
# we don't run the "test phase" when doing deploys
|
||||
if [ -z $TRAVIS_TAG ]; then
|
||||
main
|
||||
fi
|
||||
94
examples/de-emphasize.rs
Normal file
94
examples/de-emphasize.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
//! This program removes all forms of emphasis from the markdown of the book.
|
||||
extern crate mdbook;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate pulldown_cmark_to_cmark;
|
||||
|
||||
use mdbook::errors::{Error, Result};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::book::{Book, BookItem, Chapter};
|
||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
||||
use pulldown_cmark::{Event, Parser, Tag};
|
||||
use pulldown_cmark_to_cmark::fmt::cmark;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::env::{args, args_os};
|
||||
use std::process;
|
||||
|
||||
struct Deemphasize;
|
||||
|
||||
impl Preprocessor for Deemphasize {
|
||||
fn name(&self) -> &str {
|
||||
"md-links-to-html-links"
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
|
||||
eprintln!("Running '{}' preprocessor", self.name());
|
||||
let mut res: Option<_> = None;
|
||||
let mut num_removed_items = 0;
|
||||
book.for_each_mut(|item: &mut BookItem| {
|
||||
if let Some(Err(_)) = res {
|
||||
return;
|
||||
}
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
|
||||
res = Some(
|
||||
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
|
||||
Ok(md) => {
|
||||
chapter.content = md;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
eprintln!(
|
||||
"{}: removed {} events from markdown stream.",
|
||||
self.name(),
|
||||
num_removed_items
|
||||
);
|
||||
match res {
|
||||
Some(res) => res,
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn do_it(book: OsString) -> Result<()> {
|
||||
let mut book = MDBook::load(book)?;
|
||||
book.with_preprecessor(Deemphasize);
|
||||
book.build()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if args_os().count() != 2 {
|
||||
eprintln!("USAGE: {} <book>", args().next().expect("executable"));
|
||||
return;
|
||||
}
|
||||
if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl Deemphasize {
|
||||
fn remove_emphasis(num_removed_items: &mut i32, chapter: &mut Chapter) -> Result<String> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
let events = Parser::new(&chapter.content).filter(|e| {
|
||||
let should_keep = match *e {
|
||||
Event::Start(Tag::Emphasis)
|
||||
| Event::Start(Tag::Strong)
|
||||
| Event::End(Tag::Emphasis)
|
||||
| Event::End(Tag::Strong) => false,
|
||||
_ => true,
|
||||
};
|
||||
if !should_keep {
|
||||
*num_removed_items += 1;
|
||||
}
|
||||
should_keep
|
||||
});
|
||||
cmark(events, &mut buf, None)
|
||||
.map(|_| buf)
|
||||
.map_err(|err| Error::from(format!("Markdown serialization failed: {}", err)))
|
||||
}
|
||||
}
|
||||
30
src/bin/clean.rs
Normal file
30
src/bin/clean.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::*;
|
||||
use get_book_dir;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("clean")
|
||||
.about("Delete built book")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'The directory of built book{n}(Defaults to ./book when \
|
||||
omitted)'",
|
||||
)
|
||||
}
|
||||
|
||||
// Clean command implementation
|
||||
pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::load(&book_dir)?;
|
||||
|
||||
let dir_to_remove = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => PathBuf::from(dest_dir),
|
||||
None => book.root.join(&book.config.build.build_dir),
|
||||
};
|
||||
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::utils;
|
||||
use mdbook::config;
|
||||
use get_book_dir;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
@@ -20,9 +23,11 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut builder = MDBook::init(&book_dir);
|
||||
let mut config = config::Config::default();
|
||||
|
||||
// If flag `--theme` is present, copy theme to src
|
||||
if args.is_present("theme") {
|
||||
config.set("output.html.theme", "src/theme")?;
|
||||
// Skip this if `--force` is present
|
||||
if !args.is_present("force") {
|
||||
// Print warning
|
||||
@@ -38,6 +43,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
if confirm() {
|
||||
builder.copy_theme(true);
|
||||
}
|
||||
} else {
|
||||
builder.copy_theme(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +54,49 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
builder.create_gitignore(true);
|
||||
}
|
||||
|
||||
config.book.title = request_book_title();
|
||||
|
||||
if let Some(author) = get_author_name() {
|
||||
debug!("Obtained user name from gitconfig: {:?}", author);
|
||||
config.book.authors.push(author);
|
||||
builder.with_config(config);
|
||||
}
|
||||
|
||||
builder.build()?;
|
||||
println!("\nAll done, no errors...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Simple function that user comfirmation
|
||||
/// Obtains author name from git config file by running the `git config` command.
|
||||
fn get_author_name() -> Option<String> {
|
||||
let output = Command::new("git")
|
||||
.args(&["config", "--get", "user.name"])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if output.status.success() {
|
||||
Some(String::from_utf8_lossy(&output.stdout).trim().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Request book title from user and return if provided.
|
||||
fn request_book_title() -> Option<String> {
|
||||
println!("What title would you like to give the book? ");
|
||||
io::stdout().flush().unwrap();
|
||||
let mut resp = String::new();
|
||||
io::stdin().read_line(&mut resp).unwrap();
|
||||
let resp = resp.trim();
|
||||
if resp.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(resp.into())
|
||||
}
|
||||
}
|
||||
|
||||
// Simple function for user confirmation
|
||||
fn confirm() -> bool {
|
||||
io::stdout().flush().unwrap();
|
||||
let mut s = String::new();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
extern crate chrono;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate chrono;
|
||||
extern crate env_logger;
|
||||
extern crate error_chain;
|
||||
#[macro_use]
|
||||
@@ -19,6 +19,7 @@ use env_logger::Builder;
|
||||
use mdbook::utils;
|
||||
|
||||
pub mod build;
|
||||
pub mod clean;
|
||||
pub mod init;
|
||||
pub mod test;
|
||||
#[cfg(feature = "serve")]
|
||||
@@ -37,14 +38,15 @@ fn main() {
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
// Get the version from our Cargo.toml using clap's crate_version!() macro
|
||||
.version(concat!("v",crate_version!()))
|
||||
.setting(AppSettings::SubcommandRequired)
|
||||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.after_help("For more information about a specific command, \
|
||||
try `mdbook <command> --help`\n\
|
||||
Source code for mdbook available \
|
||||
at: https://github.com/rust-lang-nursery/mdBook")
|
||||
.subcommand(init::make_subcommand())
|
||||
.subcommand(build::make_subcommand())
|
||||
.subcommand(test::make_subcommand());
|
||||
.subcommand(test::make_subcommand())
|
||||
.subcommand(clean::make_subcommand());
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
let app = app.subcommand(watch::make_subcommand());
|
||||
@@ -55,6 +57,7 @@ fn main() {
|
||||
let res = match app.get_matches().subcommand() {
|
||||
("init", Some(sub_matches)) => init::execute(sub_matches),
|
||||
("build", Some(sub_matches)) => build::execute(sub_matches),
|
||||
("clean", Some(sub_matches)) => clean::execute(sub_matches),
|
||||
#[cfg(feature = "watch")]
|
||||
("watch", Some(sub_matches)) => watch::execute(sub_matches),
|
||||
#[cfg(feature = "serve")]
|
||||
@@ -74,11 +77,14 @@ fn init_logger() {
|
||||
let mut builder = Builder::new();
|
||||
|
||||
builder.format(|formatter, record| {
|
||||
writeln!(formatter, "{} [{}] ({}): {}",
|
||||
Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
record.level(),
|
||||
record.target(),
|
||||
record.args())
|
||||
writeln!(
|
||||
formatter,
|
||||
"{} [{}] ({}): {}",
|
||||
Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
record.level(),
|
||||
record.target(),
|
||||
record.args()
|
||||
)
|
||||
});
|
||||
|
||||
if let Ok(var) = env::var("RUST_LOG") {
|
||||
@@ -86,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();
|
||||
|
||||
@@ -71,7 +71,8 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Book {
|
||||
/// The sections in this book.
|
||||
sections: Vec<BookItem>,
|
||||
pub sections: Vec<BookItem>,
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
impl Book {
|
||||
@@ -152,15 +153,23 @@ pub struct Chapter {
|
||||
pub sub_items: Vec<BookItem>,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
pub path: PathBuf,
|
||||
/// An ordered list of the names of each chapter above this one, in the hierarchy.
|
||||
pub parent_names: Vec<String>,
|
||||
}
|
||||
|
||||
impl Chapter {
|
||||
/// Create a new chapter with the provided content.
|
||||
pub fn new<P: Into<PathBuf>>(name: &str, content: String, path: P) -> Chapter {
|
||||
pub fn new<P: Into<PathBuf>>(
|
||||
name: &str,
|
||||
content: String,
|
||||
path: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Chapter {
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content: content,
|
||||
path: path.into(),
|
||||
parent_names: parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -183,21 +192,34 @@ fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<
|
||||
let mut chapters = Vec::new();
|
||||
|
||||
for summary_item in summary_items {
|
||||
let chapter = load_summary_item(summary_item, src_dir)?;
|
||||
let chapter = load_summary_item(summary_item, src_dir, Vec::new())?;
|
||||
chapters.push(chapter);
|
||||
}
|
||||
|
||||
Ok(Book { sections: chapters })
|
||||
Ok(Book {
|
||||
sections: chapters,
|
||||
__non_exhaustive: (),
|
||||
})
|
||||
}
|
||||
|
||||
fn load_summary_item<P: AsRef<Path>>(item: &SummaryItem, src_dir: P) -> Result<BookItem> {
|
||||
fn load_summary_item<P: AsRef<Path>>(
|
||||
item: &SummaryItem,
|
||||
src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Result<BookItem> {
|
||||
match *item {
|
||||
SummaryItem::Separator => Ok(BookItem::Separator),
|
||||
SummaryItem::Link(ref link) => load_chapter(link, src_dir).map(|c| BookItem::Chapter(c)),
|
||||
SummaryItem::Link(ref link) => {
|
||||
load_chapter(link, src_dir, parent_names).map(|c| BookItem::Chapter(c))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_chapter<P: AsRef<Path>>(link: &Link, src_dir: P) -> Result<Chapter> {
|
||||
fn load_chapter<P: AsRef<Path>>(
|
||||
link: &Link,
|
||||
src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Result<Chapter> {
|
||||
debug!("Loading {} ({})", link.name, link.location.display());
|
||||
let src_dir = src_dir.as_ref();
|
||||
|
||||
@@ -218,12 +240,14 @@ fn load_chapter<P: AsRef<Path>>(link: &Link, src_dir: P) -> Result<Chapter> {
|
||||
.strip_prefix(&src_dir)
|
||||
.expect("Chapters are always inside a book");
|
||||
|
||||
let mut ch = Chapter::new(&link.name, content, stripped);
|
||||
let mut sub_item_parents = parent_names.clone();
|
||||
let mut ch = Chapter::new(&link.name, content, stripped, parent_names);
|
||||
ch.number = link.number.clone();
|
||||
|
||||
sub_item_parents.push(link.name.clone());
|
||||
let sub_items = link.nested_items
|
||||
.iter()
|
||||
.map(|i| load_summary_item(i, src_dir))
|
||||
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
ch.sub_items = sub_items;
|
||||
@@ -273,7 +297,7 @@ impl Display for Chapter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempdir::TempDir;
|
||||
use tempfile::{TempDir, Builder as TempFileBuilder};
|
||||
use std::io::Write;
|
||||
|
||||
const DUMMY_SRC: &'static str = "
|
||||
@@ -287,7 +311,7 @@ And here is some \
|
||||
|
||||
/// Create a dummy `Link` in a temporary directory.
|
||||
fn dummy_link() -> (Link, TempDir) {
|
||||
let temp = TempDir::new("book").unwrap();
|
||||
let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap();
|
||||
|
||||
let chapter_path = temp.path().join("chapter_1.md");
|
||||
File::create(&chapter_path)
|
||||
@@ -324,9 +348,14 @@ 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 +363,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 +376,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 +384,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 +392,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);
|
||||
}
|
||||
|
||||
@@ -381,6 +412,7 @@ And here is some \
|
||||
..Default::default()
|
||||
}),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got = load_book_from_disk(&summary, temp.path()).unwrap();
|
||||
@@ -399,6 +431,7 @@ And here is some \
|
||||
}),
|
||||
BookItem::Separator,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let should_be: Vec<_> = book.sections.iter().collect();
|
||||
@@ -417,22 +450,26 @@ 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(),
|
||||
)),
|
||||
],
|
||||
}),
|
||||
BookItem::Separator,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got: Vec<_> = book.iter().collect();
|
||||
@@ -464,22 +501,26 @@ 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(),
|
||||
)),
|
||||
],
|
||||
}),
|
||||
BookItem::Separator,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let num_items = book.iter().count();
|
||||
|
||||
@@ -16,7 +16,7 @@ pub use self::init::BookBuilder;
|
||||
use std::path::PathBuf;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use tempdir::TempDir;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use toml::Value;
|
||||
|
||||
use utils;
|
||||
@@ -213,7 +213,7 @@ impl MDBook {
|
||||
.flat_map(|x| vec![x.0, x.1])
|
||||
.collect();
|
||||
|
||||
let temp_dir = TempDir::new("mdbook")?;
|
||||
let temp_dir = TempFileBuilder::new().prefix("mdbook").tempdir()?;
|
||||
|
||||
let preprocess_context = PreprocessorContext::new(self.root.clone(), self.config.clone());
|
||||
|
||||
@@ -288,13 +288,12 @@ impl MDBook {
|
||||
self.root.join(&self.config.book.src)
|
||||
}
|
||||
|
||||
// FIXME: This really belongs as part of the `HtmlConfig`.
|
||||
#[doc(hidden)]
|
||||
/// Get the directory containing the theme resources for the book.
|
||||
pub fn theme_dir(&self) -> PathBuf {
|
||||
match self.config.html_config().and_then(|h| h.theme) {
|
||||
Some(d) => self.root.join(d),
|
||||
None => self.root.join("theme"),
|
||||
}
|
||||
self.config
|
||||
.html_config()
|
||||
.unwrap_or_default()
|
||||
.theme_dir(&self.root)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::iter::FromIterator;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
use memchr::{self, Memchr};
|
||||
use pulldown_cmark::{self, Alignment, Event, Tag};
|
||||
use pulldown_cmark::{self, Event, Tag};
|
||||
use errors::*;
|
||||
|
||||
/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be
|
||||
@@ -313,24 +313,15 @@ impl<'a> SummaryParser<'a> {
|
||||
break;
|
||||
}
|
||||
Some(Event::Start(other_tag)) => {
|
||||
// FIXME: Remove this when google/pulldown_cmark#120 lands (new patch release)
|
||||
// replace with `other_tag == Tag::Rule`
|
||||
if tag_eq(&other_tag, &Tag::Rule) {
|
||||
if other_tag == Tag::Rule {
|
||||
items.push(SummaryItem::Separator);
|
||||
}
|
||||
trace!("Skipping contents of {:?}", other_tag);
|
||||
|
||||
// Skip over the contents of this tag
|
||||
while let Some(event) = self.next_event() {
|
||||
// FIXME: Remove this when google/pulldown_cmark#120 lands (new patch release)
|
||||
// and replace the nested if-let with:
|
||||
// if next == Event::End(other_tag.clone()) {
|
||||
// break;
|
||||
// }
|
||||
if let Event::End(tag) = event {
|
||||
if tag_eq(&tag, &other_tag) {
|
||||
break;
|
||||
}
|
||||
if event == Event::End(other_tag.clone()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,43 +475,6 @@ fn stringify_events(events: Vec<Event>) -> String {
|
||||
.collect()
|
||||
}
|
||||
|
||||
// FIXME: Remove this when google/pulldown_cmark#120 lands (new patch release)
|
||||
fn tag_eq(left: &Tag, right: &Tag) -> bool {
|
||||
match (left, right) {
|
||||
(&Tag::Paragraph, &Tag::Paragraph) => true,
|
||||
(&Tag::Rule, &Tag::Rule) => true,
|
||||
(&Tag::Header(a), &Tag::Header(b)) => a == b,
|
||||
(&Tag::BlockQuote, &Tag::BlockQuote) => true,
|
||||
(&Tag::CodeBlock(ref a), &Tag::CodeBlock(ref b)) => a == b,
|
||||
(&Tag::List(ref a), &Tag::List(ref b)) => a == b,
|
||||
(&Tag::Item, &Tag::Item) => true,
|
||||
(&Tag::FootnoteDefinition(ref a), &Tag::FootnoteDefinition(ref b)) => a == b,
|
||||
(&Tag::Table(ref a), &Tag::Table(ref b)) => {
|
||||
a.iter().zip(b.iter()).all(|(l, r)| alignment_eq(*l, *r))
|
||||
}
|
||||
(&Tag::TableHead, &Tag::TableHead) => true,
|
||||
(&Tag::TableRow, &Tag::TableRow) => true,
|
||||
(&Tag::TableCell, &Tag::TableCell) => true,
|
||||
(&Tag::Emphasis, &Tag::Emphasis) => true,
|
||||
(&Tag::Strong, &Tag::Strong) => true,
|
||||
(&Tag::Code, &Tag::Code) => true,
|
||||
(&Tag::Link(ref a_1, ref a_2), &Tag::Link(ref b_1, ref b_2)) => a_1 == b_1 && a_2 == b_2,
|
||||
(&Tag::Image(ref a_1, ref a_2), &Tag::Image(ref b_1, ref b_2)) => a_1 == b_1 && a_2 == b_2,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Remove this when google/pulldown_cmark#120 lands (new patch release)
|
||||
fn alignment_eq(left: Alignment, right: Alignment) -> bool {
|
||||
match (left, right) {
|
||||
(Alignment::None, Alignment::None) => true,
|
||||
(Alignment::Left, Alignment::Left) => true,
|
||||
(Alignment::Center, Alignment::Center) => true,
|
||||
(Alignment::Right, Alignment::Right) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
|
||||
/// a pretty `Display` impl.
|
||||
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
|
||||
|
||||
107
src/config.rs
107
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"));
|
||||
@@ -383,7 +383,6 @@ pub struct BuildConfig {
|
||||
pub create_missing: bool,
|
||||
/// Which preprocessors should be applied
|
||||
pub preprocess: Option<Vec<String>>,
|
||||
|
||||
}
|
||||
|
||||
impl Default for BuildConfig {
|
||||
@@ -410,7 +409,7 @@ pub struct HtmlConfig {
|
||||
pub google_analytics: Option<String>,
|
||||
/// Additional CSS stylesheets to include in the rendered page's `<head>`.
|
||||
pub additional_css: Vec<PathBuf>,
|
||||
/// Additional JS scripts to include at the bottom of the rendered page's
|
||||
/// Additional JS scripts to include at the bottom of the rendered page's
|
||||
/// `<body>`.
|
||||
pub additional_js: Vec<PathBuf>,
|
||||
/// Playpen settings.
|
||||
@@ -425,25 +424,85 @@ pub struct HtmlConfig {
|
||||
pub livereload_url: Option<String>,
|
||||
/// Should section labels be rendered?
|
||||
pub no_section_label: bool,
|
||||
/// Search settings. If `None`, the default will be used.
|
||||
pub search: Option<Search>,
|
||||
}
|
||||
|
||||
impl HtmlConfig {
|
||||
/// Returns the directory of theme from the provided root directory. If the
|
||||
/// directory is not present it will append the default directory of "theme"
|
||||
pub fn theme_dir(&self, root: &PathBuf) -> PathBuf {
|
||||
match self.theme {
|
||||
Some(ref d) => root.join(d),
|
||||
None => root.join("theme"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -520,12 +579,14 @@ mod tests {
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("outputs"),
|
||||
create_missing: false,
|
||||
preprocess: Some(vec!["first_preprocessor".to_string(),
|
||||
"second_preprocessor".to_string()]),
|
||||
preprocess: Some(vec![
|
||||
"first_preprocessor".to_string(),
|
||||
"second_preprocessor".to_string(),
|
||||
]),
|
||||
};
|
||||
let playpen_should_be = Playpen {
|
||||
editable: true,
|
||||
editor: PathBuf::from("ace"),
|
||||
copy_js: true,
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
|
||||
30
src/lib.rs
30
src/lib.rs
@@ -1,7 +1,8 @@
|
||||
//! # mdBook
|
||||
//!
|
||||
//! **mdBook** is similar to GitBook but implemented in Rust.
|
||||
//! It offers a command line interface, but can also be used as a regular crate.
|
||||
//! **mdBook** is a tool for rendering a collection of markdown documents into
|
||||
//! a form more suitable for end users like HTML or EPUB. It offers a command
|
||||
//! line interface, but this crate can be used if more control is required.
|
||||
//!
|
||||
//! This is the API doc, the [user guide] is also available if you want
|
||||
//! information about the command line tool, format, structure etc. It is also
|
||||
@@ -15,6 +16,12 @@
|
||||
//! - Accessing the public API to help create a new Renderer
|
||||
//! - ...
|
||||
//!
|
||||
//! > **Note:** While we try to ensure `mdbook`'s command-line interface and
|
||||
//! > behaviour are backwards compatible, the tool's internals are still
|
||||
//! > evolving and being iterated on. If you wish to prevent accidental
|
||||
//! > breakages it is recommended to pin any tools building on top of the
|
||||
//! > `mdbook` crate to a specific release.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! If creating a new book from scratch, you'll want to get a `BookBuilder` via
|
||||
@@ -52,20 +59,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/
|
||||
@@ -92,7 +99,7 @@ extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
extern crate shlex;
|
||||
extern crate tempdir;
|
||||
extern crate tempfile;
|
||||
extern crate toml;
|
||||
extern crate toml_query;
|
||||
|
||||
@@ -122,6 +129,7 @@ pub mod errors {
|
||||
HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"];
|
||||
HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"];
|
||||
Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
|
||||
SerdeJson(::serde_json::Error) #[doc = "JSON conversion failed"];
|
||||
}
|
||||
|
||||
links {
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
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 +24,10 @@ impl HtmlHandlebars {
|
||||
HtmlHandlebars
|
||||
}
|
||||
|
||||
fn write_file<P: AsRef<Path>>(
|
||||
&self,
|
||||
build_dir: &Path,
|
||||
filename: P,
|
||||
content: &[u8],
|
||||
) -> Result<()> {
|
||||
let path = build_dir.join(filename);
|
||||
|
||||
utils::fs::create_file(&path)?
|
||||
.write_all(content)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn render_item(
|
||||
&self,
|
||||
item: &BookItem,
|
||||
mut ctx: RenderItemContext,
|
||||
item: &BookItem,
|
||||
mut ctx: RenderItemContext,
|
||||
print_content: &mut String,
|
||||
) -> Result<()> {
|
||||
// FIXME: This should be made DRY-er and rely less on mutable state
|
||||
@@ -56,6 +41,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 +73,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 +110,7 @@ impl HtmlHandlebars {
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
self.write_file(destination, "index.html", content.as_bytes())?;
|
||||
utils::fs::write_file(destination, "index.html", content.as_bytes())?;
|
||||
|
||||
debug!(
|
||||
"Creating index.html from {} ✓",
|
||||
@@ -153,45 +140,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 +189,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 +295,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 +318,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 +333,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 +400,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 +470,7 @@ fn wrap_header_with_link(level: usize,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
filepath: &str)
|
||||
-> String {
|
||||
let raw_id = id_from_content(content);
|
||||
let raw_id = utils::id_from_content(content);
|
||||
|
||||
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
|
||||
|
||||
@@ -489,33 +490,6 @@ fn wrap_header_with_link(level: usize,
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate an id for use with anchors which is derived from a "normalised"
|
||||
/// string.
|
||||
fn id_from_content(content: &str) -> String {
|
||||
let mut content = content.to_string();
|
||||
|
||||
// Skip any tags or html-encoded stuff
|
||||
const REPL_SUB: &[&str] = &["<em>",
|
||||
"</em>",
|
||||
"<code>",
|
||||
"</code>",
|
||||
"<strong>",
|
||||
"</strong>",
|
||||
"<",
|
||||
">",
|
||||
"&",
|
||||
"'",
|
||||
"""];
|
||||
for sub in REPL_SUB {
|
||||
content = content.replace(sub, "");
|
||||
}
|
||||
|
||||
// Remove spaces and hastags indicating a header
|
||||
let trimmed = content.trim().trim_left_matches('#').trim();
|
||||
|
||||
normalize_id(trimmed)
|
||||
}
|
||||
|
||||
// anchors to the same page (href="#anchor") do not work because of
|
||||
// <base href="../"> pointing to the root folder. This function *fixes*
|
||||
// that in a very inelegant way
|
||||
@@ -555,8 +529,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 +564,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 +596,6 @@ struct RenderItemContext<'a> {
|
||||
html_config: HtmlConfig,
|
||||
}
|
||||
|
||||
pub fn normalize_path(path: &str) -> String {
|
||||
use std::path::is_separator;
|
||||
path.chars()
|
||||
.map(|ch| if is_separator(ch) { '/' } else { ch })
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
pub fn normalize_id(content: &str) -> String {
|
||||
content.chars()
|
||||
.filter_map(|ch| if ch.is_alphanumeric() || ch == '_' || ch == '-' {
|
||||
Some(ch.to_ascii_lowercase())
|
||||
} else if ch.is_whitespace() {
|
||||
Some('-')
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -687,12 +639,4 @@ mod tests {
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchor_generation() {
|
||||
assert_eq!(id_from_content("## `--passes`: add more rustdoc passes"),
|
||||
"--passes-add-more-rustdoc-passes");
|
||||
assert_eq!(id_from_content("## Method-call expressions"),
|
||||
"method-call-expressions");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
pub mod navigation;
|
||||
pub mod toc;
|
||||
pub mod navigation;
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::collections::BTreeMap;
|
||||
use serde_json;
|
||||
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
|
||||
|
||||
|
||||
type StringMap = BTreeMap<String, String>;
|
||||
|
||||
/// Target for `find_chapter`.
|
||||
@@ -15,22 +14,23 @@ enum Target {
|
||||
|
||||
impl Target {
|
||||
/// Returns target if found.
|
||||
fn find(&self,
|
||||
base_path: &String,
|
||||
current_path: &String,
|
||||
current_item: &StringMap,
|
||||
previous_item: &StringMap,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
fn find(
|
||||
&self,
|
||||
base_path: &String,
|
||||
current_path: &String,
|
||||
current_item: &StringMap,
|
||||
previous_item: &StringMap,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
match self {
|
||||
&Target::Next => {
|
||||
let previous_path = previous_item.get("path").ok_or_else(|| {
|
||||
RenderError::new("No path found for chapter in JSON data")
|
||||
})?;
|
||||
let previous_path = previous_item
|
||||
.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
|
||||
|
||||
if previous_path == base_path {
|
||||
return Ok(Some(current_item.clone()));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
&Target::Previous => {
|
||||
if current_path == base_path {
|
||||
@@ -43,21 +43,18 @@ impl Target {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_chapter(
|
||||
rc: &mut RenderContext,
|
||||
target: Target
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
fn find_chapter(rc: &mut RenderContext, target: Target) -> Result<Option<StringMap>, RenderError> {
|
||||
debug!("Get data from context");
|
||||
|
||||
let chapters = rc.evaluate_absolute("chapters").and_then(|c| {
|
||||
let chapters = rc.evaluate_absolute("chapters", true).and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
|
||||
let base_path = rc.evaluate_absolute("path")?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
let base_path = rc.evaluate_absolute("path", true)?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
let mut previous: Option<StringMap> = None;
|
||||
|
||||
@@ -78,7 +75,7 @@ fn find_chapter(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn render(
|
||||
@@ -91,18 +88,21 @@ fn render(
|
||||
|
||||
let mut context = BTreeMap::new();
|
||||
|
||||
chapter.get("name")
|
||||
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
|
||||
.map(|name| context.insert("title".to_owned(), json!(name)))?;
|
||||
chapter
|
||||
.get("name")
|
||||
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
|
||||
.map(|name| context.insert("title".to_owned(), json!(name)))?;
|
||||
|
||||
chapter.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
|
||||
.and_then(|p| {
|
||||
Path::new(p).with_extension("html")
|
||||
.to_str()
|
||||
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
|
||||
.map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/"))))
|
||||
})?;
|
||||
chapter
|
||||
.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
|
||||
.and_then(|p| {
|
||||
Path::new(p)
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
|
||||
.map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/"))))
|
||||
})?;
|
||||
|
||||
trace!("Render template");
|
||||
|
||||
@@ -138,14 +138,14 @@ pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), R
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::*;
|
||||
|
||||
static TEMPLATE: &'static str =
|
||||
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
|
||||
static TEMPLATE: &'static str =
|
||||
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
|
||||
|
||||
#[test]
|
||||
fn test_next_previous() {
|
||||
let data = json!({
|
||||
#[test]
|
||||
fn test_next_previous() {
|
||||
let data = json!({
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
"chapters": [
|
||||
@@ -164,18 +164,19 @@ mod tests {
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
|
||||
assert_eq!(
|
||||
h.template_render(TEMPLATE, &data).unwrap(),
|
||||
"one: one.html|three: three.html");
|
||||
}
|
||||
assert_eq!(
|
||||
h.render_template(TEMPLATE, &data).unwrap(),
|
||||
"one: one.html|three: three.html"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first() {
|
||||
let data = json!({
|
||||
#[test]
|
||||
fn test_first() {
|
||||
let data = json!({
|
||||
"name": "one",
|
||||
"path": "one.path",
|
||||
"chapters": [
|
||||
@@ -194,17 +195,18 @@ mod tests {
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
|
||||
assert_eq!(
|
||||
h.template_render(TEMPLATE, &data).unwrap(),
|
||||
"|two: two.html");
|
||||
}
|
||||
#[test]
|
||||
fn test_last() {
|
||||
let data = json!({
|
||||
assert_eq!(
|
||||
h.render_template(TEMPLATE, &data).unwrap(),
|
||||
"|two: two.html"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_last() {
|
||||
let data = json!({
|
||||
"name": "three",
|
||||
"path": "three.path",
|
||||
"chapters": [
|
||||
@@ -223,12 +225,13 @@ mod tests {
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
|
||||
assert_eq!(
|
||||
h.template_render(TEMPLATE, &data).unwrap(),
|
||||
"two: two.html|");
|
||||
}
|
||||
assert_eq!(
|
||||
h.render_template(TEMPLATE, &data).unwrap(),
|
||||
"two: two.html|"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use pulldown_cmark::{html, Event, Parser, Tag};
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RenderToc {
|
||||
pub no_section_label: bool
|
||||
pub no_section_label: bool,
|
||||
}
|
||||
|
||||
impl HelperDef for RenderToc {
|
||||
@@ -16,14 +16,14 @@ impl HelperDef for RenderToc {
|
||||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = rc.evaluate_absolute("chapters").and_then(|c| {
|
||||
let chapters = rc.evaluate_absolute("chapters", true).and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
let current = rc.evaluate_absolute("path")?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
let current = rc.evaluate_absolute("path", true)?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
rc.writer.write_all(b"<ol class=\"chapter\">")?;
|
||||
|
||||
@@ -107,12 +107,12 @@ impl HelperDef for RenderToc {
|
||||
|
||||
// filter all events that are not inline code blocks
|
||||
let parser = Parser::new(name).filter(|event| match *event {
|
||||
Event::Start(Tag::Code) |
|
||||
Event::End(Tag::Code) |
|
||||
Event::InlineHtml(_) |
|
||||
Event::Text(_) => true,
|
||||
_ => false,
|
||||
});
|
||||
Event::Start(Tag::Code)
|
||||
| Event::End(Tag::Code)
|
||||
| Event::InlineHtml(_)
|
||||
| Event::Text(_) => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// render markdown to html
|
||||
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
|
||||
|
||||
@@ -4,3 +4,6 @@ pub use self::hbs_renderer::HtmlHandlebars;
|
||||
|
||||
mod hbs_renderer;
|
||||
mod helpers;
|
||||
|
||||
#[cfg(feature = "search")]
|
||||
mod search;
|
||||
|
||||
231
src/renderer/html_handlebars/search.rs
Normal file
231
src/renderer/html_handlebars/search.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
extern crate ammonia;
|
||||
extern crate elasticlunr;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use pulldown_cmark::*;
|
||||
use serde_json;
|
||||
use self::elasticlunr::Index;
|
||||
|
||||
use book::{Book, BookItem};
|
||||
use config::Search;
|
||||
use errors::*;
|
||||
use utils;
|
||||
use theme::searcher;
|
||||
|
||||
/// Creates all files required for search.
|
||||
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
|
||||
let mut index = Index::new(&["title", "body", "breadcrumbs"]);
|
||||
|
||||
for item in book.iter() {
|
||||
render_item(&mut index, &search_config, item)?;
|
||||
}
|
||||
|
||||
let index = write_to_js(index, &search_config)?;
|
||||
debug!("Writing search index ✓");
|
||||
|
||||
if search_config.copy_js {
|
||||
utils::fs::write_file(destination, "searchindex.js", index.as_bytes())?;
|
||||
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
||||
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
||||
utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
|
||||
debug!("Copying search files ✓");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uses the given arguments to construct a search document, then inserts it to the given index.
|
||||
fn add_doc<'a>(
|
||||
index: &mut Index,
|
||||
anchor_base: &'a str,
|
||||
section_id: &Option<String>,
|
||||
items: &[&str],
|
||||
) {
|
||||
let doc_ref: Cow<'a, str> = if let &Some(ref id) = section_id {
|
||||
format!("{}#{}", anchor_base, id).into()
|
||||
} else {
|
||||
anchor_base.into()
|
||||
};
|
||||
let doc_ref = utils::collapse_whitespace(doc_ref.trim());
|
||||
let items = items.iter().map(|&x| utils::collapse_whitespace(x.trim()));
|
||||
index.add_doc(&doc_ref, items);
|
||||
}
|
||||
|
||||
/// Renders markdown into flat unformatted text and adds it to the search index.
|
||||
fn render_item(
|
||||
index: &mut Index,
|
||||
search_config: &Search,
|
||||
item: &BookItem,
|
||||
) -> Result<()> {
|
||||
let chapter = match item {
|
||||
&BookItem::Chapter(ref ch) => ch,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
let filepath = Path::new(&chapter.path).with_extension("html");
|
||||
let filepath = filepath
|
||||
.to_str()
|
||||
.chain_err(|| "Could not convert HTML path to str")?;
|
||||
let anchor_base = utils::fs::normalize_path(filepath);
|
||||
|
||||
let mut opts = Options::empty();
|
||||
opts.insert(OPTION_ENABLE_TABLES);
|
||||
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
||||
let p = Parser::new_ext(&chapter.content, opts);
|
||||
|
||||
let mut in_header = false;
|
||||
let max_section_depth = search_config.heading_split_level as i32;
|
||||
let mut section_id = None;
|
||||
let mut heading = String::new();
|
||||
let mut body = String::new();
|
||||
let mut breadcrumbs = chapter.parent_names.clone();
|
||||
let mut footnote_numbers = HashMap::new();
|
||||
|
||||
for event in p {
|
||||
match event {
|
||||
Event::Start(Tag::Header(i)) if i <= max_section_depth => {
|
||||
if heading.len() > 0 {
|
||||
// Section finished, the next header is following now
|
||||
// Write the data to the index, and clear it for the next section
|
||||
add_doc(
|
||||
index,
|
||||
&anchor_base,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
section_id = None;
|
||||
heading.clear();
|
||||
body.clear();
|
||||
breadcrumbs.pop();
|
||||
}
|
||||
|
||||
in_header = true;
|
||||
}
|
||||
Event::End(Tag::Header(i)) if i <= max_section_depth => {
|
||||
in_header = false;
|
||||
section_id = Some(utils::id_from_content(&heading));
|
||||
breadcrumbs.push(heading.clone());
|
||||
}
|
||||
Event::Start(Tag::FootnoteDefinition(name)) => {
|
||||
let number = footnote_numbers.len() + 1;
|
||||
footnote_numbers.entry(name).or_insert(number);
|
||||
}
|
||||
Event::Start(_) | Event::End(_) | Event::SoftBreak | Event::HardBreak => {
|
||||
// Insert spaces where HTML output would usually seperate text
|
||||
// to ensure words don't get merged together
|
||||
if in_header {
|
||||
heading.push(' ');
|
||||
} else {
|
||||
body.push(' ');
|
||||
}
|
||||
}
|
||||
Event::Text(text) => {
|
||||
if in_header {
|
||||
heading.push_str(&text);
|
||||
} else {
|
||||
body.push_str(&text);
|
||||
}
|
||||
}
|
||||
Event::Html(html) | Event::InlineHtml(html) => {
|
||||
body.push_str(&clean_html(&html));
|
||||
}
|
||||
Event::FootnoteReference(name) => {
|
||||
let len = footnote_numbers.len() + 1;
|
||||
let number = footnote_numbers.entry(name).or_insert(len);
|
||||
body.push_str(&format!(" [{}] ", number));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if heading.len() > 0 {
|
||||
// Make sure the last section is added to the index
|
||||
add_doc(
|
||||
index,
|
||||
&anchor_base,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Exports the index and search options to a JS script which stores the index in `window.search`.
|
||||
/// Using a JS script is a workaround for CORS in `file://` URIs. It also removes the need for
|
||||
/// downloading/parsing JSON in JS.
|
||||
fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
|
||||
use std::collections::BTreeMap;
|
||||
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ResultsOptions {
|
||||
limit_results: u32,
|
||||
teaser_word_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SearchindexJson {
|
||||
/// The options used for displaying search results
|
||||
resultsoptions: ResultsOptions,
|
||||
/// The searchoptions for elasticlunr.js
|
||||
searchoptions: SearchOptions,
|
||||
/// The index for elasticlunr.js
|
||||
index: elasticlunr::Index,
|
||||
}
|
||||
|
||||
let mut fields = BTreeMap::new();
|
||||
let mut opt = SearchOptionsField::default();
|
||||
opt.boost = Some(search_config.boost_title);
|
||||
fields.insert("title".into(), opt);
|
||||
opt.boost = Some(search_config.boost_paragraph);
|
||||
fields.insert("body".into(), opt);
|
||||
opt.boost = Some(search_config.boost_hierarchy);
|
||||
fields.insert("breadcrumbs".into(), opt);
|
||||
|
||||
let searchoptions = SearchOptions {
|
||||
bool: if search_config.use_boolean_and {
|
||||
SearchBool::And
|
||||
} else {
|
||||
SearchBool::Or
|
||||
},
|
||||
expand: search_config.expand,
|
||||
fields,
|
||||
};
|
||||
|
||||
let resultsoptions = ResultsOptions {
|
||||
limit_results: search_config.limit_results,
|
||||
teaser_word_count: search_config.teaser_word_count,
|
||||
};
|
||||
|
||||
let json_contents = SearchindexJson {
|
||||
resultsoptions,
|
||||
searchoptions,
|
||||
index,
|
||||
};
|
||||
let json_contents = serde_json::to_string(&json_contents)?;
|
||||
|
||||
Ok(format!("window.search = {};", json_contents))
|
||||
}
|
||||
|
||||
fn clean_html(html: &str) -> String {
|
||||
lazy_static! {
|
||||
static ref AMMONIA: ammonia::Builder<'static> = {
|
||||
let mut clean_content = HashSet::new();
|
||||
clean_content.insert("script");
|
||||
clean_content.insert("style");
|
||||
let mut builder = ammonia::Builder::new();
|
||||
builder
|
||||
.tags(HashSet::new())
|
||||
.tag_attributes(HashMap::new())
|
||||
.generic_attributes(HashSet::new())
|
||||
.link_rel(None)
|
||||
.allowed_classes(HashMap::new())
|
||||
.clean_content_tags(clean_content);
|
||||
builder
|
||||
};
|
||||
}
|
||||
AMMONIA.clean(html).to_string()
|
||||
}
|
||||
@@ -36,6 +36,15 @@ h5 {
|
||||
.header + .header h5 {
|
||||
margin-top: 1em;
|
||||
}
|
||||
a.header:target h1:before,
|
||||
a.header:target h2:before,
|
||||
a.header:target h3:before,
|
||||
a.header:target h4:before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-left: -30px;
|
||||
width: 30px;
|
||||
}
|
||||
table {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
@@ -47,6 +56,17 @@ table td {
|
||||
table thead td {
|
||||
font-weight: 700;
|
||||
}
|
||||
:not(.footnote-definition) + .footnote-definition,
|
||||
.footnote-definition + :not(.footnote-definition) {
|
||||
margin-top: 2em;
|
||||
}
|
||||
.footnote-definition {
|
||||
font-size: 0.9em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
.footnote-definition p {
|
||||
display: inline;
|
||||
}
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@@ -60,6 +80,7 @@ table thead td {
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
-webkit-transition: -webkit-transform 0.5s;
|
||||
-moz-transition: -moz-transform 0.5s;
|
||||
-o-transition: -o-transform 0.5s;
|
||||
@@ -83,16 +104,26 @@ table thead td {
|
||||
}
|
||||
.chapter li a {
|
||||
display: block;
|
||||
padding: 5px 0;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
.chapter li a {
|
||||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
.chapter li a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.chapter .spacer {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
margin: 10px 0px;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
.chapter .spacer {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
.section {
|
||||
list-style: none outside none;
|
||||
@@ -106,33 +137,45 @@ table thead td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.page-wrapper {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-transition: padding-left 0.5s, margin-left 0.5s;
|
||||
-moz-transition: padding-left 0.5s, margin-left 0.5s;
|
||||
-o-transition: padding-left 0.5s, margin-left 0.5s;
|
||||
-ms-transition: padding-left 0.5s, margin-left 0.5s;
|
||||
transition: padding-left 0.5s, margin-left 0.5s;
|
||||
-webkit-transition: padding-left 0.5s, margin-left 0.5s, left 0.5s;
|
||||
-moz-transition: padding-left 0.5s, margin-left 0.5s, left 0.5s;
|
||||
-o-transition: padding-left 0.5s, margin-left 0.5s, left 0.5s;
|
||||
-ms-transition: padding-left 0.5s, margin-left 0.5s, left 0.5s;
|
||||
transition: padding-left 0.5s, margin-left 0.5s, left 0.5s;
|
||||
}
|
||||
.sidebar-visible .page-wrapper {
|
||||
padding-left: 300px;
|
||||
}
|
||||
@media only screen and (max-width: 1079px) {
|
||||
.sidebar-visible .page-wrapper {
|
||||
padding-left: 0;
|
||||
margin-left: 300px;
|
||||
}
|
||||
left: 300px;
|
||||
}
|
||||
.page {
|
||||
outline: 0;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.content {
|
||||
position: relative;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
right: 0;
|
||||
left: 0;
|
||||
padding: 0 15px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
.sidebar-visible .content {
|
||||
position: absolute;
|
||||
top: 52px;
|
||||
}
|
||||
.content > main {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 750px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
.content a {
|
||||
text-decoration: none;
|
||||
@@ -209,6 +252,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||
overflow: hidden;
|
||||
-o-text-overflow: ellipsis;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav-chapters {
|
||||
font-size: 2.5em;
|
||||
@@ -314,6 +358,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 {
|
||||
@@ -382,6 +427,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 {
|
||||
@@ -476,10 +522,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 {
|
||||
@@ -548,6 +618,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 {
|
||||
@@ -642,10 +713,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 {
|
||||
@@ -714,6 +809,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 {
|
||||
@@ -808,10 +904,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 {
|
||||
@@ -880,6 +1000,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 {
|
||||
@@ -974,10 +1095,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 {
|
||||
@@ -1046,6 +1191,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 {
|
||||
@@ -1140,6 +1286,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,
|
||||
@@ -1220,3 +1389,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;
|
||||
}
|
||||
|
||||
@@ -298,11 +298,13 @@ function playpen_text(playpen) {
|
||||
function showThemes() {
|
||||
themePopup.style.display = 'block';
|
||||
themeToggleButton.setAttribute('aria-expanded', true);
|
||||
themePopup.querySelector("button#" + document.body.className).focus();
|
||||
}
|
||||
|
||||
function hideThemes() {
|
||||
themePopup.style.display = 'none';
|
||||
themeToggleButton.setAttribute('aria-expanded', false);
|
||||
themeToggleButton.focus();
|
||||
}
|
||||
|
||||
function set_theme(theme) {
|
||||
@@ -369,19 +371,51 @@ function playpen_text(playpen) {
|
||||
set_theme(theme);
|
||||
});
|
||||
|
||||
// Hide theme selector popup when clicking outside of it
|
||||
document.addEventListener('click', function (event) {
|
||||
if (themePopup.style.display === 'block' && !themeToggleButton.contains(event.target) && !themePopup.contains(event.target)) {
|
||||
themePopup.addEventListener('focusout', function(e) {
|
||||
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
|
||||
if (!!e.relatedTarget && !themePopup.contains(e.relatedTarget)) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang-nursery/mdBook/issues/628
|
||||
document.addEventListener('click', function(e) {
|
||||
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
if (!themePopup.contains(e.target)) { return; }
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
hideThemes();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
var li = document.activeElement.parentElement;
|
||||
if (li && li.previousElementSibling) {
|
||||
li.previousElementSibling.querySelector('button').focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
var li = document.activeElement.parentElement;
|
||||
if (li && li.nextElementSibling) {
|
||||
li.nextElementSibling.querySelector('button').focus();
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
themePopup.querySelector('li:first-child button').focus();
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
themePopup.querySelector('li:last-child button').focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -465,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':
|
||||
@@ -522,6 +557,14 @@ function playpen_text(playpen) {
|
||||
});
|
||||
})();
|
||||
|
||||
(function scrollToTop () {
|
||||
var menuTitle = document.querySelector('.menu-title');
|
||||
|
||||
menuTitle.addEventListener('click', function () {
|
||||
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
})();
|
||||
|
||||
(function autoHideMenu() {
|
||||
var menu = document.getElementById('menu-bar');
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}">
|
||||
<html lang="{{ language }}" class="sidebar-visible">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
@@ -41,6 +42,14 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<noscript>
|
||||
<style type="text/css">
|
||||
.javascript-only {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
|
||||
</head>
|
||||
<body class="light">
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
@@ -70,12 +79,14 @@
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script type="text/javascript">
|
||||
var html = document.querySelector('html');
|
||||
var sidebar = 'hidden';
|
||||
if (document.body.clientWidth >= 1080) {
|
||||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||
sidebar = sidebar || 'visible';
|
||||
}
|
||||
document.querySelector('html').classList.add("sidebar-" + sidebar);
|
||||
html.classList.remove('sidebar-visible');
|
||||
html.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
@@ -88,23 +99,28 @@
|
||||
{{> header}}
|
||||
<div id="menu-bar" class="menu-bar">
|
||||
<div id="menu-bar-sticky-container">
|
||||
<div class="left-buttons">
|
||||
<div class="left-buttons javascript-only">
|
||||
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
</button>
|
||||
<ul id="theme-list" class="theme-popup" aria-label="submenu">
|
||||
<li><button class="theme" id="light">Light <span class="default">(default)</span></button></li>
|
||||
<li><button class="theme" id="rust">Rust</button></li>
|
||||
<li><button class="theme" id="coal">Coal</button></li>
|
||||
<li><button class="theme" id="navy">Navy</button></li>
|
||||
<li><button class="theme" id="ayu">Ayu</button></li>
|
||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="light">Light <span class="default">(default)</span></button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||||
</ul>
|
||||
{{#if search_enabled}}
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
|
||||
<div class="right-buttons">
|
||||
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||
@@ -114,6 +130,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if search_enabled}}
|
||||
<div id="searchbar-outer" class="searchbar-outer">
|
||||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
</div>
|
||||
<div id="searchresults-outer" class="searchresults-outer">
|
||||
<div class="searchresults-header" id="searchresults-header"></div>
|
||||
<ul id="searchresults">
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||
<script type="text/javascript">
|
||||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||
@@ -211,12 +238,21 @@
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playpens_editable}}
|
||||
<script src="{{ ace_js }}" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ editor_js }}" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ mode_rust_js }}" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ theme_dawn_js }}" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ theme_tomorrow_night_js }}" type="text/javascript" charset="utf-8"></script>
|
||||
{{#if playpen_js}}
|
||||
<script src="ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="editor.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="mode-rust.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="theme-dawn.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
|
||||
{{/if}}
|
||||
|
||||
{{#if search_enabled}}
|
||||
<script src="searchindex.js" type="text/javascript" charset="utf-8"></script>
|
||||
{{/if}}
|
||||
{{#if search_js}}
|
||||
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||
{{/if}}
|
||||
|
||||
{{#if is_print}}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#![allow(missing_docs)] // FIXME: Document this
|
||||
#![allow(missing_docs)]
|
||||
|
||||
pub mod playpen_editor;
|
||||
|
||||
#[cfg(feature = "search")]
|
||||
pub mod searcher;
|
||||
|
||||
use std::path::Path;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
@@ -52,6 +56,8 @@ pub struct Theme {
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
/// Creates a `Theme` from the given `theme_dir`.
|
||||
/// If a file is found in the theme dir, it will override the default version.
|
||||
pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
|
||||
let theme_dir = theme_dir.as_ref();
|
||||
let mut theme = Theme::default();
|
||||
@@ -128,7 +134,7 @@ fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempdir::TempDir;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
@@ -153,7 +159,7 @@ mod tests {
|
||||
.map(|f| f.path())
|
||||
.filter(|p| p.is_file() && !p.ends_with(".rs"));
|
||||
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
|
||||
// "touch" all of the special files so we have empty copies
|
||||
for special_file in special_files {
|
||||
|
||||
@@ -1,70 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use theme::load_file_contents;
|
||||
//! Theme dependencies for the playpen editor.
|
||||
|
||||
pub static JS: &'static [u8] = include_bytes!("editor.js");
|
||||
pub static ACE_JS: &'static [u8] = include_bytes!("ace.js");
|
||||
pub static MODE_RUST_JS: &'static [u8] = include_bytes!("mode-rust.js");
|
||||
pub static THEME_DAWN_JS: &'static [u8] = include_bytes!("theme-dawn.js");
|
||||
pub static THEME_TOMORROW_NIGHT_JS: &'static [u8] = include_bytes!("theme-tomorrow_night.js");
|
||||
|
||||
/// Integration of a JavaScript editor for playpens.
|
||||
/// Uses the Ace editor: https://ace.c9.io/.
|
||||
/// The Ace editor itself, the mode, and the theme files are the
|
||||
/// generated minified no conflict versions.
|
||||
///
|
||||
/// The `PlaypenEditor` struct should be used instead of the static variables because
|
||||
/// the `new()` method
|
||||
/// will look if the user has an editor directory in his source folder and use
|
||||
/// the users editor instead
|
||||
/// of the default.
|
||||
///
|
||||
/// You should exceptionnaly use the static variables only if you need the
|
||||
/// default editor even if the
|
||||
/// user has specified another editor.
|
||||
pub struct PlaypenEditor {
|
||||
pub js: Vec<u8>,
|
||||
pub ace_js: Vec<u8>,
|
||||
pub mode_rust_js: Vec<u8>,
|
||||
pub theme_dawn_js: Vec<u8>,
|
||||
pub theme_tomorrow_night_js: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PlaypenEditor {
|
||||
pub fn new(src: &Path) -> Self {
|
||||
let mut editor = PlaypenEditor {
|
||||
js: JS.to_owned(),
|
||||
ace_js: ACE_JS.to_owned(),
|
||||
mode_rust_js: MODE_RUST_JS.to_owned(),
|
||||
theme_dawn_js: THEME_DAWN_JS.to_owned(),
|
||||
theme_tomorrow_night_js: THEME_TOMORROW_NIGHT_JS.to_owned(),
|
||||
};
|
||||
|
||||
// Check if the given path exists
|
||||
if !src.exists() || !src.is_dir() {
|
||||
return editor;
|
||||
}
|
||||
|
||||
// Check for individual files if they exist
|
||||
{
|
||||
let files = vec![(src.join("editor.js"), &mut editor.js),
|
||||
(src.join("ace.js"), &mut editor.ace_js),
|
||||
(src.join("mode-rust.js"), &mut editor.mode_rust_js),
|
||||
(src.join("theme-dawn.js"), &mut editor.theme_dawn_js),
|
||||
(src.join("theme-tomorrow_night.js"),
|
||||
&mut editor.theme_tomorrow_night_js)];
|
||||
|
||||
for (filename, dest) in files {
|
||||
if !filename.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = load_file_contents(&filename, dest) {
|
||||
warn!("Couldn't load custom file, {}: {}", filename.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor
|
||||
}
|
||||
}
|
||||
|
||||
10
src/theme/searcher/elasticlunr.min.js
vendored
Normal file
10
src/theme/searcher/elasticlunr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
src/theme/searcher/mark.min.js
vendored
Normal file
7
src/theme/searcher/mark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/theme/searcher/mod.rs
Normal file
6
src/theme/searcher/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! Theme dependencies for in-browser search. Not included in mdbook when
|
||||
//! the "search" cargo feature is disabled.
|
||||
|
||||
pub static JS: &'static [u8] = include_bytes!("searcher.js");
|
||||
pub static MARK_JS: &'static [u8] = include_bytes!("mark.min.js");
|
||||
pub static ELASTICLUNR_JS: &'static [u8] = include_bytes!("elasticlunr.min.js");
|
||||
464
src/theme/searcher/searcher.js
Normal file
464
src/theme/searcher/searcher.js
Normal file
@@ -0,0 +1,464 @@
|
||||
window.search = window.search || {};
|
||||
(function search(search) {
|
||||
// Search functionality
|
||||
//
|
||||
// You can use !hasFocus() to prevent keyhandling in your key
|
||||
// event handlers while the user is typing his search.
|
||||
|
||||
if (!Mark || !elasticlunr) {
|
||||
return;
|
||||
}
|
||||
|
||||
var searchbar = document.getElementById('searchbar'),
|
||||
searchbar_outer = document.getElementById('searchbar-outer'),
|
||||
searchresults = document.getElementById('searchresults'),
|
||||
searchresults_outer = document.getElementById('searchresults-outer'),
|
||||
searchresults_header = document.getElementById('searchresults-header'),
|
||||
searchicon = document.getElementById('search-toggle'),
|
||||
content = document.getElementById('content'),
|
||||
|
||||
searchindex = null,
|
||||
resultsoptions = {
|
||||
teaser_word_count: 30,
|
||||
limit_results: 30,
|
||||
},
|
||||
searchoptions = {
|
||||
bool: "AND",
|
||||
expand: true,
|
||||
fields: {
|
||||
title: {boost: 1},
|
||||
body: {boost: 1},
|
||||
breadcrumbs: {boost: 0}
|
||||
}
|
||||
},
|
||||
mark_exclude = [],
|
||||
marker = new Mark(content),
|
||||
current_searchterm = "",
|
||||
URL_SEARCH_PARAM = 'search',
|
||||
URL_MARK_PARAM = 'highlight',
|
||||
teaser_count = 0,
|
||||
|
||||
SEARCH_HOTKEY_KEYCODE = 83,
|
||||
ESCAPE_KEYCODE = 27,
|
||||
DOWN_KEYCODE = 40,
|
||||
UP_KEYCODE = 38,
|
||||
SELECT_KEYCODE = 13;
|
||||
|
||||
function hasFocus() {
|
||||
return searchbar === document.activeElement;
|
||||
}
|
||||
|
||||
function removeChildren(elem) {
|
||||
while (elem.firstChild) {
|
||||
elem.removeChild(elem.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to parse a url into its building blocks.
|
||||
function parseURL(url) {
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
return {
|
||||
source: url,
|
||||
protocol: a.protocol.replace(':',''),
|
||||
host: a.hostname,
|
||||
port: a.port,
|
||||
params: (function(){
|
||||
var ret = {};
|
||||
var seg = a.search.replace(/^\?/,'').split('&');
|
||||
var len = seg.length, i = 0, s;
|
||||
for (;i<len;i++) {
|
||||
if (!seg[i]) { continue; }
|
||||
s = seg[i].split('=');
|
||||
ret[s[0]] = s[1];
|
||||
}
|
||||
return ret;
|
||||
})(),
|
||||
file: (a.pathname.match(/\/([^/?#]+)$/i) || [,''])[1],
|
||||
hash: a.hash.replace('#',''),
|
||||
path: a.pathname.replace(/^([^/])/,'/$1')
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to recreate a url string from its building blocks.
|
||||
function renderURL(urlobject) {
|
||||
var url = urlobject.protocol + "://" + urlobject.host;
|
||||
if (urlobject.port != "") {
|
||||
url += ":" + urlobject.port;
|
||||
}
|
||||
url += urlobject.path;
|
||||
var joiner = "?";
|
||||
for(var prop in urlobject.params) {
|
||||
if(urlobject.params.hasOwnProperty(prop)) {
|
||||
url += joiner + prop + "=" + urlobject.params[prop];
|
||||
joiner = "&";
|
||||
}
|
||||
}
|
||||
if (urlobject.hash != "") {
|
||||
url += "#" + urlobject.hash;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// Helper to escape html special chars for displaying the teasers
|
||||
var escapeHTML = (function() {
|
||||
var MAP = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
var repl = function(c) { return MAP[c]; };
|
||||
return function(s) {
|
||||
return s.replace(/[&<>'"]/g, repl);
|
||||
};
|
||||
})();
|
||||
|
||||
function formatSearchMetric(count, searchterm) {
|
||||
if (count == 1) {
|
||||
return count + " search result for '" + searchterm + "':";
|
||||
} else if (count == 0) {
|
||||
return "No search results for '" + searchterm + "'.";
|
||||
} else {
|
||||
return count + " search results for '" + searchterm + "':";
|
||||
}
|
||||
}
|
||||
|
||||
function formatSearchResult(result, searchterms) {
|
||||
var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
|
||||
teaser_count++;
|
||||
|
||||
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
|
||||
var url = result.ref.split("#");
|
||||
if (url.length == 1) { // no anchor found
|
||||
url.push("");
|
||||
}
|
||||
|
||||
return '<a href="' + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
|
||||
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
|
||||
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
|
||||
+ teaser + '</span>';
|
||||
}
|
||||
|
||||
function makeTeaser(body, searchterms) {
|
||||
// The strategy is as follows:
|
||||
// First, assign a value to each word in the document:
|
||||
// Words that correspond to search terms (stemmer aware): 40
|
||||
// Normal words: 2
|
||||
// First word in a sentence: 8
|
||||
// Then use a sliding window with a constant number of words and count the
|
||||
// sum of the values of the words within the window. Then use the window that got the
|
||||
// maximum sum. If there are multiple maximas, then get the last one.
|
||||
// Enclose the terms in <em>.
|
||||
var stemmed_searchterms = searchterms.map(function(w) {
|
||||
return elasticlunr.stemmer(w.toLowerCase());
|
||||
});
|
||||
var searchterm_weight = 40;
|
||||
var weighted = []; // contains elements of ["word", weight, index_in_document]
|
||||
// split in sentences, then words
|
||||
var sentences = body.toLowerCase().split('. ');
|
||||
var index = 0;
|
||||
var value = 0;
|
||||
var searchterm_found = false;
|
||||
for (var sentenceindex in sentences) {
|
||||
var words = sentences[sentenceindex].split(' ');
|
||||
value = 8;
|
||||
for (var wordindex in words) {
|
||||
var word = words[wordindex];
|
||||
if (word.length > 0) {
|
||||
for (var searchtermindex in stemmed_searchterms) {
|
||||
if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) {
|
||||
value = searchterm_weight;
|
||||
searchterm_found = true;
|
||||
}
|
||||
};
|
||||
weighted.push([word, value, index]);
|
||||
value = 2;
|
||||
}
|
||||
index += word.length;
|
||||
index += 1; // ' ' or '.' if last word in sentence
|
||||
};
|
||||
index += 1; // because we split at a two-char boundary '. '
|
||||
};
|
||||
|
||||
if (weighted.length == 0) {
|
||||
return body;
|
||||
}
|
||||
|
||||
var window_weight = [];
|
||||
var window_size = Math.min(weighted.length, resultsoptions.teaser_word_count);
|
||||
|
||||
var cur_sum = 0;
|
||||
for (var wordindex = 0; wordindex < window_size; wordindex++) {
|
||||
cur_sum += weighted[wordindex][1];
|
||||
};
|
||||
window_weight.push(cur_sum);
|
||||
for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
|
||||
cur_sum -= weighted[wordindex][1];
|
||||
cur_sum += weighted[wordindex + window_size][1];
|
||||
window_weight.push(cur_sum);
|
||||
};
|
||||
|
||||
if (searchterm_found) {
|
||||
var max_sum = 0;
|
||||
var max_sum_window_index = 0;
|
||||
// backwards
|
||||
for (var i = window_weight.length - 1; i >= 0; i--) {
|
||||
if (window_weight[i] > max_sum) {
|
||||
max_sum = window_weight[i];
|
||||
max_sum_window_index = i;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
max_sum_window_index = 0;
|
||||
}
|
||||
|
||||
// add <em/> around searchterms
|
||||
var teaser_split = [];
|
||||
var index = weighted[max_sum_window_index][2];
|
||||
for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) {
|
||||
var word = weighted[i];
|
||||
if (index < word[2]) {
|
||||
// missing text from index to start of `word`
|
||||
teaser_split.push(body.substring(index, word[2]));
|
||||
index = word[2];
|
||||
}
|
||||
if (word[1] == searchterm_weight) {
|
||||
teaser_split.push("<em>")
|
||||
}
|
||||
index = word[2] + word[0].length;
|
||||
teaser_split.push(body.substring(word[2], index));
|
||||
if (word[1] == searchterm_weight) {
|
||||
teaser_split.push("</em>")
|
||||
}
|
||||
};
|
||||
|
||||
return teaser_split.join('');
|
||||
}
|
||||
|
||||
function init() {
|
||||
resultsoptions = window.search.resultsoptions;
|
||||
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';
|
||||
content.style.display = 'none';
|
||||
searchicon.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
content.style.display = 'block';
|
||||
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';
|
||||
content.style.display = 'none';
|
||||
searchresults_outer.style.display = 'block';
|
||||
} else {
|
||||
content.style.display = 'block';
|
||||
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, resultsoptions.limit_results);
|
||||
|
||||
// Display search metrics
|
||||
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
|
||||
|
||||
// Clear and insert results
|
||||
var searchterms = searchterm.split(' ');
|
||||
removeChildren(searchresults);
|
||||
for(var i = 0; i < resultcount ; i++){
|
||||
var resultElem = document.createElement('li');
|
||||
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
|
||||
searchresults.appendChild(resultElem);
|
||||
}
|
||||
|
||||
// Display results
|
||||
showResults(true);
|
||||
}
|
||||
|
||||
init();
|
||||
// Exported functions
|
||||
search.hasFocus = hasFocus;
|
||||
})(window.search);
|
||||
@@ -9,3 +9,4 @@
|
||||
@import 'themes'
|
||||
@import 'print'
|
||||
@import 'tooltip'
|
||||
@import 'searchbar'
|
||||
|
||||
@@ -35,6 +35,16 @@ h4, h5 { margin-top: 2em }
|
||||
|
||||
.header + .header h3, .header + .header h4, .header + .header h5 { margin-top: 1em }
|
||||
|
||||
a.header:target h1:before,
|
||||
a.header:target h2:before,
|
||||
a.header:target h3:before,
|
||||
a.header:target h4:before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-left: -30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
@@ -48,3 +58,15 @@ table {
|
||||
td { font-weight: 700; }
|
||||
}
|
||||
}
|
||||
|
||||
:not(.footnote-definition) + .footnote-definition,
|
||||
.footnote-definition + :not(.footnote-definition) {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.footnote-definition {
|
||||
font-size: 0.9em;
|
||||
margin: 0.5em 0;
|
||||
|
||||
p { display: inline; }
|
||||
}
|
||||
|
||||
@@ -38,4 +38,5 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -2,20 +2,18 @@
|
||||
|
||||
.page-wrapper {
|
||||
box-sizing: border-box
|
||||
left: 0
|
||||
position: absolute
|
||||
right: 0
|
||||
top: 0
|
||||
bottom: 0
|
||||
|
||||
// Animation: slide away
|
||||
transition: padding-left 0.5s, margin-left 0.5s
|
||||
transition: padding-left 0.5s, margin-left 0.5s, left 0.5s
|
||||
}
|
||||
|
||||
.sidebar-visible .page-wrapper {
|
||||
padding-left: $sidebar-width
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $page-plus-sidebar-width - 1) {
|
||||
.sidebar-visible .page-wrapper {
|
||||
padding-left: 0
|
||||
margin-left: $sidebar-width
|
||||
}
|
||||
left: $sidebar-width
|
||||
}
|
||||
|
||||
.page {
|
||||
@@ -24,11 +22,21 @@
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-left: auto
|
||||
margin-right: auto
|
||||
max-width: $content-max-width
|
||||
position: relative
|
||||
top: 0
|
||||
bottom: 0
|
||||
overflow-y: auto
|
||||
right: 0
|
||||
left: 0
|
||||
padding: 0 15px
|
||||
padding-bottom: 50px
|
||||
|
||||
main {
|
||||
margin-left: auto
|
||||
margin-right: auto
|
||||
max-width: $content-max-width
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
&:hover { text-decoration: underline; }
|
||||
@@ -36,3 +44,8 @@
|
||||
|
||||
img { max-width: 100%; }
|
||||
}
|
||||
|
||||
.sidebar-visible .content {
|
||||
position: absolute
|
||||
top: 52px
|
||||
}
|
||||
|
||||
67
src/theme/stylus/searchbar.styl
Normal file
67
src/theme/stylus/searchbar.styl
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
font-size: 0.875em
|
||||
box-sizing: border-box
|
||||
-webkit-overflow-scrolling: touch
|
||||
overscroll-behavior-y: contain;
|
||||
|
||||
// Animation: slide away
|
||||
transition: transform 0.5s
|
||||
@@ -31,16 +32,18 @@
|
||||
|
||||
li a {
|
||||
display: block;
|
||||
padding: 5px 0
|
||||
padding: 0
|
||||
text-decoration: none
|
||||
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) { padding: 5px 0; }
|
||||
&:hover { text-decoration: none }
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 100%
|
||||
height: 3px
|
||||
margin: 10px 0px
|
||||
margin: 5px 0px
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) { margin: 10px 0; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
|
||||
$table-header-bg = lighten($bg, 20%)
|
||||
$table-alternate-bg = lighten($bg, 3%)
|
||||
|
||||
$searchbar-border-color = #848484
|
||||
$searchbar-bg = #424242
|
||||
$searchbar-fg = #fff
|
||||
$searchbar-shadow-color = #d4c89f
|
||||
$searchresults-header-fg = #666
|
||||
$searchresults-border-color = #888
|
||||
$searchresults-li-bg = #252932
|
||||
$search-mark-bg = #e3b171
|
||||
|
||||
@import 'base'
|
||||
|
||||
@@ -84,7 +84,10 @@
|
||||
background-color: $sidebar-bg
|
||||
}
|
||||
|
||||
.content a:link, a:visited, a > .hljs {
|
||||
#searchresults a,
|
||||
.content a:link,
|
||||
a:visited,
|
||||
a > .hljs {
|
||||
color: $links
|
||||
}
|
||||
|
||||
@@ -188,4 +191,32 @@
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: $scrollbar;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
#searchbar {
|
||||
border: 1px solid $searchbar-border-color;
|
||||
border-radius: 3px;
|
||||
background-color: $searchbar-bg;
|
||||
color: $searchbar-fg
|
||||
|
||||
&:focus, &.active {
|
||||
box-shadow: 0 0 3px $searchbar-shadow-color;
|
||||
}
|
||||
}
|
||||
|
||||
.searchresults-header {
|
||||
color: $searchresults-header-fg;
|
||||
}
|
||||
|
||||
.searchresults-outer {
|
||||
border-bottom: 1px dashed $searchresults-border-color;
|
||||
}
|
||||
|
||||
ul#searchresults li.focus {
|
||||
background-color: $searchresults-li-bg;
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: $search-mark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
|
||||
$table-header-bg = lighten($bg, 20%)
|
||||
$table-alternate-bg = lighten($bg, 3%)
|
||||
|
||||
$searchbar-border-color = #aaa
|
||||
$searchbar-bg = #b7b7b7
|
||||
$searchbar-fg = #000
|
||||
$searchbar-shadow-color = #aaa
|
||||
$searchresults-header-fg = #666
|
||||
$searchresults-border-color = #98a3ad
|
||||
$searchresults-li-bg = #2b2b2f
|
||||
$search-mark-bg = #355c7d
|
||||
|
||||
@import 'base'
|
||||
|
||||
@@ -16,7 +16,7 @@ $icons-hover = #333333
|
||||
|
||||
$links = #4183c4
|
||||
|
||||
$inline-code-color = #6e6b5e;
|
||||
$inline-code-color = #6e6b5e
|
||||
|
||||
$theme-popup-bg = #fafafa
|
||||
$theme-popup-border = #cccccc
|
||||
@@ -29,4 +29,13 @@ $table-border-color = darken($bg, 5%)
|
||||
$table-header-bg = darken($bg, 20%)
|
||||
$table-alternate-bg = darken($bg, 3%)
|
||||
|
||||
$searchbar-border-color = #aaa
|
||||
$searchbar-bg = #fafafa
|
||||
$searchbar-fg = #000
|
||||
$searchbar-shadow-color = #aaa
|
||||
$searchresults-header-fg = #666
|
||||
$searchresults-border-color = #888
|
||||
$searchresults-li-bg = #e4f2fe
|
||||
$search-mark-bg = #a2cff5
|
||||
|
||||
@import 'base'
|
||||
|
||||
@@ -29,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
|
||||
$table-header-bg = lighten($bg, 20%)
|
||||
$table-alternate-bg = lighten($bg, 3%)
|
||||
|
||||
$searchbar-border-color = #aaa
|
||||
$searchbar-bg = #aeaec6
|
||||
$searchbar-fg = #000
|
||||
$searchbar-shadow-color = #aaa
|
||||
$searchresults-header-fg = #5f5f71
|
||||
$searchresults-border-color = #5c5c68
|
||||
$searchresults-li-bg = #242430
|
||||
$search-mark-bg = #a2cff5
|
||||
|
||||
@import 'base'
|
||||
|
||||
@@ -29,4 +29,13 @@ $table-border-color = darken($bg, 5%)
|
||||
$table-header-bg = #b3a497
|
||||
$table-alternate-bg = darken($bg, 3%)
|
||||
|
||||
$searchbar-border-color = #aaa
|
||||
$searchbar-bg = #fafafa
|
||||
$searchbar-fg = #000
|
||||
$searchbar-shadow-color = #aaa
|
||||
$searchresults-header-fg = #666
|
||||
$searchresults-border-color = #888
|
||||
$searchresults-li-bg = #dec2a2
|
||||
$search-mark-bg = #e69f67
|
||||
|
||||
@import 'base'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use errors::*;
|
||||
use std::io::Read;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
/// Takes a path to a file and try to read the file into a String
|
||||
pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
@@ -16,6 +16,27 @@ pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Naively replaces any path seperator with a forward-slash '/'
|
||||
pub fn normalize_path(path: &str) -> String {
|
||||
use std::path::is_separator;
|
||||
path.chars()
|
||||
.map(|ch| if is_separator(ch) { '/' } else { ch })
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
/// Write the given data to a file, creating it first if necessary
|
||||
pub fn write_file<P: AsRef<Path>>(
|
||||
build_dir: &Path,
|
||||
filename: P,
|
||||
content: &[u8],
|
||||
) -> Result<()> {
|
||||
let path = build_dir.join(filename);
|
||||
|
||||
create_file(&path)?
|
||||
.write_all(content)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Takes a path and returns a path containing just enough `../` to point to
|
||||
/// the root of the given path.
|
||||
///
|
||||
@@ -38,7 +59,6 @@ pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
/// it doesn't return the correct path.
|
||||
/// Consider [submitting a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
|
||||
/// or a [pull-request](https://github.com/rust-lang-nursery/mdBook/pulls) to improve it.
|
||||
|
||||
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||
debug!("path_to_root");
|
||||
// Remove filename and add "../" for every directory
|
||||
@@ -61,7 +81,6 @@ pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||
/// This function creates a file and returns it. But before creating the file
|
||||
/// it checks every directory in the path to see if it exists,
|
||||
/// and if it does not it will be created.
|
||||
|
||||
pub fn create_file(path: &Path) -> Result<File> {
|
||||
debug!("Creating {}", path.display());
|
||||
|
||||
@@ -76,7 +95,6 @@ pub fn create_file(path: &Path) -> Result<File> {
|
||||
}
|
||||
|
||||
/// Removes all the content of a directory but not the directory itself
|
||||
|
||||
pub fn remove_dir_content(dir: &Path) -> Result<()> {
|
||||
for item in fs::read_dir(dir)? {
|
||||
if let Ok(item) = item {
|
||||
@@ -93,7 +111,6 @@ pub fn remove_dir_content(dir: &Path) -> Result<()> {
|
||||
|
||||
/// Copies all files of a directory to another one except the files
|
||||
/// with the extensions given in the `ext_blacklist` array
|
||||
|
||||
pub fn copy_files_except_ext(
|
||||
from: &Path,
|
||||
to: &Path,
|
||||
@@ -176,14 +193,14 @@ pub fn copy_files_except_ext(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate tempdir;
|
||||
extern crate tempfile;
|
||||
|
||||
use super::copy_files_except_ext;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn copy_files_except_ext_test() {
|
||||
let tmp = match tempdir::TempDir::new("") {
|
||||
let tmp = match tempfile::TempDir::new() {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic!("Could not create a temp dir"),
|
||||
};
|
||||
|
||||
@@ -3,13 +3,71 @@
|
||||
pub mod fs;
|
||||
mod string;
|
||||
use errors::Error;
|
||||
use regex::Regex;
|
||||
|
||||
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
|
||||
OPTION_ENABLE_TABLES};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use self::string::{RangeArgument, take_lines};
|
||||
|
||||
/// Replaces multiple consecutive whitespace characters with a single space character.
|
||||
pub fn collapse_whitespace<'a>(text: &'a str) -> Cow<'a, str> {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"\s\s+").unwrap();
|
||||
}
|
||||
RE.replace_all(text, " ")
|
||||
}
|
||||
|
||||
/// Convert the given string to a valid HTML element ID
|
||||
pub fn normalize_id(content: &str) -> String {
|
||||
let mut ret = content
|
||||
.chars()
|
||||
.filter_map(|ch| {
|
||||
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
|
||||
Some(ch.to_ascii_lowercase())
|
||||
} else if ch.is_whitespace() {
|
||||
Some('-')
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
// Ensure that the first character is [A-Za-z]
|
||||
if ret.chars().next().map_or(false, |c| !c.is_ascii_alphabetic()) {
|
||||
ret.insert(0, 'a');
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
/// Generate an ID for use with anchors which is derived from a "normalised"
|
||||
/// string.
|
||||
pub fn id_from_content(content: &str) -> String {
|
||||
let mut content = content.to_string();
|
||||
|
||||
// Skip any tags or html-encoded stuff
|
||||
const REPL_SUB: &[&str] = &["<em>",
|
||||
"</em>",
|
||||
"<code>",
|
||||
"</code>",
|
||||
"<strong>",
|
||||
"</strong>",
|
||||
"<",
|
||||
">",
|
||||
"&",
|
||||
"'",
|
||||
"""];
|
||||
for sub in REPL_SUB {
|
||||
content = content.replace(sub, "");
|
||||
}
|
||||
|
||||
// Remove spaces and hashes indicating a header
|
||||
let trimmed = content.trim().trim_left_matches('#').trim();
|
||||
|
||||
normalize_id(trimmed)
|
||||
}
|
||||
|
||||
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
|
||||
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
||||
let mut s = String::with_capacity(text.len() * 3 / 2);
|
||||
@@ -212,6 +270,29 @@ more text with spaces
|
||||
}
|
||||
}
|
||||
|
||||
mod html_munging {
|
||||
use super::super::{id_from_content, normalize_id};
|
||||
|
||||
#[test]
|
||||
fn it_generates_anchors() {
|
||||
assert_eq!(id_from_content("## `--passes`: add more rustdoc passes"),
|
||||
"a--passes-add-more-rustdoc-passes");
|
||||
assert_eq!(id_from_content("## Method-call expressions"),
|
||||
"method-call-expressions");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_normalizes_ids() {
|
||||
assert_eq!(normalize_id("`--passes`: add more rustdoc passes"),
|
||||
"a--passes-add-more-rustdoc-passes");
|
||||
assert_eq!(normalize_id("Method-call 🐙 expressions \u{1f47c}"),
|
||||
"method-call--expressions-");
|
||||
assert_eq!(normalize_id("_-_12345"), "a_-_12345");
|
||||
assert_eq!(normalize_id("12345"), "a12345");
|
||||
assert_eq!(normalize_id(""), "");
|
||||
}
|
||||
}
|
||||
|
||||
mod convert_quotes_to_curly {
|
||||
use super::super::convert_quotes_to_curly;
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
//! Integration tests to make sure alternate backends work.
|
||||
|
||||
extern crate mdbook;
|
||||
extern crate tempdir;
|
||||
extern crate tempfile;
|
||||
|
||||
use std::fs::File;
|
||||
#[cfg(not(windows))]
|
||||
use std::path::Path;
|
||||
use tempdir::TempDir;
|
||||
use tempfile::{TempDir, Builder as TempFileBuilder};
|
||||
use mdbook::config::Config;
|
||||
use mdbook::MDBook;
|
||||
use mdbook::renderer::RenderContext;
|
||||
|
||||
#[test]
|
||||
fn passing_alternate_backend() {
|
||||
@@ -39,6 +38,7 @@ fn alternate_backend_with_arguments() {
|
||||
}
|
||||
|
||||
/// Get a command which will pipe `stdin` to the provided file.
|
||||
#[cfg(not(windows))]
|
||||
fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
|
||||
let out_file = out_file.as_ref();
|
||||
|
||||
@@ -52,7 +52,10 @@ fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn backends_receive_render_context_via_stdin() {
|
||||
let temp = TempDir::new("output").unwrap();
|
||||
use std::fs::File;
|
||||
use mdbook::renderer::RenderContext;
|
||||
|
||||
let temp = TempFileBuilder::new().prefix("output").tempdir().unwrap();
|
||||
let out_file = temp.path().join("out.txt");
|
||||
let cmd = tee_command(&out_file);
|
||||
|
||||
@@ -67,7 +70,7 @@ fn backends_receive_render_context_via_stdin() {
|
||||
}
|
||||
|
||||
fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) {
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
|
||||
let mut config = Config::default();
|
||||
config
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Not all features are used in all test crates, so...
|
||||
#![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
|
||||
extern crate mdbook;
|
||||
extern crate tempdir;
|
||||
extern crate tempfile;
|
||||
extern crate walkdir;
|
||||
|
||||
use std::path::Path;
|
||||
@@ -15,7 +15,7 @@ use mdbook::utils::fs::file_to_string;
|
||||
|
||||
// The funny `self::` here is because we've got an `extern crate ...` and are
|
||||
// in a submodule
|
||||
use self::tempdir::TempDir;
|
||||
use self::tempfile::{TempDir, Builder as TempFileBuilder};
|
||||
use self::mdbook::MDBook;
|
||||
use self::walkdir::WalkDir;
|
||||
|
||||
@@ -47,7 +47,7 @@ impl DummyBook {
|
||||
|
||||
/// Write a book to a temporary directory using the provided settings.
|
||||
pub fn build(&self) -> Result<TempDir> {
|
||||
let temp = TempDir::new("dummy_book").chain_err(|| "Unable to create temp directory")?;
|
||||
let temp = TempFileBuilder::new().prefix("dummy_book").tempdir().chain_err(|| "Unable to create temp directory")?;
|
||||
|
||||
let dummy_book_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/dummy_book");
|
||||
recursive_copy(&dummy_book_root, temp.path()).chain_err(|| {
|
||||
@@ -128,11 +128,11 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
|
||||
}
|
||||
|
||||
pub fn new_copy_of_example_book() -> Result<TempDir> {
|
||||
let temp = TempDir::new("book-example")?;
|
||||
let temp = TempFileBuilder::new().prefix("book-example").tempdir()?;
|
||||
|
||||
let book_example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
|
||||
|
||||
recursive_copy(book_example, temp.path())?;
|
||||
|
||||
Ok(temp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,20 @@
|
||||
# Conclusion
|
||||
# Conclusion
|
||||
|
||||
<p>
|
||||
<!--secret secret-->
|
||||
I put <HTML> in here!<br/>
|
||||
</p>
|
||||
<script type="text/javascript" >
|
||||
// I probably shouldn't do this
|
||||
if (3 < 5 > 10)
|
||||
{
|
||||
alert("The sky is falling!");
|
||||
}
|
||||
</script >
|
||||
<style >
|
||||
/*
|
||||
css looks, like this {
|
||||
foo: < 3 <bar >
|
||||
}
|
||||
*/
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
extern crate mdbook;
|
||||
extern crate tempdir;
|
||||
extern crate tempfile;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
use mdbook::MDBook;
|
||||
use mdbook::config::Config;
|
||||
use tempdir::TempDir;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
|
||||
|
||||
/// Run `mdbook init` in an empty directory and make sure the default files
|
||||
@@ -14,7 +14,7 @@ use tempdir::TempDir;
|
||||
fn base_mdbook_init_should_create_default_content() {
|
||||
let created_files = vec!["book", "src", "src/SUMMARY.md", "src/chapter_1.md"];
|
||||
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
for file in &created_files {
|
||||
assert!(!temp.path().join(file).exists());
|
||||
}
|
||||
@@ -34,7 +34,7 @@ fn base_mdbook_init_should_create_default_content() {
|
||||
fn run_mdbook_init_with_custom_book_and_src_locations() {
|
||||
let created_files = vec!["out", "in", "in/SUMMARY.md", "in/chapter_1.md"];
|
||||
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
for file in &created_files {
|
||||
assert!(
|
||||
!temp.path().join(file).exists(),
|
||||
@@ -61,7 +61,7 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
|
||||
|
||||
#[test]
|
||||
fn book_toml_isnt_required() {
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
let md = MDBook::init(temp.path()).build().unwrap();
|
||||
|
||||
let _ = fs::remove_file(temp.path().join("book.toml"));
|
||||
|
||||
@@ -2,7 +2,7 @@ extern crate mdbook;
|
||||
#[macro_use]
|
||||
extern crate pretty_assertions;
|
||||
extern crate select;
|
||||
extern crate tempdir;
|
||||
extern crate tempfile;
|
||||
extern crate walkdir;
|
||||
|
||||
mod dummy_book;
|
||||
@@ -16,7 +16,7 @@ use std::ffi::OsStr;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
use select::document::Document;
|
||||
use select::predicate::{Class, Name, Predicate};
|
||||
use tempdir::TempDir;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils::fs::file_to_string;
|
||||
use mdbook::config::Config;
|
||||
@@ -324,7 +324,7 @@ fn example_book_can_build() {
|
||||
|
||||
#[test]
|
||||
fn book_with_a_reserved_filename_does_not_build() {
|
||||
let tmp_dir = TempDir::new("mdBook").unwrap();
|
||||
let tmp_dir = TempFileBuilder::new().prefix("mdBook").tempdir().unwrap();
|
||||
let src_path = tmp_dir.path().join("src");
|
||||
fs::create_dir(&src_path).unwrap();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2160
tests/searchindex_fixture.json
Normal file
2160
tests/searchindex_fixture.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user