mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 17:21:52 -05:00
Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91ffca1bbc | ||
|
|
6f963bbe3c | ||
|
|
93af92910a | ||
|
|
f30ce0184d | ||
|
|
ccb2340fbe | ||
|
|
bbe6e324d0 | ||
|
|
a776aa9783 | ||
|
|
b8f8e76899 | ||
|
|
ac4e00c7c6 | ||
|
|
67fde37030 | ||
|
|
b2eb1ace08 | ||
|
|
b5fd170008 | ||
|
|
b3665c287d | ||
|
|
436c084b9e | ||
|
|
47f85e71a8 | ||
|
|
1d448fc8cc | ||
|
|
add23a43c2 | ||
|
|
8ba1830750 | ||
|
|
76c1c9e0a8 | ||
|
|
d054140117 | ||
|
|
512826c465 | ||
|
|
99019b74aa | ||
|
|
d87e77edd0 | ||
|
|
abfc3009fc | ||
|
|
028c8b0f75 | ||
|
|
05f3c693a7 | ||
|
|
8b3038e3ef | ||
|
|
bc432c8f42 | ||
|
|
e88970d172 | ||
|
|
4c87a0b5f0 | ||
|
|
ac38f05bb6 | ||
|
|
3119a7e4bf | ||
|
|
cc745d04f2 | ||
|
|
d1a23109e2 | ||
|
|
b3e0942bc9 | ||
|
|
3e998eb766 | ||
|
|
2bbabdcd62 | ||
|
|
a287a0dcc8 | ||
|
|
e7afb3340c | ||
|
|
b4e15e5357 | ||
|
|
1d1d4d7c30 | ||
|
|
35c2d1ff91 | ||
|
|
fd9d27e082 | ||
|
|
0e1787c617 | ||
|
|
e5563182fc | ||
|
|
a08255316a | ||
|
|
ebea8c2337 | ||
|
|
a7342230d5 | ||
|
|
15b18a682b | ||
|
|
a8590928cf | ||
|
|
c9a9987aec | ||
|
|
775377acce | ||
|
|
5dd0496a4f | ||
|
|
f300a21a47 | ||
|
|
0f89abb1c7 | ||
|
|
b5e32f57dc | ||
|
|
b88abb171c | ||
|
|
7c8dd5085b | ||
|
|
4f793af53b | ||
|
|
29b3ff14c7 | ||
|
|
0ac36f2183 | ||
|
|
5835da2432 | ||
|
|
646e3f8fd2 | ||
|
|
da9be67516 | ||
|
|
d9dbba49ea | ||
|
|
384582aeba | ||
|
|
e94078cc9c | ||
|
|
e1a46d213e | ||
|
|
62c8311301 | ||
|
|
b8011de3e8 | ||
|
|
019e74041d | ||
|
|
8cd7061ff2 | ||
|
|
96b99472fd | ||
|
|
4d357b6779 | ||
|
|
1e6328c112 | ||
|
|
21c24c2815 | ||
|
|
bb8b43d396 | ||
|
|
f07e734efc | ||
|
|
db2c16102e | ||
|
|
cae8a8ffe2 | ||
|
|
bdb37ec117 | ||
|
|
01656b610f | ||
|
|
b9ff0e8a77 | ||
|
|
0bda57175d | ||
|
|
374e1d3f94 | ||
|
|
6287e6a44f | ||
|
|
953d3821b6 | ||
|
|
488ace15ff | ||
|
|
b452d5e0c7 | ||
|
|
289028850f | ||
|
|
2a55ff62f3 | ||
|
|
6bf86806e4 | ||
|
|
90bd7207ec | ||
|
|
27b29fdaf2 | ||
|
|
154e0fb308 | ||
|
|
0de177a344 | ||
|
|
f154b2fb65 | ||
|
|
d7759fbf4d | ||
|
|
f84e670edd | ||
|
|
9a9c625319 | ||
|
|
b9ca108fca | ||
|
|
e99dc51fb3 | ||
|
|
7ee5b6643b | ||
|
|
42781bcd6b | ||
|
|
41d372de26 | ||
|
|
69599646e7 | ||
|
|
69fef40e57 | ||
|
|
a323620e02 | ||
|
|
ea0b835b38 | ||
|
|
58f0f3b0f2 | ||
|
|
e7a61efb39 | ||
|
|
d48bc29373 | ||
|
|
72f154bee4 | ||
|
|
1c71eaa964 | ||
|
|
c195aa990d | ||
|
|
34bdcaf8b3 | ||
|
|
41399fc29c | ||
|
|
7f82a197b9 | ||
|
|
71d44933f0 | ||
|
|
f01bf88e69 | ||
|
|
b5ea84c60d | ||
|
|
148c806e34 | ||
|
|
38279deed7 | ||
|
|
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
|
||||
|
||||
77
.travis.yml
77
.travis.yml
@@ -1,87 +1,44 @@
|
||||
# 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:
|
||||
timeout: 360
|
||||
cargo: true
|
||||
|
||||
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
|
||||
|
||||
after_success:
|
||||
- bash ci/github_pages.sh
|
||||
- cargo build --all --no-default-features
|
||||
- cargo build --verbose
|
||||
- cargo test --verbose
|
||||
|
||||
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
|
||||
- master
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
|
||||
notifications:
|
||||
email:
|
||||
|
||||
@@ -48,30 +48,6 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
|
||||
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
|
||||
|
||||
|
||||
### Making changes to the style
|
||||
|
||||
mdBook doesn't use CSS directly but uses [Stylus](http://stylus-lang.com/), a CSS-preprocessor which compiles to CSS.
|
||||
|
||||
When you want to change the style, it is important to not change the CSS directly because any manual modification to
|
||||
the CSS files will be overwritten when compiling the stylus files. Instead, you should make your changes directly in the
|
||||
[stylus files](https://github.com/rust-lang-nursery/mdBook/tree/master/src/theme/stylus) and regenerate the CSS.
|
||||
|
||||
For this to work, you first need [Node and NPM](https://nodejs.org/en/) installed on your machine.
|
||||
Then run the following command to install both [stylus](http://stylus-lang.com/) and [nib](https://tj.github.io/nib/), you might need `sudo` to install successfully.
|
||||
|
||||
```
|
||||
npm install -g stylus nib
|
||||
```
|
||||
|
||||
When that finished, you can simply regenerate the CSS files by building mdBook with the following command:
|
||||
|
||||
```
|
||||
cargo build --features=regenerate-css
|
||||
```
|
||||
|
||||
This should automatically call the appropriate stylus command to recompile the files to CSS and include them in the project.
|
||||
|
||||
### Making a pull-request
|
||||
|
||||
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
|
||||
|
||||
1571
Cargo.lock
generated
Normal file
1571
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
Cargo.toml
56
Cargo.toml
@@ -1,72 +1,66 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.1.2"
|
||||
authors = ["Mathieu David <mathieudavid@mathieudavid.org>", "Michael-F-Bryan <michaelfbryan@gmail.com>"]
|
||||
description = "create books from markdown files (like Gitbook)"
|
||||
version = "0.2.0"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
"Matt Ickstadt <mattico8@gmail.com>"
|
||||
]
|
||||
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"]
|
||||
license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
build = "build.rs"
|
||||
exclude = [
|
||||
"book-example/*",
|
||||
"src/theme/stylus/**",
|
||||
]
|
||||
|
||||
[package.metadata.release]
|
||||
sign-commit = true
|
||||
push-remote = "upstream"
|
||||
tag-prefix = "v"
|
||||
exclude = ["book-example/*"]
|
||||
|
||||
[dependencies]
|
||||
clap = "2.24"
|
||||
chrono = "0.4"
|
||||
handlebars = "0.29"
|
||||
handlebars = "0.32"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
error-chain = "0.11"
|
||||
error-chain = "0.12"
|
||||
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"
|
||||
env_logger = "0.5"
|
||||
toml = "0.4"
|
||||
memchr = "2.0"
|
||||
open = "1.1"
|
||||
regex = "0.2.1"
|
||||
tempdir = "0.3.4"
|
||||
regex = "1.0.0"
|
||||
tempfile = "3.0"
|
||||
itertools = "0.7"
|
||||
shlex = "0.1"
|
||||
toml-query = "0.6"
|
||||
toml-query = "0.7"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "4.0", optional = true }
|
||||
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}
|
||||
|
||||
[build-dependencies]
|
||||
error-chain = "0.11"
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
||||
ammonia = { version = "1.1", optional = true }
|
||||
|
||||
[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"]
|
||||
watch = ["notify"]
|
||||
serve = ["iron", "staticfile", "ws"]
|
||||
search = ["elasticlunr-rs", "ammonia"]
|
||||
|
||||
[[bin]]
|
||||
doc = false
|
||||
name = "mdbook"
|
||||
path = "src/bin/mdbook.rs"
|
||||
|
||||
10
README.md
10
README.md
@@ -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,8 +40,8 @@ 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
|
||||
|
||||
This requires at least [Rust] 1.20 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
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
environment:
|
||||
global:
|
||||
PROJECT_NAME: mdBook
|
||||
nodejs_version: "6"
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
@@ -32,17 +31,12 @@ install:
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -Vv
|
||||
- cargo -V
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- node --version
|
||||
- npm --version
|
||||
- npm install -g stylus nib
|
||||
|
||||
build: false
|
||||
|
||||
# Equivalent to Travis' `script` phase
|
||||
test_script:
|
||||
- cargo build --verbose
|
||||
- cargo build --verbose --features=regenerate-css
|
||||
- cargo test --verbose
|
||||
|
||||
before_deploy:
|
||||
@@ -67,3 +61,4 @@ deploy:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
# Summary
|
||||
|
||||
- [mdBook](README.md)
|
||||
- [Command Line Tool](cli/cli-tool.md)
|
||||
- [Command Line Tool](cli/README.md)
|
||||
- [init](cli/init.md)
|
||||
- [build](cli/build.md)
|
||||
- [watch](cli/watch.md)
|
||||
- [serve](cli/serve.md)
|
||||
- [test](cli/test.md)
|
||||
- [Format](format/format.md)
|
||||
- [clean](cli/clean.md)
|
||||
- [Format](format/README.md)
|
||||
- [SUMMARY.md](format/summary.md)
|
||||
- [Configuration](format/config.md)
|
||||
- [Theme](format/theme/theme.md)
|
||||
- [Theme](format/theme/README.md)
|
||||
- [index.hbs](format/theme/index-hbs.md)
|
||||
- [Syntax highlighting](format/theme/syntax-highlighting.md)
|
||||
- [Editor](format/theme/editor.md)
|
||||
- [MathJax Support](format/mathjax.md)
|
||||
- [mdBook specific features](format/mdbook.md)
|
||||
- [For Developers](for_developers/index.md)
|
||||
- [Continuous Integration](continuous-integration.md)
|
||||
- [For Developers](for_developers/README.md)
|
||||
- [Preprocessors](for_developers/preprocessors.md)
|
||||
- [Alternate Backends](for_developers/backends.md)
|
||||
|
||||
-----------
|
||||
|
||||
[Contributors](misc/contributors.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.
|
||||
The `build` command can take a directory as an argument to use as the book's
|
||||
root instead of the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook build path/to/book
|
||||
@@ -23,13 +23,16 @@ mdbook build path/to/book
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) option, mdbook will open the rendered book in
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the rendered book in
|
||||
your default web browser after building it.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for
|
||||
the book. If not specified it will default to the value of the
|
||||
`build.build-dir` key in `book.toml`, or to `./book` relative to the book's
|
||||
root directory.
|
||||
|
||||
-------------------
|
||||
|
||||
***note:*** *make sure to run the build command in the root directory and not in the source directory*
|
||||
***Note:*** *Make sure to run the build command in the root directory and not in the source directory*
|
||||
|
||||
30
book-example/src/cli/clean.md
Normal file
30
book-example/src/cli/clean.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# The clean command
|
||||
|
||||
The clean command is used to delete the generated book and any other build
|
||||
artifacts.
|
||||
|
||||
```bash
|
||||
mdbook clean
|
||||
```
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
The `clean` command can take a directory as an argument to use as the book's
|
||||
root instead of the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook clean path/to/book
|
||||
```
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to override the book's output
|
||||
directory, which will be deleted by this command. If not specified it
|
||||
will default to the value of the `build.build-dir` key in `book.toml`, or to
|
||||
`./book` relative to the book's root directory.
|
||||
|
||||
```bash
|
||||
mdbook clean --dest-dir=path/to/book
|
||||
```
|
||||
|
||||
`path/to/book` could be absolute or relative.
|
||||
@@ -1,5 +1,7 @@
|
||||
# The init command
|
||||
There is some minimal boilerplate that is the same for every new book. It's for this purpose that mdBook includes an `init` command.
|
||||
|
||||
There is some minimal boilerplate that is the same for every new book. It's for this purpose
|
||||
that mdBook includes an `init` command.
|
||||
|
||||
The `init` command is used like this:
|
||||
|
||||
@@ -22,23 +24,27 @@ configuration files, etc.
|
||||
- The `book` directory is where your book is rendered. All the output is ready to be uploaded
|
||||
to a server to be seen by your audience.
|
||||
|
||||
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is discussed in more detail in another [chapter](format/summary.html).
|
||||
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is
|
||||
discussed in more detail [in another chapter](../format/summary.md)
|
||||
|
||||
#### Tip & Trick: Hidden Feature
|
||||
When a `SUMMARY.md` file already exists, the `init` command will first parse it and generate the missing files according to the paths used in the `SUMMARY.md`. This allows you to think and create the whole structure of your book and then let mdBook generate it for you.
|
||||
#### Tip: Generate chapters from SUMMARY.md
|
||||
|
||||
When a `SUMMARY.md` file already exists, the `init` command will first parse it and generate the
|
||||
missing files according to the paths used in the `SUMMARY.md`. This allows you to think and create
|
||||
the whole structure of your book and then let mdBook generate it for you.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
When using the `init` command, you can also specify a directory, instead of using the current working directory,
|
||||
by appending a path to the command:
|
||||
The `init` command can take a directory as an argument to use as the book's
|
||||
root instead of the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook init path/to/book
|
||||
```
|
||||
|
||||
## --theme
|
||||
#### --theme
|
||||
|
||||
When you use the `--theme` argument, the default theme will be copied into a directory
|
||||
When you use the `--theme` flag, the default theme will be copied into a directory
|
||||
called `theme` in your source directory so that you can modify it.
|
||||
|
||||
The theme is selectively overwritten, this means that if you don't want to overwrite a
|
||||
|
||||
@@ -1,40 +1,50 @@
|
||||
# The serve command
|
||||
|
||||
The `serve` command is useful when you want to preview your book. It also does hot reloading of the webpage whenever a file changes.
|
||||
It achieves this by serving the books content over `localhost:3000` (unless otherwise configured, see below) and runs a websocket server on `localhost:3001` which triggers the reloads.
|
||||
This preferred by many for writing books with mdbook because it allows for you to see the result of your work instantly after every file change.
|
||||
The serve command is used to preview a book by serving it over HTTP at
|
||||
`localhost:3000` by default. Additionally it watches the book's directory
|
||||
for changes, rebuilding the book and refreshing clients for each change.
|
||||
A websocket connection is used to trigger the client-side refresh.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
Like `watch`, `serve` can take a directory as argument to use instead of the
|
||||
current working directory.
|
||||
The `serve` command can take a directory as an argument to use as the book's
|
||||
root instead of the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook serve path/to/book
|
||||
```
|
||||
|
||||
|
||||
#### Server options
|
||||
|
||||
`serve` has four options: the http port, the websocket port, the interface to serve on, and the public address of the server so that the browser may reach the websocket server.
|
||||
`serve` has four options: the HTTP port, the WebSocket port, the HTTP hostname
|
||||
to listen on, and the hostname for the browser to connect to for WebSockets.
|
||||
|
||||
For example: suppose you had an nginx server for SSL termination which has a public address of 192.168.1.100 on port 80 and proxied that to 127.0.0.1 on port 8000. To run use the nginx proxy do:
|
||||
For example: suppose you have an nginx server for SSL termination which has a
|
||||
public address of 192.168.1.100 on port 80 and proxied that to 127.0.0.1 on
|
||||
port 8000. To run use the nginx proxy do:
|
||||
|
||||
```bash
|
||||
mdbook serve path/to/book -p 8000 -i 127.0.0.1 -a 192.168.1.100
|
||||
mdbook serve path/to/book -p 8000 -n 127.0.0.1 --websocket-hostname 192.168.1.100
|
||||
```
|
||||
|
||||
If you were to want live reloading for this you would need to proxy the websocket calls through nginx as well from `192.168.1.100:<WS_PORT>` to `127.0.0.1:<WS_PORT>`. The `-w` flag allows for the websocket port to be configured.
|
||||
If you were to want live reloading for this you would need to proxy the
|
||||
websocket calls through nginx as well from `192.168.1.100:<WS_PORT>` to
|
||||
`127.0.0.1:<WS_PORT>`. The `-w` flag allows for the websocket port to be
|
||||
configured.
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) option, mdbook will open the book in your
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the book in your
|
||||
your default web browser after starting the server.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for
|
||||
the book. If not specified it will default to the value of the
|
||||
`build.build-dir` key in `book.toml`, or to `./book` relative to the book's
|
||||
root directory.
|
||||
|
||||
-----
|
||||
|
||||
***note:*** *the `serve` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/rust-lang-nursery/mdBook/issues)*
|
||||
***Note:*** *The `serve` command is for testing, and is not intended
|
||||
to be a complete HTTP server for a website.*
|
||||
|
||||
@@ -1,19 +1,53 @@
|
||||
# The test command
|
||||
|
||||
When writing a book, you sometimes need to automate some tests. For example, [The Rust Programming Book](https://doc.rust-lang.org/stable/book/) uses a lot of code examples that could get outdated.
|
||||
Therefore it is very important for them to be able to automatically test these code examples.
|
||||
When writing a book, you sometimes need to automate some tests. For example,
|
||||
[The Rust Programming Book](https://doc.rust-lang.org/stable/book/) uses a lot
|
||||
of code examples that could get outdated. Therefore it is very important for
|
||||
them to be able to automatically test these code examples.
|
||||
|
||||
mdBook supports a `test` command that will run all available tests in mdBook. At the moment, only one test is available:
|
||||
*"Test Rust code examples using Rustdoc"*, but I hope this will be expanded in the future to include more tests like:
|
||||
mdBook supports a `test` command that will run all available tests in a book.
|
||||
At the moment, only rustdoc tests are supported, but this may be expanded upon
|
||||
in the future.
|
||||
|
||||
- checking for broken links
|
||||
- checking for unused files
|
||||
- ...
|
||||
#### Disable tests on a code block
|
||||
|
||||
In the future I would like the user to be able to enable / disable test from the `book.toml` configuration file and support custom tests.
|
||||
rustdoc doesn't test code blocks which contain the `ignore` attribute:
|
||||
|
||||
```rust,ignore
|
||||
fn main() {}
|
||||
```
|
||||
|
||||
rustdoc also doesn't test code blocks which specify a language other than Rust:
|
||||
|
||||
```markdown
|
||||
**Foo**: _bar_
|
||||
```
|
||||
|
||||
rustdoc *does* test code blocks which have no language specified:
|
||||
|
||||
```
|
||||
This is going to cause an error!
|
||||
```
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
The `test` command can take a directory as an argument to use as the book's
|
||||
root instead of the current working directory.
|
||||
|
||||
**How to use it:**
|
||||
```bash
|
||||
$ mdbook test
|
||||
[*]: Testing file: "/mdBook/book-example/src/README.md”
|
||||
mdbook test path/to/book
|
||||
```
|
||||
|
||||
#### --library-path
|
||||
|
||||
The `--library-path` (`-L`) option allows you to add directories to the library
|
||||
search path used by `rustdoc` when it builds and tests the examples. Multiple
|
||||
directories can be specified with multiple options (`-L foo -L bar`) or with a
|
||||
comma-delimited list (`-L foo,bar`).
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for
|
||||
the book. If not specified it will default to the value of the
|
||||
`build.build-dir` key in `book.toml`, or to `./book` relative to the book's
|
||||
root directory.
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# The watch command
|
||||
|
||||
The `watch` command is useful when you want your book to be rendered on every file change.
|
||||
You could repeatedly issue `mdbook build` every time a file is changed. But using `mdbook watch` once will watch your files and will trigger a build automatically whenever you modify a file.
|
||||
The `watch` command is useful when you want your book to be rendered on every
|
||||
file change. You could repeatedly issue `mdbook build` every time a file is
|
||||
changed. But using `mdbook watch` once will watch your files and will trigger
|
||||
a build automatically whenever you modify a file.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
Like `init` and `build`, `watch` can take a directory as argument to use instead of the
|
||||
current working directory.
|
||||
The `watch` command can take a directory as an argument to use as the book's
|
||||
root instead of the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook watch path/to/book
|
||||
@@ -19,8 +21,7 @@ your default web browser.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
|
||||
|
||||
-----
|
||||
|
||||
***note:*** *the `watch` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/rust-lang-nursery/mdBook/issues)*
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for
|
||||
the book. If not specified it will default to the value of the
|
||||
`build.build-dir` key in `book.toml`, or to `./book` relative to the book's
|
||||
root directory.
|
||||
|
||||
52
book-example/src/continuous-integration.md
Normal file
52
book-example/src/continuous-integration.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Running `mdbook` in Continuous Integration
|
||||
|
||||
While the following examples use Travis CI, their principles should
|
||||
straightforwardly transfer to other continuous integration providers as well.
|
||||
|
||||
## Ensuring Your Book Builds and Tests Pass
|
||||
|
||||
Here is a sample Travis CI `.travis.yml` configuration that ensures `mdbook
|
||||
build` and `mdbook test` run successfully. The key to fast CI turnaround times
|
||||
is caching `mdbook` installs, so that you aren't compiling `mdbook` on every CI
|
||||
run.
|
||||
|
||||
```yaml
|
||||
language: rust
|
||||
sudo: false
|
||||
|
||||
cache:
|
||||
- cargo
|
||||
|
||||
rust:
|
||||
- stable
|
||||
|
||||
before_script:
|
||||
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
|
||||
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.1" mdbook)
|
||||
- cargo install-update -a
|
||||
|
||||
script:
|
||||
- cd path/to/mybook && mdbook build && mdbook test
|
||||
```
|
||||
|
||||
## Deploying Your Book to GitHub Pages
|
||||
|
||||
Following these instructions will result in your book being published to GitHub
|
||||
pages after a successful CI run on your repository's `master` branch.
|
||||
|
||||
First, create a new GitHub "Personal Access Token" with the "public_repo" permissions (or "repo" for private repositories). Go to your repository's Travis CI settings page and add an environment variable named `GITHUB_TOKEN` that is marked secure and *not* shown in the logs.
|
||||
|
||||
Then, append this snippet to your `.travis.yml` and update the path to the `book` directory:
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
provider: pages
|
||||
skip-cleanup: true
|
||||
github-token: $GITHUB_TOKEN
|
||||
local-dir: path/to/mybook/book
|
||||
keep-history: false
|
||||
on:
|
||||
branch: master
|
||||
```
|
||||
|
||||
That's it!
|
||||
@@ -11,8 +11,8 @@ The *For Developers* chapters are here to show you the more advanced usage of
|
||||
|
||||
The two main ways a developer can hook into the book's build process is via,
|
||||
|
||||
- [Preprocessors](for_developers/preprocessors.html)
|
||||
- [Alternate Backends](for_developers/backends.html)
|
||||
- [Preprocessors](preprocessors.md)
|
||||
- [Alternate Backends](backends.md)
|
||||
|
||||
|
||||
## The Build Process
|
||||
@@ -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
|
||||
@@ -55,12 +58,21 @@ This controls the build process of your book.
|
||||
will be created when the book is built (i.e. `create-missing = true`). If this
|
||||
is `false` then the build process will instead exit with an error if any files
|
||||
do not exist.
|
||||
- **preprocess:** Specify which preprocessors to be applied. Default is `["links", "index"]`. To disable default preprocessors, pass an empty array `[]` in.
|
||||
|
||||
|
||||
The following preprocessors are available and included by default:
|
||||
|
||||
- `links`: Expand the `{{# playpen}}` and `{{# include}}` handlebars helpers in a chapter.
|
||||
- `index`: Convert all chapter files named `README.md` into `index.md`. That is to say, all `README.md` would be rendered to an index file `index.html` in the rendered book.
|
||||
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
[build]
|
||||
build-dir = "build"
|
||||
create-missing = false
|
||||
preprocess = ["links", "index"]
|
||||
```
|
||||
|
||||
### HTML renderer options
|
||||
@@ -81,20 +93,60 @@ 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:
|
||||
|
||||
- **enable:** Enables the search feature. Defaults to `true`.
|
||||
- **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"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
[build]
|
||||
build-dir = "book"
|
||||
create-missing = true
|
||||
preprocess = ["links", "index"]
|
||||
|
||||
[output.html]
|
||||
theme = "my-theme"
|
||||
curly-quotes = true
|
||||
@@ -105,12 +157,25 @@ 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
|
||||
copy-js = true
|
||||
```
|
||||
|
||||
|
||||
## 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 +210,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.
|
||||
|
||||
@@ -9,9 +9,13 @@ To enable MathJax, you need to add the `mathjax-support` key to your `book.toml`
|
||||
mathjax-support = true
|
||||
```
|
||||
|
||||
>**Note:**
|
||||
>**Note:**
|
||||
The usual delimiters MathJax uses are not yet supported. You can't currently use `$$ ... $$` as delimiters and the `\[ ... \]` delimiters need an extra backslash to work. Hopefully this limitation will be lifted soon.
|
||||
|
||||
>**Note:**
|
||||
> When you use double backslashes in MathJax blocks (for example in commands such as `\begin{cases} \frac 1 2 \\ \frac 3 4 \end{cases}`) you need to add _two extra_ backslashes (e.g., `\begin{cases} \frac 1 2 \\\\ \frac 3 4 \end{cases}`).
|
||||
|
||||
|
||||
### Inline equations
|
||||
Inline equations are delimited by `\\(` and `\\)`. So for example, to render the following inline equation \\( \int x dx = \frac{x^2}{2} + C \\) you would write the following:
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Since the original directory structure is maintained, it is useful to prepend re
|
||||
|
||||
In addition to the properties you can access, there are some handlebars helpers at your disposal.
|
||||
|
||||
1. ### toc
|
||||
### 1. toc
|
||||
|
||||
The toc helper is used like this
|
||||
|
||||
@@ -68,7 +68,7 @@ In addition to the properties you can access, there are some handlebars helpers
|
||||
</script>
|
||||
```
|
||||
|
||||
2. ### previous / next
|
||||
### 2. previous / next
|
||||
|
||||
The previous and next helpers expose a `link` and `name` property to the previous and next chapters.
|
||||
|
||||
@@ -87,5 +87,4 @@ In addition to the properties you can access, there are some handlebars helpers
|
||||
|
||||
------
|
||||
|
||||
*If you would like me to expose other properties or helpers, please [create a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
|
||||
and I will consider it.*
|
||||
*If you would like other properties or helpers exposed, please [create a new issue](https://github.com/rust-lang-nursery/mdBook/issues)*
|
||||
|
||||
@@ -14,3 +14,6 @@ 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)
|
||||
- Weihang Lo ([@weihanglo](https://github.com/weihanglo))
|
||||
|
||||
98
build.rs
98
build.rs
@@ -1,98 +0,0 @@
|
||||
// build.rs
|
||||
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod execs {
|
||||
use std::process::Command;
|
||||
|
||||
pub fn cmd(program: &str) -> Command {
|
||||
let mut cmd = Command::new("cmd");
|
||||
cmd.args(&["/c", program]);
|
||||
cmd
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
mod execs {
|
||||
use std::process::Command;
|
||||
|
||||
pub fn cmd(program: &str) -> Command {
|
||||
Command::new(program)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
error_chain!{
|
||||
foreign_links {
|
||||
Io(std::io::Error);
|
||||
}
|
||||
}
|
||||
|
||||
fn program_exists(program: &str) -> Result<()> {
|
||||
execs::cmd(program).arg("-v")
|
||||
.output()
|
||||
.chain_err(|| format!("Please install '{}'!", program))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn npm_package_exists(package: &str) -> Result<()> {
|
||||
let status = execs::cmd("npm").args(&["list", "-g"])
|
||||
.arg(package)
|
||||
.output();
|
||||
|
||||
match status {
|
||||
Ok(ref out) if out.status.success() => Ok(()),
|
||||
_ => {
|
||||
bail!("Missing npm package '{0}' install with: 'npm -g install {0}'",
|
||||
package)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Resource<'a> {
|
||||
Program(&'a str),
|
||||
Package(&'a str),
|
||||
}
|
||||
use Resource::{Package, Program};
|
||||
|
||||
impl<'a> Resource<'a> {
|
||||
pub fn exists(&self) -> Result<()> {
|
||||
match *self {
|
||||
Program(name) => program_exists(name),
|
||||
Package(name) => npm_package_exists(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") {
|
||||
// Check dependencies
|
||||
Program("npm").exists()?;
|
||||
Program("node").exists().or(Program("nodejs").exists())?;
|
||||
Package("nib").exists()?;
|
||||
Package("stylus").exists()?;
|
||||
|
||||
// Compile stylus stylesheet to css
|
||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR")
|
||||
.chain_err(|| "Please run the script with: 'cargo build'!")?;
|
||||
let theme_dir = Path::new(&manifest_dir).join("src/theme/");
|
||||
let stylus_dir = theme_dir.join("stylus/book.styl");
|
||||
|
||||
if !execs::cmd("stylus").arg(stylus_dir)
|
||||
.arg("--out")
|
||||
.arg(theme_dir)
|
||||
.arg("--use")
|
||||
.arg("nib")
|
||||
.status()?
|
||||
.success()
|
||||
{
|
||||
bail!("Stylus encountered an error");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
quick_main!(run);
|
||||
@@ -15,9 +15,10 @@ main() {
|
||||
;;
|
||||
esac
|
||||
|
||||
test -f Cargo.lock || cargo generate-lockfile
|
||||
# This will slow down the build, but is necessary to not run out of disk space
|
||||
cargo clean
|
||||
|
||||
cross rustc --bin mdbook --target $TARGET --release -- -C lto
|
||||
cargo rustc --bin mdbook --target $TARGET --release -- -C lto
|
||||
|
||||
cp target/$TARGET/release/mdbook $stage/
|
||||
|
||||
@@ -28,4 +29,4 @@ main() {
|
||||
rm -rf $stage
|
||||
}
|
||||
|
||||
main
|
||||
main
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Deploys the `book-example` to GitHub Pages
|
||||
|
||||
set -ex
|
||||
|
||||
# Only run this on the master branch for stable
|
||||
if [ "$TRAVIS_PULL_REQUEST" != "false" ] ||
|
||||
[ "$TRAVIS_BRANCH" != "master" ] ||
|
||||
[ "$TRAVIS_RUST_VERSION" != "stable" ] ||
|
||||
[ "$TARGET" != "x86_64-unknown-linux-gnu" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Make sure we have the css dependencies
|
||||
npm install -g stylus nib
|
||||
|
||||
NC='\033[39m'
|
||||
CYAN='\033[36m'
|
||||
GREEN='\033[32m'
|
||||
|
||||
rev=$(git rev-parse --short HEAD)
|
||||
|
||||
echo -e "${CYAN}Running cargo doc${NC}"
|
||||
cargo doc --features regenerate-css > /dev/null
|
||||
|
||||
echo -e "${CYAN}Running mdbook build${NC}"
|
||||
cargo run -- build book-example/
|
||||
|
||||
echo -e "${CYAN}Copying book to target/doc${NC}"
|
||||
cp -R book-example/book/* target/doc/
|
||||
|
||||
cd target/doc
|
||||
|
||||
echo -e "${CYAN}Initializing Git${NC}"
|
||||
git init
|
||||
git config user.name "Michael Bryan"
|
||||
git config user.email "michaelfbryan@gmail.com"
|
||||
|
||||
git remote add upstream "https://$GH_TOKEN@github.com/rust-lang-nursery/mdBook.git"
|
||||
git fetch upstream --quiet
|
||||
git reset upstream/gh-pages --quiet
|
||||
|
||||
touch .
|
||||
|
||||
echo -e "${CYAN}Pushing changes to gh-pages${NC}"
|
||||
git add -A .
|
||||
git commit -m "rebuild pages at ${rev}" --quiet
|
||||
git push -q upstream HEAD:gh-pages --quiet
|
||||
|
||||
echo -e "${GREEN}Deployed docs to GitHub Pages${NC}"
|
||||
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::book::{Book, BookItem, Chapter};
|
||||
use mdbook::errors::{Error, Result};
|
||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
||||
use mdbook::MDBook;
|
||||
use pulldown_cmark::{Event, Parser, Tag};
|
||||
use pulldown_cmark_to_cmark::fmt::cmark;
|
||||
|
||||
use std::env::{args, args_os};
|
||||
use std::ffi::OsString;
|
||||
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)))
|
||||
}
|
||||
}
|
||||
3
release.toml
Normal file
3
release.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
sign-commit = true
|
||||
push-remote = "origin"
|
||||
tag-prefix = "v"
|
||||
@@ -1,7 +0,0 @@
|
||||
array_layout = "Visual"
|
||||
chain_indent = "Visual"
|
||||
fn_args_layout = "Visual"
|
||||
fn_call_style = "Visual"
|
||||
format_strings = true
|
||||
generics_indent = "Visual"
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use get_book_dir;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("test")
|
||||
.about("Test that code samples compile")
|
||||
.arg_from_usage(
|
||||
"-L, --library-path [DIR]... 'directory to add to crate search path'",
|
||||
)
|
||||
}
|
||||
|
||||
// test command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let library_paths: Vec<&str> = args.values_of("library-path")
|
||||
.map(|v| v.collect())
|
||||
.unwrap_or_default();
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
book.test(library_paths)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
113
src/book/book.rs
113
src/book/book.rs
@@ -1,8 +1,8 @@
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
use config::BuildConfig;
|
||||
@@ -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,8 +297,8 @@ impl Display for Chapter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempdir::TempDir;
|
||||
use std::io::Write;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
const DUMMY_SRC: &'static str = "
|
||||
# Dummy Chapter
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -373,14 +404,13 @@ And here is some \
|
||||
..Default::default()
|
||||
};
|
||||
let should_be = Book {
|
||||
sections: vec![
|
||||
BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
path: PathBuf::from("chapter_1.md"),
|
||||
..Default::default()
|
||||
}),
|
||||
],
|
||||
sections: vec![BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
path: PathBuf::from("chapter_1.md"),
|
||||
..Default::default()
|
||||
})],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got = load_book_from_disk(&summary, temp.path()).unwrap();
|
||||
@@ -399,6 +429,7 @@ And here is some \
|
||||
}),
|
||||
BookItem::Separator,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let should_be: Vec<_> = book.sections.iter().collect();
|
||||
@@ -417,22 +448,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 +499,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();
|
||||
@@ -494,13 +533,11 @@ And here is some \
|
||||
fn cant_load_chapters_with_an_empty_path() {
|
||||
let (_, temp) = dummy_link();
|
||||
let summary = Summary {
|
||||
numbered_chapters: vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Empty"),
|
||||
location: PathBuf::from(""),
|
||||
..Default::default()
|
||||
}),
|
||||
],
|
||||
numbered_chapters: vec![SummaryItem::Link(Link {
|
||||
name: String::from("Empty"),
|
||||
location: PathBuf::from(""),
|
||||
..Default::default()
|
||||
})],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -515,13 +552,11 @@ And here is some \
|
||||
fs::create_dir(&dir).unwrap();
|
||||
|
||||
let summary = Summary {
|
||||
numbered_chapters: vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("nested"),
|
||||
location: dir,
|
||||
..Default::default()
|
||||
}),
|
||||
],
|
||||
numbered_chapters: vec![SummaryItem::Link(Link {
|
||||
name: String::from("nested"),
|
||||
location: dir,
|
||||
..Default::default()
|
||||
})],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::fs::{self, File};
|
||||
use std::path::PathBuf;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use toml;
|
||||
|
||||
use config::Config;
|
||||
use super::MDBook;
|
||||
use theme;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
use theme;
|
||||
|
||||
/// A helper for setting up a new book and its directory structure.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -127,8 +127,20 @@ impl BookBuilder {
|
||||
let mut index = File::create(themedir.join("index.hbs"))?;
|
||||
index.write_all(theme::INDEX)?;
|
||||
|
||||
let mut css = File::create(themedir.join("book.css"))?;
|
||||
css.write_all(theme::CSS)?;
|
||||
let cssdir = themedir.join("css");
|
||||
fs::create_dir(&cssdir)?;
|
||||
|
||||
let mut general_css = File::create(cssdir.join("general.css"))?;
|
||||
general_css.write_all(theme::GENERAL_CSS)?;
|
||||
|
||||
let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
|
||||
chrome_css.write_all(theme::CHROME_CSS)?;
|
||||
|
||||
let mut print_css = File::create(cssdir.join("print.css"))?;
|
||||
print_css.write_all(theme::PRINT_CSS)?;
|
||||
|
||||
let mut variables_css = File::create(cssdir.join("variables.css"))?;
|
||||
variables_css.write_all(theme::VARIABLES_CSS)?;
|
||||
|
||||
let mut favicon = File::create(themedir.join("favicon.png"))?;
|
||||
favicon.write_all(theme::FAVICON)?;
|
||||
|
||||
@@ -5,24 +5,24 @@
|
||||
//!
|
||||
//! [1]: ../index.html
|
||||
|
||||
mod summary;
|
||||
mod book;
|
||||
mod init;
|
||||
mod summary;
|
||||
|
||||
pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
|
||||
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
pub use self::init::BookBuilder;
|
||||
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tempdir::TempDir;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use toml::Value;
|
||||
|
||||
use utils;
|
||||
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
||||
use preprocess::{LinkPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use errors::*;
|
||||
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
||||
use utils;
|
||||
|
||||
use config::Config;
|
||||
|
||||
@@ -213,11 +213,13 @@ 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());
|
||||
|
||||
LinkPreprocessor::new().run(&preprocess_context, &mut self.book)?;
|
||||
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
||||
// actual markdown files.
|
||||
|
||||
for item in self.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
@@ -288,13 +290,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,15 +324,19 @@ fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
|
||||
}
|
||||
|
||||
fn default_preprocessors() -> Vec<Box<Preprocessor>> {
|
||||
vec![Box::new(LinkPreprocessor::new())]
|
||||
vec![
|
||||
Box::new(LinkPreprocessor::new()),
|
||||
Box::new(IndexPreprocessor::new()),
|
||||
]
|
||||
}
|
||||
|
||||
/// Look at the `MDBook` and try to figure out what preprocessors to run.
|
||||
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
|
||||
let preprocess_list = match config.build.preprocess {
|
||||
Some(ref p) => p,
|
||||
// If no preprocessor field is set, default to the LinkPreprocessor. This allows you
|
||||
// to disable the LinkPreprocessor by setting "preprocess" to an empty list.
|
||||
// If no preprocessor field is set, default to the LinkPreprocessor and
|
||||
// IndexPreprocessor. This allows you to disable default preprocessors
|
||||
// by setting "preprocess" to an empty list.
|
||||
None => return Ok(default_preprocessors()),
|
||||
};
|
||||
|
||||
@@ -340,6 +345,7 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
|
||||
for key in preprocess_list {
|
||||
match key.as_ref() {
|
||||
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
|
||||
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
|
||||
_ => bail!("{:?} is not a recognised preprocessor", key),
|
||||
}
|
||||
}
|
||||
@@ -404,7 +410,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_defaults_to_link_preprocessor_if_not_set() {
|
||||
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
|
||||
let cfg = Config::default();
|
||||
|
||||
// make sure we haven't got anything in the `output` table
|
||||
@@ -413,8 +419,9 @@ mod tests {
|
||||
let got = determine_preprocessors(&cfg);
|
||||
|
||||
assert!(got.is_ok());
|
||||
assert_eq!(got.as_ref().unwrap().len(), 1);
|
||||
assert_eq!(got.as_ref().unwrap().len(), 2);
|
||||
assert_eq!(got.as_ref().unwrap()[0].name(), "links");
|
||||
assert_eq!(got.as_ref().unwrap()[1].name(), "index");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use errors::*;
|
||||
use memchr::{self, Memchr};
|
||||
use pulldown_cmark::{self, Event, Tag};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
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 errors::*;
|
||||
|
||||
/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be
|
||||
/// used when loading a book from disk.
|
||||
@@ -164,33 +164,34 @@ struct SummaryParser<'a> {
|
||||
/// use pattern matching and you won't get errors because `take_while()`
|
||||
/// moves `$stream` out of self.
|
||||
macro_rules! collect_events {
|
||||
($stream:expr, start $delimiter:pat) => {
|
||||
($stream:expr,start $delimiter:pat) => {
|
||||
collect_events!($stream, Event::Start($delimiter))
|
||||
};
|
||||
($stream:expr, end $delimiter:pat) => {
|
||||
($stream:expr,end $delimiter:pat) => {
|
||||
collect_events!($stream, Event::End($delimiter))
|
||||
};
|
||||
($stream:expr, $delimiter:pat) => {
|
||||
{
|
||||
let mut events = Vec::new();
|
||||
($stream:expr, $delimiter:pat) => {{
|
||||
let mut events = Vec::new();
|
||||
|
||||
loop {
|
||||
let event = $stream.next();
|
||||
trace!("Next event: {:?}", event);
|
||||
loop {
|
||||
let event = $stream.next();
|
||||
trace!("Next event: {:?}", event);
|
||||
|
||||
match event {
|
||||
Some($delimiter) => break,
|
||||
Some(other) => events.push(other),
|
||||
None => {
|
||||
debug!("Reached end of stream without finding the closing pattern, {}", stringify!($delimiter));
|
||||
break;
|
||||
}
|
||||
match event {
|
||||
Some($delimiter) => break,
|
||||
Some(other) => events.push(other),
|
||||
None => {
|
||||
debug!(
|
||||
"Reached end of stream without finding the closing pattern, {}",
|
||||
stringify!($delimiter)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
events
|
||||
}
|
||||
}
|
||||
|
||||
events
|
||||
}};
|
||||
}
|
||||
|
||||
impl<'a> SummaryParser<'a> {
|
||||
@@ -313,24 +314,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 +476,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)]
|
||||
@@ -705,14 +660,12 @@ mod tests {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Nested"),
|
||||
location: PathBuf::from("./nested.md"),
|
||||
number: Some(SectionNumber(vec![1, 1])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
],
|
||||
nested_items: vec![SummaryItem::Link(Link {
|
||||
name: String::from("Nested"),
|
||||
location: PathBuf::from("./nested.md"),
|
||||
number: Some(SectionNumber(vec![1, 1])),
|
||||
nested_items: Vec::new(),
|
||||
})],
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
use std::path::PathBuf;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
use {get_book_dir, open};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("build")
|
||||
.about("Build the book from the markdown files")
|
||||
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
.about("Builds a book from its markdown files")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book \
|
||||
when omitted)'",
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'",
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
|
||||
}
|
||||
|
||||
// Build command implementation
|
||||
@@ -24,7 +24,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = PathBuf::from(dest_dir);
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
|
||||
book.build()?;
|
||||
33
src/cmd/clean.rs
Normal file
33
src/cmd/clean.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::MDBook;
|
||||
use std::fs;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("clean")
|
||||
.about("Deletes a built book")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
}
|
||||
|
||||
// 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) => dest_dir.into(),
|
||||
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,28 +1,32 @@
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::config;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use get_book_dir;
|
||||
use std::process::Command;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("init")
|
||||
.about("Create boilerplate structure and files in the directory")
|
||||
.about("Creates the boilerplate structure and files for a new book")
|
||||
// the {n} denotes a newline which will properly aligned in all help messages
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory \
|
||||
when omitted)'")
|
||||
.arg_from_usage("[dir] 'Directory to create the book in{n}\
|
||||
(Defaults to the Current Directory when omitted)'")
|
||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
||||
.arg_from_usage("--force 'skip confirmation prompts'")
|
||||
.arg_from_usage("--force 'Skips confirmation prompts'")
|
||||
}
|
||||
|
||||
// Init command implementation
|
||||
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 +42,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
if confirm() {
|
||||
builder.copy_theme(true);
|
||||
}
|
||||
} else {
|
||||
builder.copy_theme(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +53,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();
|
||||
10
src/cmd/mod.rs
Normal file
10
src/cmd/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! Subcommand modules for the `mdbook` binary.
|
||||
|
||||
pub mod build;
|
||||
pub mod clean;
|
||||
pub mod init;
|
||||
#[cfg(feature = "serve")]
|
||||
pub mod serve;
|
||||
pub mod test;
|
||||
#[cfg(feature = "watch")]
|
||||
pub mod watch;
|
||||
@@ -2,39 +2,69 @@ extern crate iron;
|
||||
extern crate staticfile;
|
||||
extern crate ws;
|
||||
|
||||
use std;
|
||||
use self::iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response,
|
||||
Set};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::utils;
|
||||
use mdbook::errors::*;
|
||||
use {get_book_dir, open};
|
||||
use self::iron::{
|
||||
status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set,
|
||||
};
|
||||
#[cfg(feature = "watch")]
|
||||
use watch;
|
||||
use super::watch;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
use std;
|
||||
use {get_book_dir, open};
|
||||
|
||||
struct ErrorRecover;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("serve")
|
||||
.about("Serve the book at http://localhost:3000. Rebuild and reload on change.")
|
||||
.about("Serves a book at http://localhost:3000, and rebuilds it on changes")
|
||||
.arg_from_usage(
|
||||
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
|
||||
.arg_from_usage(
|
||||
"-w, --websocket-port=[ws-port] 'Use another port for the websocket connection \
|
||||
(livereload){n}(Defaults to 3001)'",
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'",
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"-a, --address=[address] 'Address that the browser can reach the websocket server \
|
||||
from{n}(Defaults to the interface address)'",
|
||||
.arg(
|
||||
Arg::with_name("hostname")
|
||||
.short("n")
|
||||
.long("hostname")
|
||||
.takes_value(true)
|
||||
.default_value("localhost")
|
||||
.empty_values(false)
|
||||
.help("Hostname to listen on for HTTP connections"),
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Open the book server in a web browser'")
|
||||
.arg(
|
||||
Arg::with_name("port")
|
||||
.short("p")
|
||||
.long("port")
|
||||
.takes_value(true)
|
||||
.default_value("3000")
|
||||
.empty_values(false)
|
||||
.help("Port to use for HTTP connections"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("websocket-hostname")
|
||||
.long("websocket-hostname")
|
||||
.takes_value(true)
|
||||
.empty_values(false)
|
||||
.help(
|
||||
"Hostname to connect to for WebSockets connections (Defaults to the HTTP hostname)",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("websocket-port")
|
||||
.short("w")
|
||||
.long("websocket-port")
|
||||
.takes_value(true)
|
||||
.default_value("3001")
|
||||
.empty_values(false)
|
||||
.help("Port to use for WebSockets livereload connections"),
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Opens the book server in a web browser'")
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
@@ -42,19 +72,23 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
let port = args.value_of("port").unwrap_or("3000");
|
||||
let ws_port = args.value_of("websocket-port").unwrap_or("3001");
|
||||
let interface = args.value_of("interface").unwrap_or("localhost");
|
||||
let public_address = args.value_of("address").unwrap_or(interface);
|
||||
let port = args.value_of("port").unwrap();
|
||||
let ws_port = args.value_of("websocket-port").unwrap();
|
||||
let hostname = args.value_of("hostname").unwrap();
|
||||
let public_address = args.value_of("websocket-address").unwrap_or(hostname);
|
||||
let open_browser = args.is_present("open");
|
||||
|
||||
let address = format!("{}:{}", interface, port);
|
||||
let ws_address = format!("{}:{}", interface, ws_port);
|
||||
let address = format!("{}:{}", hostname, port);
|
||||
let ws_address = format!("{}:{}", hostname, ws_port);
|
||||
|
||||
let livereload_url = format!("ws://{}:{}", public_address, ws_port);
|
||||
book.config
|
||||
.set("output.html.livereload-url", &livereload_url)?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
|
||||
book.build()?;
|
||||
|
||||
let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html")));
|
||||
@@ -86,10 +120,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
|
||||
// FIXME: This area is really ugly because we need to re-set livereload :(
|
||||
|
||||
let livereload_url = livereload_url.clone();
|
||||
|
||||
let result = MDBook::load(&book_dir)
|
||||
.and_then(move |mut b| {
|
||||
.and_then(|mut b| {
|
||||
b.config.set("output.html.livereload-url", &livereload_url)?;
|
||||
Ok(b)
|
||||
})
|
||||
44
src/cmd/test.rs
Normal file
44
src/cmd/test.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("test")
|
||||
.about("Tests that a book's Rust code samples compile")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg(Arg::with_name("library-path")
|
||||
.short("L")
|
||||
.long("library-path")
|
||||
.value_name("dir")
|
||||
.takes_value(true)
|
||||
.require_delimiter(true)
|
||||
.multiple(true)
|
||||
.empty_values(false)
|
||||
.help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
|
||||
}
|
||||
|
||||
// test command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let library_paths: Vec<&str> = args.values_of("library-path")
|
||||
.map(|v| v.collect())
|
||||
.unwrap_or_default();
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
|
||||
book.test(library_paths)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,23 +1,28 @@
|
||||
extern crate notify;
|
||||
|
||||
use std::path::Path;
|
||||
use self::notify::Watcher;
|
||||
use std::time::Duration;
|
||||
use std::sync::mpsc::channel;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::utils;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::time::Duration;
|
||||
use {get_book_dir, open};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("watch")
|
||||
.about("Watch the files for changes")
|
||||
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
.about("Watches a book's files and rebuilds it on changes")
|
||||
.arg_from_usage(
|
||||
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'",
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
@@ -48,8 +53,8 @@ pub fn trigger_on_change<F>(book: &MDBook, closure: F)
|
||||
where
|
||||
F: Fn(&Path, &Path),
|
||||
{
|
||||
use self::notify::RecursiveMode::*;
|
||||
use self::notify::DebouncedEvent::*;
|
||||
use self::notify::RecursiveMode::*;
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
135
src/config.rs
135
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"));
|
||||
@@ -50,17 +50,17 @@
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::env;
|
||||
use toml::{self, Value};
|
||||
use toml::value::Table;
|
||||
use toml_query::read::TomlValueReadExt;
|
||||
use toml_query::insert::TomlValueInsertExt;
|
||||
use toml_query::delete::TomlValueDeleteExt;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use toml::value::Table;
|
||||
use toml::{self, Value};
|
||||
use toml_query::delete::TomlValueDeleteExt;
|
||||
use toml_query::insert::TomlValueInsertExt;
|
||||
use toml_query::read::TomlValueReadExt;
|
||||
|
||||
use errors::*;
|
||||
|
||||
@@ -217,9 +217,10 @@ impl Config {
|
||||
// figure out what try_into() deserializes to.
|
||||
macro_rules! get_and_insert {
|
||||
($table:expr, $key:expr => $out:expr) => {
|
||||
let got = $table.as_table_mut()
|
||||
.and_then(|t| t.remove($key))
|
||||
.and_then(|v| v.try_into().ok());
|
||||
let got = $table
|
||||
.as_table_mut()
|
||||
.and_then(|t| t.remove($key))
|
||||
.and_then(|v| v.try_into().ok());
|
||||
if let Some(value) = got {
|
||||
$out = value;
|
||||
}
|
||||
@@ -383,7 +384,6 @@ pub struct BuildConfig {
|
||||
pub create_missing: bool,
|
||||
/// Which preprocessors should be applied
|
||||
pub preprocess: Option<Vec<String>>,
|
||||
|
||||
}
|
||||
|
||||
impl Default for BuildConfig {
|
||||
@@ -410,7 +410,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 +425,88 @@ 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 {
|
||||
/// Enable the search feature. Default: `true`.
|
||||
pub enable: bool,
|
||||
/// 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 {
|
||||
enable: true,
|
||||
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 +583,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,
|
||||
|
||||
36
src/lib.rs
36
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;
|
||||
|
||||
@@ -100,17 +107,17 @@ extern crate toml_query;
|
||||
#[macro_use]
|
||||
extern crate pretty_assertions;
|
||||
|
||||
pub mod preprocess;
|
||||
pub mod book;
|
||||
pub mod config;
|
||||
pub mod preprocess;
|
||||
pub mod renderer;
|
||||
pub mod theme;
|
||||
pub mod utils;
|
||||
|
||||
pub use book::MDBook;
|
||||
pub use book::BookItem;
|
||||
pub use renderer::Renderer;
|
||||
pub use book::MDBook;
|
||||
pub use config::Config;
|
||||
pub use renderer::Renderer;
|
||||
|
||||
/// The error types used through out this crate.
|
||||
pub mod errors {
|
||||
@@ -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,6 +1,6 @@
|
||||
extern crate chrono;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate chrono;
|
||||
extern crate env_logger;
|
||||
extern crate error_chain;
|
||||
#[macro_use]
|
||||
@@ -8,58 +8,55 @@ extern crate log;
|
||||
extern crate mdbook;
|
||||
extern crate open;
|
||||
|
||||
use chrono::Local;
|
||||
use clap::{App, AppSettings, ArgMatches};
|
||||
use env_logger::Builder;
|
||||
use log::LevelFilter;
|
||||
use mdbook::utils;
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::io::Write;
|
||||
use clap::{App, AppSettings, ArgMatches};
|
||||
use chrono::Local;
|
||||
use log::LevelFilter;
|
||||
use env_logger::Builder;
|
||||
use mdbook::utils;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub mod build;
|
||||
pub mod init;
|
||||
pub mod test;
|
||||
#[cfg(feature = "serve")]
|
||||
pub mod serve;
|
||||
#[cfg(feature = "watch")]
|
||||
pub mod watch;
|
||||
mod cmd;
|
||||
|
||||
const NAME: &'static str = "mdbook";
|
||||
const NAME: &'static str = "mdBook";
|
||||
const VERSION: &'static str = concat!("v", crate_version!());
|
||||
|
||||
fn main() {
|
||||
init_logger();
|
||||
|
||||
// Create a list of valid arguments and sub-commands
|
||||
let app = App::new(NAME)
|
||||
.about("Create a book in form of a static website from markdown files")
|
||||
.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)
|
||||
.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());
|
||||
.about("Creates a book from markdown files")
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
.version(VERSION)
|
||||
.setting(AppSettings::GlobalVersion)
|
||||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.after_help(
|
||||
"For more information about a specific command, try `mdbook <command> --help`\n\
|
||||
The source code for mdBook is available at: https://github.com/rust-lang-nursery/mdBook",
|
||||
)
|
||||
.subcommand(cmd::init::make_subcommand())
|
||||
.subcommand(cmd::build::make_subcommand())
|
||||
.subcommand(cmd::test::make_subcommand())
|
||||
.subcommand(cmd::clean::make_subcommand());
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
let app = app.subcommand(watch::make_subcommand());
|
||||
let app = app.subcommand(cmd::watch::make_subcommand());
|
||||
#[cfg(feature = "serve")]
|
||||
let app = app.subcommand(serve::make_subcommand());
|
||||
let app = app.subcommand(cmd::serve::make_subcommand());
|
||||
|
||||
// Check which subcomamnd the user ran...
|
||||
let res = match app.get_matches().subcommand() {
|
||||
("init", Some(sub_matches)) => init::execute(sub_matches),
|
||||
("build", Some(sub_matches)) => build::execute(sub_matches),
|
||||
("init", Some(sub_matches)) => cmd::init::execute(sub_matches),
|
||||
("build", Some(sub_matches)) => cmd::build::execute(sub_matches),
|
||||
("clean", Some(sub_matches)) => cmd::clean::execute(sub_matches),
|
||||
#[cfg(feature = "watch")]
|
||||
("watch", Some(sub_matches)) => watch::execute(sub_matches),
|
||||
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches),
|
||||
#[cfg(feature = "serve")]
|
||||
("serve", Some(sub_matches)) => serve::execute(sub_matches),
|
||||
("test", Some(sub_matches)) => test::execute(sub_matches),
|
||||
("serve", Some(sub_matches)) => cmd::serve::execute(sub_matches),
|
||||
("test", Some(sub_matches)) => cmd::test::execute(sub_matches),
|
||||
(_, _) => unreachable!(),
|
||||
};
|
||||
|
||||
@@ -74,11 +71,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 +86,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();
|
||||
97
src/preprocess/index.rs
Normal file
97
src/preprocess/index.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use regex::Regex;
|
||||
use std::path::Path;
|
||||
|
||||
use errors::*;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use book::{Book, BookItem};
|
||||
|
||||
/// A preprocessor for converting file name `README.md` to `index.md` since
|
||||
/// `README.md` is the de facto index file in a markdown-based documentation.
|
||||
pub struct IndexPreprocessor;
|
||||
|
||||
impl IndexPreprocessor {
|
||||
/// Create a new `IndexPreprocessor`.
|
||||
pub fn new() -> Self {
|
||||
IndexPreprocessor
|
||||
}
|
||||
}
|
||||
|
||||
impl Preprocessor for IndexPreprocessor {
|
||||
fn name(&self) -> &str {
|
||||
"index"
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
|
||||
let source_dir = ctx.root.join(&ctx.config.book.src);
|
||||
book.for_each_mut(|section: &mut BookItem| {
|
||||
if let BookItem::Chapter(ref mut ch) = *section {
|
||||
if is_readme_file(&ch.path) {
|
||||
let index_md = source_dir.join(ch.path.with_file_name("index.md"));
|
||||
if index_md.exists() {
|
||||
warn_readme_name_conflict(&ch.path, &index_md);
|
||||
}
|
||||
|
||||
ch.path.set_file_name("index.md");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
|
||||
let file_name = readme_path.as_ref().file_name().unwrap_or_default();
|
||||
let parent_dir = index_path.as_ref().parent().unwrap_or(index_path.as_ref());
|
||||
warn!(
|
||||
"It seems that there are both {:?} and index.md under \"{}\".",
|
||||
file_name,
|
||||
parent_dir.display()
|
||||
);
|
||||
warn!(
|
||||
"mdbook converts {:?} into index.html by default. It may cause",
|
||||
file_name
|
||||
);
|
||||
warn!("unexpected behavior if putting both files under the same directory.");
|
||||
warn!("To solve the warning, try to rearrange the book structure or disable");
|
||||
warn!("\"index\" preprocessor to stop the conversion.");
|
||||
}
|
||||
|
||||
fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"(?i)^readme$").unwrap();
|
||||
}
|
||||
RE.is_match(
|
||||
path.as_ref()
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn file_stem_exactly_matches_readme_case_insensitively() {
|
||||
let path = "path/to/Readme.md";
|
||||
assert!(is_readme_file(path));
|
||||
|
||||
let path = "path/to/README.md";
|
||||
assert!(is_readme_file(path));
|
||||
|
||||
let path = "path/to/rEaDmE.md";
|
||||
assert!(is_readme_file(path));
|
||||
|
||||
let path = "path/to/README.markdown";
|
||||
assert!(is_readme_file(path));
|
||||
|
||||
let path = "path/to/README";
|
||||
assert!(is_readme_file(path));
|
||||
|
||||
let path = "path/to/README-README.md";
|
||||
assert!(!is_readme_file(path));
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
use errors::*;
|
||||
use regex::{CaptureMatches, Captures, Regex};
|
||||
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
|
||||
use std::path::{Path, PathBuf};
|
||||
use regex::{CaptureMatches, Captures, Regex};
|
||||
use utils::fs::file_to_string;
|
||||
use utils::take_lines;
|
||||
use errors::*;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use book::{Book, BookItem};
|
||||
|
||||
const ESCAPE_CHAR: char = '\\';
|
||||
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
|
||||
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
|
||||
/// helpers in a chapter.
|
||||
@@ -31,12 +32,13 @@ impl Preprocessor for LinkPreprocessor {
|
||||
|
||||
book.for_each_mut(|section: &mut BookItem| {
|
||||
if let BookItem::Chapter(ref mut ch) = *section {
|
||||
let base = ch.path
|
||||
let base = ch
|
||||
.path
|
||||
.parent()
|
||||
.map(|dir| src_dir.join(dir))
|
||||
.expect("All book items have a parent");
|
||||
|
||||
let content = replace_all(&ch.content, base);
|
||||
let content = replace_all(&ch.content, base, &ch.path, 0);
|
||||
ch.content = content;
|
||||
}
|
||||
});
|
||||
@@ -45,11 +47,16 @@ impl Preprocessor for LinkPreprocessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> String {
|
||||
fn replace_all<P1, P2>(s: &str, path: P1, source: P2, depth: usize) -> String
|
||||
where
|
||||
P1: AsRef<Path>,
|
||||
P2: AsRef<Path>,
|
||||
{
|
||||
// When replacing one thing in a string by something with a different length,
|
||||
// the indices after that will not correspond,
|
||||
// we therefore have to store the difference to correct this
|
||||
let path = path.as_ref();
|
||||
let source = source.as_ref();
|
||||
let mut previous_end_index = 0;
|
||||
let mut replaced = String::new();
|
||||
|
||||
@@ -58,7 +65,18 @@ fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> String {
|
||||
|
||||
match playpen.render_with_path(&path) {
|
||||
Ok(new_content) => {
|
||||
replaced.push_str(&new_content);
|
||||
if depth < MAX_LINK_NESTED_DEPTH {
|
||||
if let Some(rel_path) = playpen.link.relative_path(path) {
|
||||
replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1));
|
||||
} else {
|
||||
replaced.push_str(&new_content);
|
||||
}
|
||||
} else {
|
||||
error!(
|
||||
"Stack depth exceeded in {}. Check for cyclic includes",
|
||||
source.display()
|
||||
);
|
||||
}
|
||||
previous_end_index = playpen.end_index;
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -84,6 +102,27 @@ enum LinkType<'a> {
|
||||
Playpen(PathBuf, Vec<&'a str>),
|
||||
}
|
||||
|
||||
impl<'a> LinkType<'a> {
|
||||
fn relative_path<P: AsRef<Path>>(self, base: P) -> Option<PathBuf> {
|
||||
let base = base.as_ref();
|
||||
match self {
|
||||
LinkType::Escaped => None,
|
||||
LinkType::IncludeRange(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
|
||||
base.as_ref()
|
||||
.join(relative)
|
||||
.parent()
|
||||
.expect("Included file should not be /")
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
fn parse_include_path(path: &str) -> LinkType<'static> {
|
||||
let mut parts = path.split(':');
|
||||
let path = parts.next().unwrap().into();
|
||||
@@ -91,7 +130,7 @@ fn parse_include_path(path: &str) -> LinkType<'static> {
|
||||
let start = parts
|
||||
.next()
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.map(|val| val.checked_sub(1).unwrap_or(0));
|
||||
.map(|val| val.saturating_sub(1));
|
||||
let end = parts.next();
|
||||
let has_end = end.is_some();
|
||||
let end = end.and_then(|s| s.parse::<usize>().ok());
|
||||
@@ -210,15 +249,16 @@ fn find_links(contents: &str) -> LinkIter {
|
||||
// lazily compute following regex
|
||||
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
|
||||
\s*\}\} # whitespace and link closing parens
|
||||
").unwrap();
|
||||
static ref RE: Regex = Regex::new(
|
||||
r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
|
||||
\s*\}\} # whitespace and link closing parens"
|
||||
).unwrap();
|
||||
}
|
||||
LinkIter(RE.captures_iter(contents))
|
||||
}
|
||||
@@ -227,6 +267,21 @@ fn find_links(contents: &str) -> LinkIter {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_replace_all_escaped() {
|
||||
let start = r"
|
||||
Some text over here.
|
||||
```hbs
|
||||
\{{#include file.rs}} << an escaped link!
|
||||
```";
|
||||
let end = r"
|
||||
Some text over here.
|
||||
```hbs
|
||||
{{#include file.rs}} << an escaped link!
|
||||
```";
|
||||
assert_eq!(replace_all(start, "", "", 0), end);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_no_link() {
|
||||
let s = "Some random text without link...";
|
||||
@@ -288,14 +343,12 @@ mod tests {
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
Link {
|
||||
start_index: 22,
|
||||
end_index: 48,
|
||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
|
||||
link_text: "{{#include file.rs:10:20}}",
|
||||
},
|
||||
]
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 48,
|
||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
|
||||
link_text: "{{#include file.rs:10:20}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -306,14 +359,12 @@ mod tests {
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
Link {
|
||||
start_index: 22,
|
||||
end_index: 45,
|
||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
|
||||
link_text: "{{#include file.rs:10}}",
|
||||
},
|
||||
]
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 45,
|
||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
|
||||
link_text: "{{#include file.rs:10}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -324,14 +375,12 @@ mod tests {
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
|
||||
link_text: "{{#include file.rs:10:}}",
|
||||
},
|
||||
]
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
|
||||
link_text: "{{#include file.rs:10:}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -342,14 +391,12 @@ mod tests {
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
|
||||
link_text: "{{#include file.rs::20}}",
|
||||
},
|
||||
]
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
|
||||
link_text: "{{#include file.rs::20}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -360,14 +407,12 @@ mod tests {
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
Link {
|
||||
start_index: 22,
|
||||
end_index: 44,
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_text: "{{#include file.rs::}}",
|
||||
},
|
||||
]
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 44,
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_text: "{{#include file.rs::}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -378,14 +423,12 @@ mod tests {
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
Link {
|
||||
start_index: 22,
|
||||
end_index: 42,
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_text: "{{#include file.rs}}",
|
||||
},
|
||||
]
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 42,
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_text: "{{#include file.rs}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -398,14 +441,12 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: LinkType::Escaped,
|
||||
link_text: "\\{{#playpen file.rs editable}}",
|
||||
},
|
||||
]
|
||||
vec![Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: LinkType::Escaped,
|
||||
link_text: "\\{{#playpen file.rs editable}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Book preprocessing.
|
||||
|
||||
pub use self::index::IndexPreprocessor;
|
||||
pub use self::links::LinkPreprocessor;
|
||||
|
||||
mod index;
|
||||
mod links;
|
||||
|
||||
use book::Book;
|
||||
@@ -10,7 +12,7 @@ use errors::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Extra information for a `Preprocessor` to give them more context when
|
||||
/// Extra information for a `Preprocessor` to give them more context when
|
||||
/// processing a book.
|
||||
pub struct PreprocessorContext {
|
||||
/// The location of the book directory on disk.
|
||||
@@ -26,7 +28,7 @@ impl PreprocessorContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// An operation which is run immediately after loading a book into memory and
|
||||
/// An operation which is run immediately after loading a book into memory and
|
||||
/// before it gets rendered.
|
||||
pub trait Preprocessor {
|
||||
/// Get the `Preprocessor`'s name.
|
||||
@@ -35,4 +37,4 @@ pub trait Preprocessor {
|
||||
/// Run this `Preprocessor`, allowing it to update the book before it is
|
||||
/// given to a renderer.
|
||||
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
use book::{Book, BookItem};
|
||||
use config::{Config, HtmlConfig, Playpen};
|
||||
use errors::*;
|
||||
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 theme::{self, playpen_editor, Theme};
|
||||
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;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use handlebars::Handlebars;
|
||||
|
||||
use regex::{Captures, Regex};
|
||||
use serde_json;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -26,23 +23,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 +40,7 @@ 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");
|
||||
|
||||
// "print.html" is used for the print page.
|
||||
if ch.path == Path::new("print.md") {
|
||||
@@ -66,9 +51,9 @@ impl HtmlHandlebars {
|
||||
let title: String;
|
||||
{
|
||||
let book_title = ctx.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
title = ch.name.clone() + " - " + book_title;
|
||||
}
|
||||
|
||||
@@ -76,28 +61,33 @@ impl HtmlHandlebars {
|
||||
ctx.data.insert("content".to_owned(), json!(content));
|
||||
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
|
||||
ctx.data.insert("title".to_owned(), json!(title));
|
||||
ctx.data.insert("path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&ch.path)));
|
||||
ctx.data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&ch.path)),
|
||||
);
|
||||
|
||||
// Render the handlebars template with the data
|
||||
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()))
|
||||
})?),
|
||||
&ctx.html_config.playpen,
|
||||
);
|
||||
let rendered = self.post_process(rendered, &ctx.html_config.playpen);
|
||||
|
||||
// Write to file
|
||||
debug!("Creating {} ✓", filepath.display());
|
||||
self.write_file(&ctx.destination, filepath, &rendered.into_bytes())?;
|
||||
debug!("Creating {}", filepath.display());
|
||||
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
|
||||
|
||||
if ctx.is_index {
|
||||
self.render_index(ch, &ctx.destination)?;
|
||||
ctx.data.insert("path".to_owned(), json!("index.html"));
|
||||
ctx.data.insert("path_to_root".to_owned(), json!(""));
|
||||
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
|
||||
let rendered_index =
|
||||
self.post_process(rendered_index, &ctx.html_config.playpen);
|
||||
debug!("Creating index.html from {}", path);
|
||||
utils::fs::write_file(
|
||||
&ctx.destination,
|
||||
"index.html",
|
||||
rendered_index.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -106,41 +96,9 @@ impl HtmlHandlebars {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an index.html from the first element in SUMMARY.md
|
||||
fn render_index(&self, ch: &Chapter, destination: &Path) -> Result<()> {
|
||||
debug!("index.html");
|
||||
|
||||
let mut content = String::new();
|
||||
|
||||
File::open(destination.join(&ch.path.with_extension("html")))?
|
||||
.read_to_string(&mut content)?;
|
||||
|
||||
// This could cause a problem when someone displays
|
||||
// code containing <base href=...>
|
||||
// on the front page, however this case should be very very rare...
|
||||
content = content.lines()
|
||||
.filter(|line| !line.contains("<base href="))
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
self.write_file(destination, "index.html", content.as_bytes())?;
|
||||
|
||||
debug!(
|
||||
"Creating index.html from {} ✓",
|
||||
destination.join(&ch.path.with_extension("html")).display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
|
||||
fn post_process(&self,
|
||||
rendered: String,
|
||||
filepath: &str,
|
||||
playpen_config: &Playpen)
|
||||
-> String {
|
||||
let rendered = build_header_links(&rendered, filepath);
|
||||
let rendered = fix_anchor_links(&rendered, filepath);
|
||||
fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String {
|
||||
let rendered = build_header_links(&rendered);
|
||||
let rendered = fix_code_blocks(&rendered);
|
||||
let rendered = add_playpen_pre(&rendered, playpen_config);
|
||||
|
||||
@@ -153,63 +111,74 @@ 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,
|
||||
"_FontAwesome/css/font-awesome.css",
|
||||
".nojekyll",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.",
|
||||
)?;
|
||||
|
||||
write_file(destination, "book.js", &theme.js)?;
|
||||
write_file(destination, "css/general.css", &theme.general_css)?;
|
||||
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
|
||||
write_file(destination, "css/print.css", &theme.print_css)?;
|
||||
write_file(destination, "css/variables.css", &theme.variables_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",
|
||||
"FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT,
|
||||
)?;
|
||||
self.write_file(
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
"FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG,
|
||||
)?;
|
||||
self.write_file(
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
"FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
self.write_file(
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF,
|
||||
)?;
|
||||
self.write_file(
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2,
|
||||
)?;
|
||||
self.write_file(
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/FontAwesome.ttf",
|
||||
"FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
|
||||
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,
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -217,28 +186,42 @@ impl HtmlHandlebars {
|
||||
}
|
||||
|
||||
/// Update the context with data for this file
|
||||
fn configure_print_version(&self,
|
||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||
print_content: &str) {
|
||||
fn configure_print_version(
|
||||
&self,
|
||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||
print_content: &str,
|
||||
) {
|
||||
// Make sure that the Print chapter does not display the title from
|
||||
// the last rendered chapter by removing it from its context
|
||||
data.remove("title");
|
||||
data.insert("is_print".to_owned(), json!(true));
|
||||
data.insert("path".to_owned(), json!("print.md"));
|
||||
data.insert("content".to_owned(), json!(print_content));
|
||||
data.insert("path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(Path::new("print.md"))));
|
||||
data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(Path::new("print.md"))),
|
||||
);
|
||||
}
|
||||
|
||||
fn register_hbs_helpers(&self, handlebars: &mut Handlebars, html_config: &HtmlConfig) {
|
||||
handlebars.register_helper("toc", Box::new(helpers::toc::RenderToc {no_section_label: html_config.no_section_label}));
|
||||
handlebars.register_helper(
|
||||
"toc",
|
||||
Box::new(helpers::toc::RenderToc {
|
||||
no_section_label: html_config.no_section_label,
|
||||
}),
|
||||
);
|
||||
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
|
||||
handlebars.register_helper("next", Box::new(helpers::navigation::next));
|
||||
}
|
||||
|
||||
/// Copy across any additional CSS and JavaScript files which the book
|
||||
/// has been configured to use.
|
||||
fn copy_additional_css_and_js(&self, html: &HtmlConfig, root: &Path, destination: &Path) -> Result<()> {
|
||||
fn copy_additional_css_and_js(
|
||||
&self,
|
||||
html: &HtmlConfig,
|
||||
root: &Path,
|
||||
destination: &Path,
|
||||
) -> Result<()> {
|
||||
let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
|
||||
|
||||
debug!("Copying additional CSS and JS");
|
||||
@@ -269,6 +252,25 @@ impl HtmlHandlebars {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mattico): Remove some time after the 0.1.8 release
|
||||
fn maybe_wrong_theme_dir(dir: &Path) -> Result<bool> {
|
||||
fn entry_is_maybe_book_file(entry: fs::DirEntry) -> Result<bool> {
|
||||
Ok(entry.file_type()?.is_file()
|
||||
&& entry.path().extension().map_or(false, |ext| ext == "md"))
|
||||
}
|
||||
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
if entry_is_maybe_book_file(entry?).unwrap_or(false) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for HtmlHandlebars {
|
||||
fn name(&self) -> &str {
|
||||
"html"
|
||||
@@ -285,9 +287,19 @@ impl Renderer for HtmlHandlebars {
|
||||
|
||||
let theme_dir = match html_config.theme {
|
||||
Some(ref theme) => theme.to_path_buf(),
|
||||
None => src_dir.join("theme"),
|
||||
None => ctx.root.join("theme"),
|
||||
};
|
||||
|
||||
if html_config.theme.is_none()
|
||||
&& maybe_wrong_theme_dir(&src_dir.join("theme")).unwrap_or(false)
|
||||
{
|
||||
warn!(
|
||||
"Previous versions of mdBook erroneously accepted `./src/theme` as an automatic \
|
||||
theme directory"
|
||||
);
|
||||
warn!("Please move your theme files to `./theme` for them to continue being used");
|
||||
}
|
||||
|
||||
let theme = theme::Theme::new(theme_dir);
|
||||
|
||||
debug!("Register the index handlebars template");
|
||||
@@ -307,15 +319,17 @@ 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)?;
|
||||
is_index = false;
|
||||
}
|
||||
|
||||
// Print version
|
||||
@@ -326,14 +340,11 @@ 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);
|
||||
let rendered = self.post_process(rendered, &html_config.playpen);
|
||||
|
||||
self.write_file(&destination, "print.html", &rendered.into_bytes())?;
|
||||
utils::fs::write_file(&destination, "print.html", rendered.as_bytes())?;
|
||||
debug!("Creating print.html ✓");
|
||||
|
||||
debug!("Copy static files");
|
||||
@@ -342,6 +353,15 @@ 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")]
|
||||
{
|
||||
let search = html_config.search.unwrap_or_default();
|
||||
if search.enable {
|
||||
super::search::create_files(&search, &destination, &book)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy all remaining files
|
||||
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
|
||||
|
||||
@@ -349,14 +369,25 @@ impl Renderer for HtmlHandlebars {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
fn make_data(
|
||||
root: &Path,
|
||||
book: &Book,
|
||||
config: &Config,
|
||||
html_config: &HtmlConfig,
|
||||
) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
trace!("make_data");
|
||||
let html = config.html_config().unwrap_or_default();
|
||||
|
||||
let mut data = serde_json::Map::new();
|
||||
data.insert("language".to_owned(), json!("en"));
|
||||
data.insert("book_title".to_owned(), json!(config.book.title.clone().unwrap_or_default()));
|
||||
data.insert("description".to_owned(), json!(config.book.description.clone().unwrap_or_default()));
|
||||
data.insert(
|
||||
"book_title".to_owned(),
|
||||
json!(config.book.title.clone().unwrap_or_default()),
|
||||
);
|
||||
data.insert(
|
||||
"description".to_owned(),
|
||||
json!(config.book.description.clone().unwrap_or_default()),
|
||||
);
|
||||
data.insert("favicon".to_owned(), json!("favicon.png"));
|
||||
if let Some(ref livereload) = html_config.livereload_url {
|
||||
data.insert("livereload".to_owned(), json!(livereload));
|
||||
@@ -376,13 +407,8 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
|
||||
let mut css = Vec::new();
|
||||
for style in &html.additional_css {
|
||||
match style.strip_prefix(root) {
|
||||
Ok(p) => {
|
||||
css.push(p.to_str().expect("Could not convert to str"))
|
||||
},
|
||||
Err(_) => {
|
||||
css.push(style.to_str()
|
||||
.expect("Could not convert to str"))
|
||||
}
|
||||
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => css.push(style.to_str().expect("Could not convert to str")),
|
||||
}
|
||||
}
|
||||
data.insert("additional_css".to_owned(), json!(css));
|
||||
@@ -394,25 +420,36 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
|
||||
for script in &html.additional_js {
|
||||
match script.strip_prefix(root) {
|
||||
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => {
|
||||
js.push(script.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str"))
|
||||
}
|
||||
Err(_) => js.push(
|
||||
script
|
||||
.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str"),
|
||||
),
|
||||
}
|
||||
}
|
||||
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") {
|
||||
let search = search.unwrap_or_default();
|
||||
data.insert("search_enabled".to_owned(), json!(search.enable));
|
||||
data.insert(
|
||||
"search_js".to_owned(),
|
||||
json!(search.enable && search.copy_js),
|
||||
);
|
||||
} 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![];
|
||||
@@ -449,27 +486,29 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
|
||||
|
||||
/// Goes through the rendered HTML, making sure all header tags are wrapped in
|
||||
/// an anchor so people can link to sections directly.
|
||||
fn build_header_links(html: &str, filepath: &str) -> String {
|
||||
fn build_header_links(html: &str) -> String {
|
||||
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
regex.replace_all(html, |caps: &Captures| {
|
||||
let level = caps[1].parse()
|
||||
.expect("Regex should ensure we only ever get numbers here");
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let level = caps[1]
|
||||
.parse()
|
||||
.expect("Regex should ensure we only ever get numbers here");
|
||||
|
||||
wrap_header_with_link(level, &caps[2], &mut id_counter, filepath)
|
||||
})
|
||||
.into_owned()
|
||||
wrap_header_with_link(level, &caps[2], &mut id_counter)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Wraps a single header tag with a link, making sure each tag gets its own
|
||||
/// unique ID by appending an auto-incremented number (if necessary).
|
||||
fn wrap_header_with_link(level: usize,
|
||||
content: &str,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
filepath: &str)
|
||||
-> String {
|
||||
let raw_id = id_from_content(content);
|
||||
fn wrap_header_with_link(
|
||||
level: usize,
|
||||
content: &str,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
) -> String {
|
||||
let raw_id = utils::id_from_content(content);
|
||||
|
||||
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
|
||||
|
||||
@@ -481,61 +520,13 @@ fn wrap_header_with_link(level: usize,
|
||||
*id_count += 1;
|
||||
|
||||
format!(
|
||||
r##"<a class="header" href="{filepath}#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
|
||||
r##"<a class="header" href="#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content,
|
||||
filepath = filepath
|
||||
text = content
|
||||
)
|
||||
}
|
||||
|
||||
/// 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
|
||||
fn fix_anchor_links(html: &str, filepath: &str) -> String {
|
||||
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
|
||||
regex.replace_all(html, |caps: &Captures| {
|
||||
let before = &caps[1];
|
||||
let anchor = &caps[2];
|
||||
let after = &caps[3];
|
||||
|
||||
format!("<a{before}href=\"{filepath}#{anchor}\"{after}>",
|
||||
before = before,
|
||||
filepath = filepath,
|
||||
anchor = anchor,
|
||||
after = after)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
|
||||
// The rust book uses annotations for rustdoc to test code snippets,
|
||||
// like the following:
|
||||
// ```rust,should_panic
|
||||
@@ -546,53 +537,57 @@ fn fix_anchor_links(html: &str, filepath: &str) -> String {
|
||||
// This function replaces all commas by spaces in the code block classes
|
||||
fn fix_code_blocks(html: &str) -> String {
|
||||
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
||||
regex.replace_all(html, |caps: &Captures| {
|
||||
let before = &caps[1];
|
||||
let classes = &caps[2].replace(",", " ");
|
||||
let after = &caps[3];
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let before = &caps[1];
|
||||
let classes = &caps[2].replace(",", " ");
|
||||
let after = &caps[3];
|
||||
|
||||
format!(r#"<code{before}class="{classes}"{after}>"#,
|
||||
format!(
|
||||
r#"<code{before}class="{classes}"{after}>"#,
|
||||
before = before,
|
||||
classes = classes,
|
||||
after = after)
|
||||
})
|
||||
.into_owned()
|
||||
after = after
|
||||
)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
regex.replace_all(html, |caps: &Captures| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
|
||||
if (classes.contains("language-rust") && !classes.contains("ignore")) ||
|
||||
classes.contains("mdbook-runnable")
|
||||
{
|
||||
// wrap the contents in an external pre block
|
||||
if playpen_config.editable && classes.contains("editable") ||
|
||||
text.contains("fn main") || text.contains("quick_main!")
|
||||
if (classes.contains("language-rust")
|
||||
&& !classes.contains("ignore")
|
||||
&& !classes.contains("noplaypen"))
|
||||
|| classes.contains("mdbook-runnable")
|
||||
{
|
||||
format!("<pre class=\"playpen\">{}</pre>", text)
|
||||
} else {
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
// wrap the contents in an external pre block
|
||||
if playpen_config.editable && classes.contains("editable")
|
||||
|| text.contains("fn main")
|
||||
|| text.contains("quick_main!")
|
||||
{
|
||||
format!("<pre class=\"playpen\">{}</pre>", text)
|
||||
} else {
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
|
||||
format!("<pre class=\"playpen\"><code class=\"{}\">\n# \
|
||||
#![allow(unused_variables)]\n\
|
||||
{}#fn main() {{\n\
|
||||
{}\
|
||||
#}}</code></pre>",
|
||||
classes,
|
||||
attrs,
|
||||
code)
|
||||
format!(
|
||||
"<pre class=\"playpen\"><code class=\"{}\">\n# \
|
||||
#![allow(unused_variables)]\n{}#fn main() {{\n{}#}}</code></pre>",
|
||||
classes, attrs, code
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// not language-rust, so no-op
|
||||
text.to_owned()
|
||||
}
|
||||
} else {
|
||||
// not language-rust, so no-op
|
||||
text.to_owned()
|
||||
}
|
||||
})
|
||||
.into_owned()
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn partition_source(s: &str) -> (String, String) {
|
||||
@@ -624,26 +619,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::*;
|
||||
@@ -653,46 +628,33 @@ mod tests {
|
||||
let inputs = vec![
|
||||
(
|
||||
"blah blah <h1>Foo</h1>",
|
||||
r##"blah blah <a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
|
||||
r##"blah blah <a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1>",
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
|
||||
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
|
||||
),
|
||||
(
|
||||
"<h3>Foo^bar</h3>",
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
|
||||
r##"<a class="header" href="#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
|
||||
),
|
||||
(
|
||||
"<h4></h4>",
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#" id=""><h4></h4></a>"##,
|
||||
r##"<a class="header" href="#" id=""><h4></h4></a>"##,
|
||||
),
|
||||
(
|
||||
"<h4><em>Hï</em></h4>",
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
|
||||
r##"<a class="header" href="#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1><h3>Foo</h3>",
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a><a class="header" href="./some_chapter/some_section.html#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
|
||||
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a><a class="header" href="#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
|
||||
),
|
||||
];
|
||||
|
||||
for (src, should_be) in inputs {
|
||||
let filepath = "./some_chapter/some_section.html";
|
||||
let got = build_header_links(&src, filepath);
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
// This is redundant for most cases
|
||||
let got = fix_anchor_links(&got, filepath);
|
||||
let got = build_header_links(&src);
|
||||
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,9 +1,10 @@
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use serde_json;
|
||||
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
|
||||
use serde_json;
|
||||
|
||||
use utils;
|
||||
|
||||
type StringMap = BTreeMap<String, String>;
|
||||
|
||||
@@ -15,22 +16,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 +45,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 +77,7 @@ fn find_chapter(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn render(
|
||||
@@ -90,19 +89,31 @@ fn render(
|
||||
trace!("Creating BTreeMap to inject in context");
|
||||
|
||||
let mut context = BTreeMap::new();
|
||||
let base_path = rc.evaluate_absolute("path", false)?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
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)))?;
|
||||
context.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&base_path)),
|
||||
);
|
||||
|
||||
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("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("\\", "/"))))
|
||||
})?;
|
||||
|
||||
trace!("Render template");
|
||||
|
||||
@@ -138,14 +149,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 +175,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 +206,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 +236,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|"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use utils;
|
||||
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, Helper, HelperDef, RenderContext, RenderError};
|
||||
use pulldown_cmark::{html, Event, Parser, Tag};
|
||||
use serde_json;
|
||||
|
||||
// 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 +18,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\">")?;
|
||||
|
||||
@@ -77,6 +79,8 @@ impl HelperDef for RenderToc {
|
||||
.replace("\\", "/");
|
||||
|
||||
// Add link
|
||||
rc.writer
|
||||
.write_all(&utils::fs::path_to_root(¤t).as_bytes())?;
|
||||
rc.writer.write_all(tmp.as_bytes())?;
|
||||
rc.writer.write_all(b"\"")?;
|
||||
|
||||
@@ -107,12 +111,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;
|
||||
|
||||
251
src/renderer/html_handlebars/search.rs
Normal file
251
src/renderer/html_handlebars/search.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
extern crate ammonia;
|
||||
extern crate elasticlunr;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use self::elasticlunr::Index;
|
||||
use pulldown_cmark::*;
|
||||
use serde_json;
|
||||
|
||||
use book::{Book, BookItem};
|
||||
use config::Search;
|
||||
use errors::*;
|
||||
use theme::searcher;
|
||||
use utils;
|
||||
|
||||
/// 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"]);
|
||||
let mut doc_urls = Vec::with_capacity(book.sections.len());
|
||||
|
||||
for item in book.iter() {
|
||||
render_item(&mut index, &search_config, &mut doc_urls, item)?;
|
||||
}
|
||||
|
||||
let index = write_to_json(index, &search_config, doc_urls)?;
|
||||
debug!("Writing search index ✓");
|
||||
if index.len() > 10_000_000 {
|
||||
warn!("searchindex.json is very large ({} bytes)", index.len());
|
||||
}
|
||||
|
||||
if search_config.copy_js {
|
||||
utils::fs::write_file(destination, "searchindex.json", index.as_bytes())?;
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
"searchindex.js",
|
||||
format!("window.search = {};", 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(
|
||||
index: &mut Index,
|
||||
doc_urls: &mut Vec<String>,
|
||||
anchor_base: &str,
|
||||
section_id: &Option<String>,
|
||||
items: &[&str],
|
||||
) {
|
||||
let url = if let &Some(ref id) = section_id {
|
||||
Cow::Owned(format!("{}#{}", anchor_base, id))
|
||||
} else {
|
||||
Cow::Borrowed(anchor_base)
|
||||
};
|
||||
let url = utils::collapse_whitespace(url.trim());
|
||||
let doc_ref = doc_urls.len().to_string();
|
||||
doc_urls.push(url.into());
|
||||
|
||||
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,
|
||||
doc_urls: &mut Vec<String>,
|
||||
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,
|
||||
doc_urls,
|
||||
&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,
|
||||
doc_urls,
|
||||
&anchor_base,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
|
||||
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ResultsOptions {
|
||||
limit_results: u32,
|
||||
teaser_word_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SearchindexJson {
|
||||
/// The options used for displaying search results
|
||||
results_options: ResultsOptions,
|
||||
/// The searchoptions for elasticlunr.js
|
||||
search_options: SearchOptions,
|
||||
/// Used to lookup a document's URL from an integer document ref.
|
||||
doc_urls: Vec<String>,
|
||||
/// 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 search_options = SearchOptions {
|
||||
bool: if search_config.use_boolean_and {
|
||||
SearchBool::And
|
||||
} else {
|
||||
SearchBool::Or
|
||||
},
|
||||
expand: search_config.expand,
|
||||
fields,
|
||||
};
|
||||
|
||||
let results_options = ResultsOptions {
|
||||
limit_results: search_config.limit_results,
|
||||
teaser_word_count: search_config.teaser_word_count,
|
||||
};
|
||||
|
||||
let json_contents = SearchindexJson {
|
||||
results_options,
|
||||
search_options,
|
||||
doc_urls,
|
||||
index,
|
||||
};
|
||||
|
||||
// By converting to serde_json::Value as an intermediary, we use a
|
||||
// BTreeMap internally and can force a stable ordering of map keys.
|
||||
let json_contents = serde_json::to_value(&json_contents)?;
|
||||
let json_contents = serde_json::to_string(&json_contents)?;
|
||||
|
||||
Ok(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()
|
||||
}
|
||||
@@ -15,16 +15,16 @@ pub use self::html_handlebars::HtmlHandlebars;
|
||||
|
||||
mod html_handlebars;
|
||||
|
||||
use serde_json;
|
||||
use shlex::Shlex;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use serde_json;
|
||||
use shlex::Shlex;
|
||||
|
||||
use errors::*;
|
||||
use config::Config;
|
||||
use book::Book;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
|
||||
const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -162,17 +162,21 @@ impl Renderer for CmdRenderer {
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.current_dir(&ctx.destination)
|
||||
.spawn() {
|
||||
Ok(c) => c,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
warn!("The command wasn't found, is the \"{}\" backend installed?", self.name);
|
||||
warn!("\tCommand: {}", self.cmd);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e).chain_err(|| "Unable to start the backend")?;
|
||||
}
|
||||
};
|
||||
.spawn()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
warn!(
|
||||
"The command wasn't found, is the \"{}\" backend installed?",
|
||||
self.name
|
||||
);
|
||||
warn!("\tCommand: {}", self.cmd);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e).chain_err(|| "Unable to start the backend")?;
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
let mut stdin = child.stdin.take().expect("Child has stdin");
|
||||
|
||||
|
Before Width: | Height: | Size: 348 KiB After Width: | Height: | Size: 348 KiB |
1222
src/theme/book.css
1222
src/theme/book.css
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
// Fix back button cache problem
|
||||
window.onunload = function () { };
|
||||
|
||||
@@ -16,13 +18,30 @@ function playpen_text(playpen) {
|
||||
(function codeSnippets() {
|
||||
// Hide Rust code lines prepended with a specific character
|
||||
var hiding_character = "#";
|
||||
var request = fetch("https://play.rust-lang.org/meta/crates", {
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
});
|
||||
|
||||
function fetch_with_timeout(url, options, timeout = 6000) {
|
||||
return Promise.race([
|
||||
fetch(url, options),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
|
||||
]);
|
||||
}
|
||||
|
||||
var playpens = Array.from(document.querySelectorAll(".playpen"));
|
||||
if (playpens.length > 0) {
|
||||
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
// get list of crates available in the rust playground
|
||||
let playground_crates = response.crates.map(item => item["id"]);
|
||||
playpens.forEach(block => handle_crate_list_update(block, playground_crates));
|
||||
});
|
||||
}
|
||||
|
||||
function handle_crate_list_update(playpen_block, playground_crates) {
|
||||
// update the play buttons after receiving the response
|
||||
@@ -55,6 +74,7 @@ function playpen_text(playpen) {
|
||||
var txt = playpen_text(pre_block);
|
||||
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
|
||||
var snippet_crates = [];
|
||||
var item;
|
||||
while (item = re.exec(txt)) {
|
||||
snippet_crates.push(item[1]);
|
||||
}
|
||||
@@ -83,32 +103,28 @@ function playpen_text(playpen) {
|
||||
let text = playpen_text(code_block);
|
||||
|
||||
var params = {
|
||||
channel: "stable",
|
||||
mode: "debug",
|
||||
crateType: "bin",
|
||||
tests: false,
|
||||
code: text,
|
||||
}
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
code: text
|
||||
};
|
||||
|
||||
if (text.indexOf("#![feature") !== -1) {
|
||||
params.channel = "nightly";
|
||||
params.version = "nightly";
|
||||
}
|
||||
|
||||
result_block.innerText = "Running...";
|
||||
|
||||
var request = fetch("https://play.rust-lang.org/execute", {
|
||||
fetch_with_timeout("https://play.rust-lang.org/evaluate.json", {
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
body: JSON.stringify(params)
|
||||
});
|
||||
|
||||
request
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (response) { result_block.innerText = response.success ? response.stdout : response.stderr; })
|
||||
.catch(function (error) { result_block.innerText = "Playground communication" + error.message; });
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => result_block.innerText = response.result)
|
||||
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
|
||||
}
|
||||
|
||||
// Syntax highlighting Configuration
|
||||
@@ -147,9 +163,11 @@ function playpen_text(playpen) {
|
||||
var lines = code_block.innerHTML.split("\n");
|
||||
var first_non_hidden_line = false;
|
||||
var lines_hidden = false;
|
||||
var trimmed_line = "";
|
||||
|
||||
for (var n = 0; n < lines.length; n++) {
|
||||
if (lines[n].trim()[0] == hiding_character) {
|
||||
trimmed_line = lines[n].trim();
|
||||
if (trimmed_line[0] == hiding_character && trimmed_line[1] != hiding_character) {
|
||||
if (first_non_hidden_line) {
|
||||
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)# ?/, "$1") + "</span>";
|
||||
}
|
||||
@@ -164,6 +182,9 @@ function playpen_text(playpen) {
|
||||
else {
|
||||
first_non_hidden_line = true;
|
||||
}
|
||||
if (trimmed_line[0] == hiding_character && trimmed_line[1] == hiding_character) {
|
||||
lines[n] = lines[n].replace("##", "#")
|
||||
}
|
||||
}
|
||||
code_block.innerHTML = lines.join("");
|
||||
|
||||
@@ -175,7 +196,7 @@ function playpen_text(playpen) {
|
||||
buttons.innerHTML = "<button class=\"fa fa-expand\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
|
||||
|
||||
// add expand button
|
||||
pre_block.prepend(buttons);
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
|
||||
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('fa-expand')) {
|
||||
@@ -213,7 +234,7 @@ function playpen_text(playpen) {
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.prepend(buttons);
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
var clipButton = document.createElement('button');
|
||||
@@ -222,7 +243,7 @@ function playpen_text(playpen) {
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
|
||||
|
||||
buttons.prepend(clipButton);
|
||||
buttons.insertBefore(clipButton, buttons.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -233,7 +254,7 @@ function playpen_text(playpen) {
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.prepend(buttons);
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
var runCodeButton = document.createElement('button');
|
||||
@@ -248,8 +269,8 @@ function playpen_text(playpen) {
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
|
||||
buttons.prepend(runCodeButton);
|
||||
buttons.prepend(copyCodeClipboardButton);
|
||||
buttons.insertBefore(runCodeButton, buttons.firstChild);
|
||||
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
|
||||
|
||||
runCodeButton.addEventListener('click', function (e) {
|
||||
run_rust_code(pre_block);
|
||||
@@ -262,7 +283,7 @@ function playpen_text(playpen) {
|
||||
undoChangesButton.title = 'Undo changes';
|
||||
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
|
||||
|
||||
buttons.prepend(undoChangesButton);
|
||||
buttons.insertBefore(undoChangesButton, buttons.firstChild);
|
||||
|
||||
undoChangesButton.addEventListener('click', function () {
|
||||
let editor = window.ace.edit(code_block);
|
||||
@@ -271,17 +292,6 @@ function playpen_text(playpen) {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
request
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (response) {
|
||||
// get list of crates available in the rust playground
|
||||
let playground_crates = response.crates.map(function (item) { return item["id"]; });
|
||||
Array.from(document.querySelectorAll(".playpen")).forEach(function (block) {
|
||||
handle_crate_list_update(block, playground_crates);
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
(function themes() {
|
||||
@@ -290,19 +300,21 @@ function playpen_text(playpen) {
|
||||
var themePopup = document.getElementById('theme-list');
|
||||
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
|
||||
var stylesheets = {
|
||||
ayuHighlight: document.querySelector("[href='ayu-highlight.css']"),
|
||||
tomorrowNight: document.querySelector("[href='tomorrow-night.css']"),
|
||||
highlight: document.querySelector("[href='highlight.css']"),
|
||||
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
|
||||
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
|
||||
highlight: document.querySelector("[href$='highlight.css']"),
|
||||
};
|
||||
|
||||
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 +381,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 && !themeToggleButton.contains(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 +509,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 +567,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');
|
||||
|
||||
|
||||
417
src/theme/css/chrome.css
Normal file
417
src/theme/css/chrome.css
Normal file
@@ -0,0 +1,417 @@
|
||||
/* CSS for UI elements (a.k.a. chrome) */
|
||||
|
||||
@import 'variables.css';
|
||||
|
||||
::-webkit-scrollbar {
|
||||
background: var(--bg);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar);
|
||||
}
|
||||
|
||||
#searchresults a,
|
||||
.content a:link,
|
||||
a:visited,
|
||||
a > .hljs {
|
||||
color: var(--links);
|
||||
}
|
||||
|
||||
/* Menu Bar */
|
||||
|
||||
#menu-bar {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 101;
|
||||
margin: auto calc(0px - var(--page-padding));
|
||||
}
|
||||
#menu-bar > #menu-bar-sticky-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: var(--bg);
|
||||
border-bottom-color: var(--bg);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
.js #menu-bar > #menu-bar-sticky-container {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
#menu-bar.bordered > #menu-bar-sticky-container {
|
||||
border-bottom-color: var(--table-border-color);
|
||||
}
|
||||
#menu-bar i, #menu-bar .icon-button {
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
z-index: 10;
|
||||
line-height: 50px;
|
||||
cursor: pointer;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
@media only screen and (max-width: 420px) {
|
||||
#menu-bar i, #menu-bar .icon-button {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
.icon-button i {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#print-button {
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
|
||||
transform: translateY(-60px);
|
||||
}
|
||||
|
||||
.left-buttons {
|
||||
display: flex;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.no-js .left-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
display: inline-block;
|
||||
font-weight: 200;
|
||||
font-size: 20px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.js .menu-title {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-bar,
|
||||
.menu-bar:visited,
|
||||
.nav-chapters,
|
||||
.nav-chapters:visited,
|
||||
.mobile-nav-chapters,
|
||||
.mobile-nav-chapters:visited,
|
||||
.menu-bar .icon-button,
|
||||
.menu-bar a i {
|
||||
color: var(--icons);
|
||||
}
|
||||
|
||||
.menu-bar i:hover,
|
||||
.menu-bar .icon-button:hover,
|
||||
.nav-chapters:hover,
|
||||
.mobile-nav-chapters i:hover {
|
||||
color: var(--icons-hover);
|
||||
}
|
||||
|
||||
/* Nav Icons */
|
||||
|
||||
.nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
|
||||
position: fixed;
|
||||
top: 50px; /* Height of menu-bar */
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
max-width: 150px;
|
||||
min-width: 90px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.nav-chapters:hover { text-decoration: none; }
|
||||
|
||||
.nav-wrapper {
|
||||
margin-top: 50px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
width: 90px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
.previous {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.next {
|
||||
float: right;
|
||||
right: var(--page-padding);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1080px) {
|
||||
.nav-wide-wrapper { display: none; }
|
||||
.nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1380px) {
|
||||
.sidebar-visible .nav-wide-wrapper { display: none; }
|
||||
.sidebar-visible .nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
|
||||
:not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
color: var(--inline-code-color);
|
||||
}
|
||||
|
||||
a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
}
|
||||
pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
|
||||
color: var(--sidebar-fg);
|
||||
cursor: pointer;
|
||||
}
|
||||
pre > .buttons :hover {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
pre > .buttons i {
|
||||
margin-left: 8px;
|
||||
}
|
||||
pre > .buttons button {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: inherit;
|
||||
}
|
||||
pre > .result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
|
||||
#searchresults a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
mark {
|
||||
border-radius: 2px;
|
||||
padding: 0 3px 1px 3px;
|
||||
margin: 0 -3px -1px -3px;
|
||||
background-color: var(--search-mark-bg);
|
||||
transition: background-color 300ms linear;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
mark.fade-out {
|
||||
background-color: rgba(0,0,0,0) !important;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.searchbar-outer {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
|
||||
#searchbar {
|
||||
width: 100%;
|
||||
margin: 5px auto 0px auto;
|
||||
padding: 10px 16px;
|
||||
transition: box-shadow 300ms ease-in-out;
|
||||
border: 1px solid var(--searchbar-border-color);
|
||||
border-radius: 3px;
|
||||
background-color: var(--searchbar-bg);
|
||||
color: var(--searchbar-fg);
|
||||
}
|
||||
#searchbar:focus,
|
||||
#searchbar.active {
|
||||
box-shadow: 0 0 3px var(--searchbar-shadow-color);
|
||||
}
|
||||
|
||||
.searchresults-header {
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
padding: 18px 0 0 5px;
|
||||
color: var(--searchresults-header-fg);
|
||||
}
|
||||
|
||||
.searchresults-outer {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: var(--content-max-width);
|
||||
border-bottom: 1px dashed var(--searchresults-border-color);
|
||||
}
|
||||
|
||||
ul#searchresults {
|
||||
list-style: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
ul#searchresults li {
|
||||
margin: 10px 0px;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
ul#searchresults li.focus {
|
||||
background-color: var(--searchresults-li-bg);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--sidebar-width);
|
||||
overflow-y: auto;
|
||||
padding: 10px 10px;
|
||||
font-size: 0.875em;
|
||||
box-sizing: border-box;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.js .sidebar {
|
||||
transition: transform 0.3s; /* Animation: slide away */
|
||||
}
|
||||
.sidebar code {
|
||||
line-height: 2em;
|
||||
}
|
||||
.sidebar-hidden .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width)));
|
||||
}
|
||||
.sidebar::-webkit-scrollbar {
|
||||
background: var(--sidebar-bg);
|
||||
}
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar);
|
||||
}
|
||||
|
||||
.sidebar-visible .page-wrapper {
|
||||
transform: translateX(var(--sidebar-width));
|
||||
}
|
||||
@media only screen and (min-width: 620px) {
|
||||
.sidebar-visible .page-wrapper {
|
||||
transform: none;
|
||||
margin-left: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
|
||||
.chapter {
|
||||
list-style: none outside none;
|
||||
padding-left: 0;
|
||||
line-height: 2.2em;
|
||||
}
|
||||
.chapter li {
|
||||
color: var(--sidebar-non-existant);
|
||||
}
|
||||
.chapter li a {
|
||||
color: var(--sidebar-fg);
|
||||
display: block;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.chapter li a:hover { text-decoration: none }
|
||||
.chapter li .active,
|
||||
a:hover {
|
||||
/* Animate color change */
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
.chapter .spacer {
|
||||
background-color: var(--sidebar-spacer);
|
||||
}
|
||||
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
.chapter li a { padding: 5px 0; }
|
||||
.spacer { margin: 10px 0; }
|
||||
}
|
||||
|
||||
.section {
|
||||
list-style: none outside none;
|
||||
padding-left: 20px;
|
||||
line-height: 1.9em;
|
||||
}
|
||||
|
||||
/* Theme Menu Popup */
|
||||
|
||||
.theme-popup {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50px;
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7em;
|
||||
color: var(--fg);
|
||||
background: var(--theme-popup-bg);
|
||||
border: 1px solid var(--theme-popup-border);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: none;
|
||||
}
|
||||
.theme-popup .default {
|
||||
color: var(--icons);
|
||||
}
|
||||
.theme-popup .theme {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 2px 10px;
|
||||
line-height: 25px;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
background: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
.theme-popup .theme:hover {
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
.theme-popup .theme:hover:first-child,
|
||||
.theme-popup .theme:hover:last-child {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
}
|
||||
144
src/theme/css/general.css
Normal file
144
src/theme/css/general.css
Normal file
@@ -0,0 +1,144 @@
|
||||
/* Base styles and content styles */
|
||||
|
||||
@import 'variables.css';
|
||||
|
||||
html {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
color: var(--fg);
|
||||
background-color: var(--bg);
|
||||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
|
||||
}
|
||||
|
||||
.left { float: left; }
|
||||
.right { float: right; }
|
||||
.hidden { display: none; }
|
||||
.play-button.hidden { display: none; }
|
||||
|
||||
h2, h3 { margin-top: 2.5em; }
|
||||
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;
|
||||
}
|
||||
|
||||
.page {
|
||||
outline: 0;
|
||||
padding: 0 var(--page-padding);
|
||||
}
|
||||
.page-wrapper {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.js .page-wrapper {
|
||||
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
padding: 0 15px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
.content main {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
.content a { text-decoration: none; }
|
||||
.content a:hover { text-decoration: underline; }
|
||||
.content img { max-width: 100%; }
|
||||
.content .header:link,
|
||||
.content .header:visited {
|
||||
color: var(--fg);
|
||||
}
|
||||
.content .header:link,
|
||||
.content .header:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table td {
|
||||
padding: 3px 20px;
|
||||
border: 1px var(--table-border-color) solid;
|
||||
}
|
||||
table thead {
|
||||
background: var(--table-header-bg);
|
||||
}
|
||||
table thead td {
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
table thead tr {
|
||||
border: 1px var(--table-header-bg) solid;
|
||||
}
|
||||
/* Alternate background colors for rows */
|
||||
table tbody tr:nth-child(2n) {
|
||||
background: var(--table-alternate-bg);
|
||||
}
|
||||
|
||||
|
||||
blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
color: var(--fg);
|
||||
background-color: var(--quote-bg);
|
||||
border-top: .1em solid var(--quote-border);
|
||||
border-bottom: .1em solid var(--quote-border);
|
||||
}
|
||||
|
||||
|
||||
: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;
|
||||
}
|
||||
|
||||
.tooltiptext {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
|
||||
left: -8px; /* Half of the width of the icon */
|
||||
top: -35px;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px 8px;
|
||||
margin: 5px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.tooltipped .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
54
src/theme/css/print.css
Normal file
54
src/theme/css/print.css
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
#sidebar,
|
||||
#menu-bar,
|
||||
.nav-chapters,
|
||||
.mobile-nav-chapters {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#page-wrapper.page-wrapper {
|
||||
transform: none;
|
||||
margin-left: 0px;
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
#content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page {
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #666666;
|
||||
border-radius: 5px;
|
||||
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
|
||||
pre > .buttons {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
a, a:visited, a:active, a:hover {
|
||||
color: #4183c4;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
page-break-inside: avoid;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.fa {
|
||||
display: none !important;
|
||||
}
|
||||
210
src/theme/css/variables.css
Normal file
210
src/theme/css/variables.css
Normal file
@@ -0,0 +1,210 @@
|
||||
|
||||
/* Globals */
|
||||
|
||||
:root {
|
||||
--sidebar-width: 300px;
|
||||
--page-padding: 15px;
|
||||
--content-max-width: 750px;
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
|
||||
.ayu {
|
||||
--bg: #0f1419;
|
||||
--fg: #c5c5c5;
|
||||
|
||||
--sidebar-bg: #14191f;
|
||||
--sidebar-fg: #c8c9db;
|
||||
--sidebar-non-existant: #5c6773;
|
||||
--sidebar-active: #ffb454;
|
||||
--sidebar-spacer: #2d334f;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #737480;
|
||||
--icons-hover: #b7b9cc;
|
||||
|
||||
--links: #0096cf;
|
||||
|
||||
--inline-code-color: #ffb454;
|
||||
|
||||
--theme-popup-bg: #14191f;
|
||||
--theme-popup-border: #5c6773;
|
||||
--theme-hover: #191f26;
|
||||
|
||||
--quote-bg: #262933;
|
||||
--quote-border: lighten(var(--quote-bg), 5%);
|
||||
|
||||
--table-border-color: lighten(var(--bg), 5%);
|
||||
--table-header-bg: lighten(var(--bg), 20%);
|
||||
--table-alternate-bg: lighten(var(--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;
|
||||
}
|
||||
|
||||
.coal {
|
||||
--bg: #141617;
|
||||
--fg: #98a3ad;
|
||||
|
||||
--sidebar-bg: #292c2f;
|
||||
--sidebar-fg: #a1adb8;
|
||||
--sidebar-non-existant: #505254;
|
||||
--sidebar-active: #3473ad;
|
||||
--sidebar-spacer: #393939;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #43484d;
|
||||
--icons-hover: #b3c0cc;
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;;
|
||||
|
||||
--theme-popup-bg: #141617;
|
||||
--theme-popup-border: #43484d;
|
||||
--theme-hover: #1f2124;
|
||||
|
||||
--quote-bg: #242637;
|
||||
--quote-border: lighten(var(--quote-bg), 5%);
|
||||
|
||||
--table-border-color: lighten(var(--bg), 5%);
|
||||
--table-header-bg: lighten(var(--bg), 20%);
|
||||
--table-alternate-bg: lighten(var(--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;
|
||||
}
|
||||
|
||||
.light {
|
||||
--bg: #ffffff;
|
||||
--fg: #333333;
|
||||
|
||||
--sidebar-bg: #fafafa;
|
||||
--sidebar-fg: #364149;
|
||||
--sidebar-non-existant: #aaaaaa;
|
||||
--sidebar-active: #008cff;
|
||||
--sidebar-spacer: #f4f4f4;
|
||||
|
||||
--scrollbar: #cccccc;
|
||||
|
||||
--icons: #cccccc;
|
||||
--icons-hover: #333333;
|
||||
|
||||
--links: #4183c4;
|
||||
|
||||
--inline-code-color: #6e6b5e;
|
||||
|
||||
--theme-popup-bg: #fafafa;
|
||||
--theme-popup-border: #cccccc;
|
||||
--theme-hover: #e6e6e6;
|
||||
|
||||
--quote-bg: #f2f7f9;
|
||||
--quote-border: darken(var(--quote-bg), 5%);
|
||||
|
||||
--table-border-color: darken(var(--bg), 5%);
|
||||
--table-header-bg: darken(var(--bg), 20%);
|
||||
--table-alternate-bg: darken(var(--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;
|
||||
}
|
||||
|
||||
.navy {
|
||||
--bg: #161923;
|
||||
--fg: #bcbdd0;
|
||||
|
||||
--sidebar-bg: #282d3f;
|
||||
--sidebar-fg: #c8c9db;
|
||||
--sidebar-non-existant: #505274;
|
||||
--sidebar-active: #2b79a2;
|
||||
--sidebar-spacer: #2d334f;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #737480;
|
||||
--icons-hover: #b7b9cc;
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;;
|
||||
|
||||
--theme-popup-bg: #161923;
|
||||
--theme-popup-border: #737480;
|
||||
--theme-hover: #282e40;
|
||||
|
||||
--quote-bg: #262933;
|
||||
--quote-border: lighten(var(--quote-bg), 5%);
|
||||
|
||||
--table-border-color: lighten(var(--bg), 5%);
|
||||
--table-header-bg: lighten(var(--bg), 20%);
|
||||
--table-alternate-bg: lighten(var(--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;
|
||||
}
|
||||
|
||||
.rust {
|
||||
--bg: #e1e1db;
|
||||
--fg: #262625;
|
||||
|
||||
--sidebar-bg: #3b2e2a;
|
||||
--sidebar-fg: #c8c9db;
|
||||
--sidebar-non-existant: #505254;
|
||||
--sidebar-active: #e69f67;
|
||||
--sidebar-spacer: #45373a;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #737480;
|
||||
--icons-hover: #262625;
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #6e6b5e;
|
||||
|
||||
--theme-popup-bg: #e1e1db;
|
||||
--theme-popup-border: #b38f6b;
|
||||
--theme-hover: #99908a;
|
||||
|
||||
--quote-bg: #c1c1bb;
|
||||
--quote-border: darken(var(--quote-bg), 5%);
|
||||
|
||||
--table-border-color: darken(var(--bg), 5%);
|
||||
--table-header-bg: #b3a497;
|
||||
--table-alternate-bg: darken(var(--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;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}">
|
||||
<html lang="{{ language }}" class="sidebar-visible no-js">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
@@ -8,41 +9,36 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
<base href="{{ path_to_root }}">
|
||||
<link rel="shortcut icon" href="{{ path_to_root }}{{ favicon }}">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
|
||||
|
||||
<link rel="stylesheet" href="book.css">
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
|
||||
|
||||
<link rel="shortcut icon" href="{{ favicon }}">
|
||||
<!-- Highlight.js Stylesheets -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}highlight.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}tomorrow-night.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}ayu-highlight.css">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
|
||||
|
||||
<link rel="stylesheet" href="highlight.css">
|
||||
<link rel="stylesheet" href="tomorrow-night.css">
|
||||
<link rel="stylesheet" href="ayu-highlight.css">
|
||||
|
||||
<!-- Custom theme -->
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{this}}">
|
||||
<link rel="stylesheet" href="{{ this }}">
|
||||
{{/each}}
|
||||
|
||||
{{#if mathjax_support}}
|
||||
<!-- MathJax -->
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{{/if}}
|
||||
|
||||
<!-- Fetch Clipboard.js from CDN but have a local fallback -->
|
||||
<script src="https://cdn.jsdelivr.net/clipboard.js/1.6.1/clipboard.min.js"></script>
|
||||
<script>
|
||||
if (typeof Clipboard == 'undefined') {
|
||||
document.write(unescape("%3Cscript src='clipboard.min.js'%3E%3C/script%3E"));
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body class="light">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script type="text/javascript">var path_to_root = "{{ path_to_root }}";</script>
|
||||
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script type="text/javascript">
|
||||
try {
|
||||
@@ -65,17 +61,19 @@
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||
document.body.className = theme;
|
||||
document.querySelector('html').className = theme;
|
||||
document.querySelector('html').className = theme + ' js';
|
||||
</script>
|
||||
|
||||
<!-- 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">
|
||||
@@ -95,25 +93,43 @@
|
||||
<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">
|
||||
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
|
||||
<i id="print-button" class="fa fa-print"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if search_enabled}}
|
||||
<div id="search-wrapper" class="hidden">
|
||||
<form id="searchbar-outer" class="searchbar-outer">
|
||||
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
</form>
|
||||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||
<div id="searchresults-header" class="searchresults-header"></div>
|
||||
<ul id="searchresults">
|
||||
</ul>
|
||||
</div>
|
||||
</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');
|
||||
@@ -131,13 +147,13 @@
|
||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||
<!-- Mobile navigation buttons -->
|
||||
{{#previous}}
|
||||
<a rel="prev" href="{{link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<a rel="prev" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
|
||||
{{#next}}
|
||||
<a rel="next" href="{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
{{/next}}
|
||||
@@ -149,13 +165,13 @@
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<a href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
|
||||
{{#next}}
|
||||
<a href="{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<a href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
{{/next}}
|
||||
@@ -163,18 +179,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Local fallback for Font Awesome -->
|
||||
<script>
|
||||
if (getComputedStyle(document.querySelector(".fa")).fontFamily !== "FontAwesome") {
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = '_FontAwesome/css/font-awesome.css';
|
||||
document.head.insertBefore(link, document.head.firstChild)
|
||||
}
|
||||
</script>
|
||||
|
||||
{{#if livereload}}
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
<script type="text/javascript">
|
||||
@@ -194,7 +198,7 @@
|
||||
|
||||
{{#if google_analytics}}
|
||||
<!-- Google Analytics Tag -->
|
||||
<script>
|
||||
<script type="text/javascript">
|
||||
var localAddrs = ["localhost", "127.0.0.1", ""];
|
||||
|
||||
// make sure we don't activate google analytics if the developer is
|
||||
@@ -211,29 +215,46 @@
|
||||
</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="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
|
||||
{{/if}}
|
||||
|
||||
{{#if search_js}}
|
||||
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||
{{/if}}
|
||||
|
||||
<script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script>
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
{{#each additional_js}}
|
||||
<script type="text/javascript" src="{{ path_to_root }}{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
{{#if is_print}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.print();
|
||||
})
|
||||
{{#if mathjax_support}}
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', function() {
|
||||
MathJax.Hub.Register.StartupHook('End', function() {
|
||||
window.setTimeout(window.print, 100);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{else}}
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', function() {
|
||||
window.setTimeout(window.print, 100);
|
||||
});
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
<script src="highlight.js"></script>
|
||||
<script src="book.js"></script>
|
||||
|
||||
<!-- Custom JS script -->
|
||||
{{#each additional_js}}
|
||||
<script type="text/javascript" src="{{this}}"></script>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
100
src/theme/mod.rs
100
src/theme/mod.rs
@@ -1,15 +1,22 @@
|
||||
#![allow(missing_docs)] // FIXME: Document this
|
||||
#![allow(missing_docs)]
|
||||
|
||||
pub mod playpen_editor;
|
||||
|
||||
use std::path::Path;
|
||||
#[cfg(feature = "search")]
|
||||
pub mod searcher;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
||||
use errors::*;
|
||||
|
||||
pub static INDEX: &'static [u8] = include_bytes!("index.hbs");
|
||||
pub static HEADER: &'static [u8] = include_bytes!("header.hbs");
|
||||
pub static CSS: &'static [u8] = include_bytes!("book.css");
|
||||
pub static CHROME_CSS: &'static [u8] = include_bytes!("css/chrome.css");
|
||||
pub static GENERAL_CSS: &'static [u8] = include_bytes!("css/general.css");
|
||||
pub static PRINT_CSS: &'static [u8] = include_bytes!("css/print.css");
|
||||
pub static VARIABLES_CSS: &'static [u8] = include_bytes!("css/variables.css");
|
||||
pub static FAVICON: &'static [u8] = include_bytes!("favicon.png");
|
||||
pub static JS: &'static [u8] = include_bytes!("book.js");
|
||||
pub static HIGHLIGHT_JS: &'static [u8] = include_bytes!("highlight.js");
|
||||
@@ -17,22 +24,21 @@ pub static TOMORROW_NIGHT_CSS: &'static [u8] = include_bytes!("tomorrow-night.cs
|
||||
pub static HIGHLIGHT_CSS: &'static [u8] = include_bytes!("highlight.css");
|
||||
pub static AYU_HIGHLIGHT_CSS: &'static [u8] = include_bytes!("ayu-highlight.css");
|
||||
pub static CLIPBOARD_JS: &'static [u8] = include_bytes!("clipboard.min.js");
|
||||
pub static FONT_AWESOME: &'static [u8] = include_bytes!("_FontAwesome/css/font-awesome.min.css");
|
||||
pub static FONT_AWESOME: &'static [u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
|
||||
pub static FONT_AWESOME_EOT: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.eot");
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
|
||||
pub static FONT_AWESOME_SVG: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff");
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
|
||||
pub static FONT_AWESOME_WOFF2: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff2");
|
||||
pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/FontAwesome.otf");
|
||||
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff2");
|
||||
pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
|
||||
|
||||
/// The `Theme` struct should be used instead of the static variables because
|
||||
/// the `new()` method will look if the user has a theme directory in his
|
||||
/// the `new()` method will look if the user has a theme directory in their
|
||||
/// source folder and use the users theme instead of the default.
|
||||
///
|
||||
/// You should only ever use the static variables directly if you want to
|
||||
@@ -41,7 +47,10 @@ pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/
|
||||
pub struct Theme {
|
||||
pub index: Vec<u8>,
|
||||
pub header: Vec<u8>,
|
||||
pub css: Vec<u8>,
|
||||
pub chrome_css: Vec<u8>,
|
||||
pub general_css: Vec<u8>,
|
||||
pub print_css: Vec<u8>,
|
||||
pub variables_css: Vec<u8>,
|
||||
pub favicon: Vec<u8>,
|
||||
pub js: Vec<u8>,
|
||||
pub highlight_css: Vec<u8>,
|
||||
@@ -52,6 +61,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();
|
||||
@@ -67,13 +78,25 @@ impl Theme {
|
||||
(theme_dir.join("index.hbs"), &mut theme.index),
|
||||
(theme_dir.join("header.hbs"), &mut theme.header),
|
||||
(theme_dir.join("book.js"), &mut theme.js),
|
||||
(theme_dir.join("book.css"), &mut theme.css),
|
||||
(theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
|
||||
(theme_dir.join("css/general.css"), &mut theme.general_css),
|
||||
(theme_dir.join("css/print.css"), &mut theme.print_css),
|
||||
(
|
||||
theme_dir.join("css/variables.css"),
|
||||
&mut theme.variables_css,
|
||||
),
|
||||
(theme_dir.join("favicon.png"), &mut theme.favicon),
|
||||
(theme_dir.join("highlight.js"), &mut theme.highlight_js),
|
||||
(theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
|
||||
(theme_dir.join("highlight.css"), &mut theme.highlight_css),
|
||||
(theme_dir.join("tomorrow-night.css"), &mut theme.tomorrow_night_css),
|
||||
(theme_dir.join("ayu-highlight.css"), &mut theme.ayu_highlight_css),
|
||||
(
|
||||
theme_dir.join("tomorrow-night.css"),
|
||||
&mut theme.tomorrow_night_css,
|
||||
),
|
||||
(
|
||||
theme_dir.join("ayu-highlight.css"),
|
||||
&mut theme.ayu_highlight_css,
|
||||
),
|
||||
];
|
||||
|
||||
for (filename, dest) in files {
|
||||
@@ -96,7 +119,10 @@ impl Default for Theme {
|
||||
Theme {
|
||||
index: INDEX.to_owned(),
|
||||
header: HEADER.to_owned(),
|
||||
css: CSS.to_owned(),
|
||||
chrome_css: CHROME_CSS.to_owned(),
|
||||
general_css: GENERAL_CSS.to_owned(),
|
||||
print_css: PRINT_CSS.to_owned(),
|
||||
variables_css: VARIABLES_CSS.to_owned(),
|
||||
favicon: FAVICON.to_owned(),
|
||||
js: JS.to_owned(),
|
||||
highlight_css: HIGHLIGHT_CSS.to_owned(),
|
||||
@@ -124,12 +150,12 @@ fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempdir::TempDir;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
|
||||
#[test]
|
||||
fn theme_uses_defaults_with_nonexistent_src_dir() {
|
||||
@@ -144,21 +170,28 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn theme_dir_overrides_defaults() {
|
||||
// Get all the non-Rust files in the theme directory
|
||||
let special_files = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src/theme")
|
||||
.read_dir()
|
||||
.unwrap()
|
||||
.filter_map(|f| f.ok())
|
||||
.map(|f| f.path())
|
||||
.filter(|p| p.is_file() && !p.ends_with(".rs"));
|
||||
let files = [
|
||||
"index.hbs",
|
||||
"header.hbs",
|
||||
"favicon.png",
|
||||
"css/chrome.css",
|
||||
"css/general.css",
|
||||
"css/print.css",
|
||||
"css/variables.css",
|
||||
"book.js",
|
||||
"highlight.js",
|
||||
"tomorrow-night.css",
|
||||
"highlight.css",
|
||||
"ayu-highlight.css",
|
||||
"clipboard.min.js",
|
||||
];
|
||||
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
|
||||
fs::create_dir(temp.path().join("css")).unwrap();
|
||||
|
||||
// "touch" all of the special files so we have empty copies
|
||||
for special_file in special_files {
|
||||
let filename = temp.path().join(special_file.file_name().unwrap());
|
||||
let _ = File::create(&filename);
|
||||
for file in &files {
|
||||
File::create(&temp.path().join(file)).unwrap();
|
||||
}
|
||||
|
||||
let got = Theme::new(temp.path());
|
||||
@@ -166,7 +199,10 @@ mod tests {
|
||||
let empty = Theme {
|
||||
index: Vec::new(),
|
||||
header: Vec::new(),
|
||||
css: Vec::new(),
|
||||
chrome_css: Vec::new(),
|
||||
general_css: Vec::new(),
|
||||
print_css: Vec::new(),
|
||||
variables_css: Vec::new(),
|
||||
favicon: Vec::new(),
|
||||
js: Vec::new(),
|
||||
highlight_css: Vec::new(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
"use strict";
|
||||
window.editors = [];
|
||||
(function(editors) {
|
||||
if (typeof(ace) === 'undefined' || !ace) {
|
||||
@@ -11,7 +12,8 @@ window.editors = [];
|
||||
showPrintMargin: false,
|
||||
showLineNumbers: false,
|
||||
showGutter: false,
|
||||
maxLines: Infinity
|
||||
maxLines: Infinity,
|
||||
fontSize: "0.875em" // please adjust the font size of the code in general.styl
|
||||
});
|
||||
|
||||
editor.$blockScrolling = Infinity;
|
||||
|
||||
@@ -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");
|
||||
477
src/theme/searcher/searcher.js
Normal file
477
src/theme/searcher/searcher.js
Normal file
@@ -0,0 +1,477 @@
|
||||
"use strict";
|
||||
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 their search.
|
||||
|
||||
if (!Mark || !elasticlunr) {
|
||||
return;
|
||||
}
|
||||
|
||||
//IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
||||
if (!String.prototype.startsWith) {
|
||||
String.prototype.startsWith = function(search, pos) {
|
||||
return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
|
||||
};
|
||||
}
|
||||
|
||||
var search_wrap = document.getElementById('search-wrapper'),
|
||||
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,
|
||||
doc_urls = [],
|
||||
results_options = {
|
||||
teaser_word_count: 30,
|
||||
limit_results: 30,
|
||||
},
|
||||
search_options = {
|
||||
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 = doc_urls[result.ref].split("#");
|
||||
if (url.length == 1) { // no anchor found
|
||||
url.push("");
|
||||
}
|
||||
|
||||
return '<a href="' + path_to_root + 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, results_options.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(config) {
|
||||
results_options = config.results_options;
|
||||
search_options = config.search_options;
|
||||
searchbar_outer = config.searchbar_outer;
|
||||
doc_urls = config.doc_urls;
|
||||
searchindex = elasticlunr.Index.load(config.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(); };
|
||||
// Suppress "submit" events so the page doesn't reload when the user presses Enter
|
||||
document.addEventListener('submit', function(e) { e.preventDefault(); }, false);
|
||||
|
||||
// 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 || e.target.type === 'textarea') { 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();
|
||||
} else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) {
|
||||
e.preventDefault();
|
||||
showSearch(true);
|
||||
window.scrollTo(0, 0);
|
||||
searchbar.select();
|
||||
} else if (hasFocus() && e.keyCode === DOWN_KEYCODE) {
|
||||
e.preventDefault();
|
||||
unfocusSearchbar();
|
||||
searchresults.firstElementChild.classList.add("focus");
|
||||
} else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE
|
||||
|| e.keyCode === UP_KEYCODE
|
||||
|| e.keyCode === SELECT_KEYCODE)) {
|
||||
// not `:focus` because browser does annoying scrolling
|
||||
var focused = searchresults.querySelector("li.focus");
|
||||
if (!focused) return;
|
||||
e.preventDefault();
|
||||
if (e.keyCode === DOWN_KEYCODE) {
|
||||
var next = focused.nextElementSibling;
|
||||
if (next) {
|
||||
focused.classList.remove("focus");
|
||||
next.classList.add("focus");
|
||||
}
|
||||
} else if (e.keyCode === UP_KEYCODE) {
|
||||
focused.classList.remove("focus");
|
||||
var prev = focused.previousElementSibling;
|
||||
if (prev) {
|
||||
prev.classList.add("focus");
|
||||
} else {
|
||||
searchbar.select();
|
||||
}
|
||||
} else { // SELECT_KEYCODE
|
||||
window.location.assign(focused.querySelector('a'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showSearch(yes) {
|
||||
if (yes) {
|
||||
search_wrap.classList.remove('hidden');
|
||||
searchicon.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
search_wrap.classList.add('hidden');
|
||||
searchicon.setAttribute('aria-expanded', 'false');
|
||||
var results = searchresults.children;
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
results[i].classList.remove("focus");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showResults(yes) {
|
||||
if (yes) {
|
||||
searchresults_outer.classList.remove('hidden');
|
||||
} else {
|
||||
searchresults_outer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Eventhandler for search icon
|
||||
function searchIconClickHandler() {
|
||||
if (search_wrap.classList.contains('hidden')) {
|
||||
showSearch(true);
|
||||
window.scrollTo(0, 0);
|
||||
searchbar.select();
|
||||
} else {
|
||||
showSearch(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 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, search_options);
|
||||
var resultcount = Math.min(results.length, results_options.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);
|
||||
}
|
||||
|
||||
fetch(path_to_root + 'searchindex.json')
|
||||
.then(response => response.json())
|
||||
.then(json => init(json))
|
||||
.catch(error => { // Try to load searchindex.js if fetch failed
|
||||
var script = document.createElement('script');
|
||||
script.src = path_to_root + 'searchindex.js';
|
||||
script.onload = () => init(window.search);
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
// Exported functions
|
||||
search.hasFocus = hasFocus;
|
||||
})(window.search);
|
||||
@@ -1,11 +0,0 @@
|
||||
@import "nib"
|
||||
|
||||
@import 'general'
|
||||
@import 'sidebar'
|
||||
@import 'page'
|
||||
@import 'menu'
|
||||
@import 'nav-icons'
|
||||
@import 'theme-popup'
|
||||
@import 'themes'
|
||||
@import 'print'
|
||||
@import 'tooltip'
|
||||
@@ -1,50 +0,0 @@
|
||||
html {
|
||||
font-family: "Open Sans", sans-serif
|
||||
color: #333
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.left {
|
||||
float: left
|
||||
}
|
||||
|
||||
.right {
|
||||
float: right
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.play-button.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h2, h3 { margin-top: 2.5em }
|
||||
h4, h5 { margin-top: 2em }
|
||||
|
||||
.header + .header h3, .header + .header h4, .header + .header h5 { margin-top: 1em }
|
||||
|
||||
table {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
|
||||
td {
|
||||
padding: 3px 20px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
thead {
|
||||
td { font-weight: 700; }
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
#menu-bar {
|
||||
position: -webkit-sticky
|
||||
position: sticky
|
||||
top: 0
|
||||
z-index: 101
|
||||
|
||||
& > #menu-bar-sticky-container {
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
transition: transform 0.5s, border-bottom-color 0.5s
|
||||
}
|
||||
|
||||
i, .icon-button {
|
||||
position: relative
|
||||
margin: 0 10px
|
||||
z-index: 10
|
||||
line-height: 50px
|
||||
|
||||
transition: color 0.5s
|
||||
|
||||
&:hover { cursor: pointer }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
|
||||
transform: translateY(-60px);
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
display: inline-block
|
||||
font-weight: 200
|
||||
font-size: 20px
|
||||
line-height: 50px
|
||||
text-align: center
|
||||
margin: 0
|
||||
flex: 1
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
.nav-chapters {
|
||||
font-size: 2.5em
|
||||
text-align: center
|
||||
text-decoration: none
|
||||
|
||||
position: fixed
|
||||
top: 50px /* Height of menu-bar */
|
||||
bottom: 0
|
||||
margin: 0
|
||||
max-width: 150px
|
||||
min-width: 90px
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-content: center
|
||||
flex-direction: column
|
||||
|
||||
transition: color 0.5s
|
||||
}
|
||||
|
||||
.nav-chapters:hover { text-decoration: none }
|
||||
|
||||
.nav-wrapper {
|
||||
margin-top: 50px
|
||||
display: none
|
||||
}
|
||||
|
||||
.mobile-nav-chapters {
|
||||
font-size: 2.5em
|
||||
text-align: center
|
||||
text-decoration: none
|
||||
width: 90px
|
||||
border-radius: 5px
|
||||
}
|
||||
|
||||
.previous {
|
||||
float: left
|
||||
}
|
||||
|
||||
.next {
|
||||
float: right
|
||||
right: $page-padding
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $page-plus-sidebar-width) {
|
||||
.nav-wide-wrapper { display: none }
|
||||
.nav-wrapper { display: block }
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $page-plus-sidebar-width + $sidebar-width) {
|
||||
.sidebar-visible {
|
||||
.nav-wide-wrapper { display: none }
|
||||
.nav-wrapper { display: block }
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
@require 'variables'
|
||||
|
||||
.page-wrapper {
|
||||
box-sizing: border-box
|
||||
|
||||
// Animation: slide away
|
||||
transition: padding-left 0.5s, margin-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
|
||||
}
|
||||
}
|
||||
|
||||
.page {
|
||||
outline: 0
|
||||
padding: 0 $page-padding
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-left: auto
|
||||
margin-right: auto
|
||||
max-width: $content-max-width
|
||||
padding-bottom: 50px
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
&:hover { text-decoration: underline; }
|
||||
}
|
||||
|
||||
img { max-width: 100%; }
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
@media only print {
|
||||
|
||||
#sidebar,
|
||||
#menu-bar,
|
||||
.nav-chapters,
|
||||
.mobile-nav-chapters {
|
||||
display: none
|
||||
}
|
||||
|
||||
#page-wrapper {
|
||||
left: 0;
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
#page-wrapper.page-wrapper {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
#content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page {
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #666666
|
||||
border-radius: 5px
|
||||
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact
|
||||
}
|
||||
|
||||
pre > .buttons {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
a, a:visited, a:active, a:hover {
|
||||
color: #4183c4
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-inside: avoid
|
||||
page-break-after: avoid
|
||||
/*break-after: avoid*/
|
||||
}
|
||||
|
||||
pre, code {
|
||||
page-break-inside: avoid
|
||||
white-space: pre-wrap /* CSS 3 */
|
||||
white-space: -moz-pre-wrap /* Mozilla, since 1999 */
|
||||
white-space: -pre-wrap /* Opera 4-6 */
|
||||
white-space: -o-pre-wrap /* Opera 7 */
|
||||
word-wrap: break-word /* Internet Explorer 5.5+ */
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
@require 'variables'
|
||||
|
||||
.sidebar {
|
||||
position: fixed
|
||||
left: 0
|
||||
top: 0
|
||||
bottom: 0
|
||||
width: $sidebar-width
|
||||
overflow-y: auto
|
||||
padding: 10px 10px
|
||||
font-size: 0.875em
|
||||
box-sizing: border-box
|
||||
-webkit-overflow-scrolling: touch
|
||||
|
||||
// Animation: slide away
|
||||
transition: transform 0.5s
|
||||
|
||||
code {
|
||||
line-height: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-hidden .sidebar {
|
||||
transform: translateX(- $sidebar-width)
|
||||
}
|
||||
|
||||
.chapter {
|
||||
list-style: none outside none
|
||||
padding-left: 0
|
||||
line-height: 2.2em
|
||||
|
||||
li a {
|
||||
display: block;
|
||||
padding: 5px 0
|
||||
text-decoration: none
|
||||
|
||||
&:hover { text-decoration: none }
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 100%
|
||||
height: 3px
|
||||
margin: 10px 0px
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
list-style: none outside none
|
||||
padding-left: 20px
|
||||
line-height: 1.9em
|
||||
|
||||
li {
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
white-space: nowrap
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
.theme-popup {
|
||||
position: absolute
|
||||
left: 10px
|
||||
|
||||
z-index: 1000;
|
||||
|
||||
border-radius: 4px
|
||||
font-size: 0.7em
|
||||
|
||||
.theme {
|
||||
display: inline
|
||||
border: 0
|
||||
margin: 0
|
||||
padding: 2px 10px
|
||||
line-height: 25px
|
||||
width: 100%
|
||||
white-space: nowrap
|
||||
text-align: left
|
||||
cursor: pointer
|
||||
color inherit
|
||||
background: inherit;
|
||||
font-size: inherit;
|
||||
|
||||
&:hover:first-child,
|
||||
&:hover:last-child {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
$theme-name = 'ayu'
|
||||
|
||||
$bg = #0f1419
|
||||
$fg = #c5c5c5
|
||||
|
||||
$sidebar-bg = #14191f
|
||||
$sidebar-fg = #c8c9db
|
||||
$sidebar-non-existant = #5c6773
|
||||
$sidebar-active = #ffb454
|
||||
$sidebar-spacer = #2d334f
|
||||
|
||||
$scrollbar = $sidebar-fg
|
||||
|
||||
$icons = #737480
|
||||
$icons-hover = #b7b9cc
|
||||
|
||||
$links = #0096cf
|
||||
|
||||
$inline-code-color = #ffb454
|
||||
|
||||
$theme-popup-bg = #14191f
|
||||
$theme-popup-border = #5c6773
|
||||
$theme-hover = #191f26
|
||||
|
||||
$quote-bg = #262933
|
||||
$quote-border = lighten($quote-bg, 5%)
|
||||
|
||||
$table-border-color = lighten($bg, 5%)
|
||||
$table-header-bg = lighten($bg, 20%)
|
||||
$table-alternate-bg = lighten($bg, 3%)
|
||||
|
||||
@import 'base'
|
||||
@@ -1,191 +0,0 @@
|
||||
.{unquote($theme-name)} {
|
||||
|
||||
color: $fg
|
||||
background-color: $bg
|
||||
|
||||
.content .header:link, .content .header:visited {
|
||||
color: $fg;
|
||||
pointer: cursor;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-bar {
|
||||
margin: auto (- $page-padding);
|
||||
|
||||
& > #menu-bar-sticky-container {
|
||||
background-color: $bg
|
||||
border-bottom-color: $bg
|
||||
border-bottom-width: 1px
|
||||
border-bottom-style: solid
|
||||
}
|
||||
|
||||
&.bordered > #menu-bar-sticky-container {
|
||||
border-bottom-color: $table-border-color
|
||||
}
|
||||
}
|
||||
|
||||
$table-border-color
|
||||
|
||||
.sidebar {
|
||||
background-color: $sidebar-bg
|
||||
color: $sidebar-fg
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background: $sidebar-bg;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $scrollbar;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter li {
|
||||
color: $sidebar-non-existant
|
||||
|
||||
a { color: $sidebar-fg }
|
||||
|
||||
.active,
|
||||
a:hover, {
|
||||
/* Animate color change */
|
||||
color: $sidebar-active
|
||||
}
|
||||
}
|
||||
|
||||
.chapter .spacer {
|
||||
background-color: $sidebar-spacer
|
||||
}
|
||||
|
||||
.menu-bar,
|
||||
.menu-bar:visited,
|
||||
.nav-chapters,
|
||||
.nav-chapters:visited,
|
||||
.mobile-nav-chapters,
|
||||
.mobile-nav-chapters:visited,
|
||||
.menu-bar .icon-button,
|
||||
.menu-bar a i {
|
||||
color: $icons
|
||||
}
|
||||
|
||||
.menu-bar i:hover,
|
||||
.menu-bar .icon-button:hover,
|
||||
.nav-chapters:hover,
|
||||
.mobile-nav-chapters i:hover {
|
||||
color: $icons-hover
|
||||
}
|
||||
|
||||
.mobile-nav-chapters i:hover {
|
||||
color: $sidebar-fg
|
||||
}
|
||||
|
||||
.mobile-nav-chapters {
|
||||
background-color: $sidebar-bg
|
||||
}
|
||||
|
||||
.content a:link, a:visited, a > .hljs {
|
||||
color: $links
|
||||
}
|
||||
|
||||
.theme-popup {
|
||||
color: $fg
|
||||
background: $theme-popup-bg
|
||||
border: 1px solid $theme-popup-border
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: none;
|
||||
|
||||
.theme:hover { background-color: $theme-hover }
|
||||
|
||||
.default { color: $icons }
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
color: $fg;
|
||||
background-color: $quote-bg;
|
||||
border-top: .1em solid $quote-border;
|
||||
border-bottom: .1em solid $quote-border;
|
||||
}
|
||||
|
||||
|
||||
table {
|
||||
|
||||
td {
|
||||
border-color: $table-border-color;
|
||||
}
|
||||
|
||||
// Alternate background colors for rows
|
||||
tbody tr:nth-child(2n) {
|
||||
background: $table-alternate-bg;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: $table-header-bg;
|
||||
td { border: none; }
|
||||
tr { border: 1px $table-header-bg solid; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
color: $inline-code-color;
|
||||
}
|
||||
|
||||
a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
|
||||
& > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
|
||||
color: $sidebar-fg;
|
||||
cursor: pointer;
|
||||
|
||||
:hover { color: $sidebar-active; }
|
||||
i { margin-left: 8px; }
|
||||
button {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
& > .result { margin-top: 10px; }
|
||||
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
|
||||
i {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
background: $bg;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: $scrollbar;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
$theme-name = 'coal'
|
||||
|
||||
$bg = #141617
|
||||
$fg = #98a3ad
|
||||
|
||||
$sidebar-bg = #292c2f
|
||||
$sidebar-fg = #a1adb8
|
||||
$sidebar-non-existant = #505254
|
||||
$sidebar-active = #3473ad
|
||||
$sidebar-spacer = #393939
|
||||
|
||||
$scrollbar = $sidebar-fg
|
||||
|
||||
$icons = #43484d
|
||||
$icons-hover = #b3c0cc
|
||||
|
||||
$links = #2b79a2
|
||||
|
||||
$inline-code-color = #c5c8c6;
|
||||
|
||||
$theme-popup-bg = #141617
|
||||
$theme-popup-border = #43484d
|
||||
$theme-hover = #1f2124
|
||||
|
||||
$quote-bg = #242637
|
||||
$quote-border = lighten($quote-bg, 5%)
|
||||
|
||||
$table-border-color = lighten($bg, 5%)
|
||||
$table-header-bg = lighten($bg, 20%)
|
||||
$table-alternate-bg = lighten($bg, 3%)
|
||||
|
||||
@import 'base'
|
||||
@@ -1,5 +0,0 @@
|
||||
@import 'light'
|
||||
@import 'coal'
|
||||
@import 'navy'
|
||||
@import 'rust'
|
||||
@import 'ayu'
|
||||
@@ -1,32 +0,0 @@
|
||||
$theme-name = 'light'
|
||||
|
||||
$bg = #ffffff
|
||||
$fg = #333333
|
||||
|
||||
$sidebar-bg = #fafafa
|
||||
$sidebar-fg = #364149
|
||||
$sidebar-non-existant = #aaaaaa
|
||||
$sidebar-active = #008cff
|
||||
$sidebar-spacer = #f4f4f4
|
||||
|
||||
$scrollbar = #cccccc
|
||||
|
||||
$icons = #cccccc
|
||||
$icons-hover = #333333
|
||||
|
||||
$links = #4183c4
|
||||
|
||||
$inline-code-color = #6e6b5e;
|
||||
|
||||
$theme-popup-bg = #fafafa
|
||||
$theme-popup-border = #cccccc
|
||||
$theme-hover = #e6e6e6
|
||||
|
||||
$quote-bg = #f2f7f9
|
||||
$quote-border = darken($quote-bg, 5%)
|
||||
|
||||
$table-border-color = darken($bg, 5%)
|
||||
$table-header-bg = darken($bg, 20%)
|
||||
$table-alternate-bg = darken($bg, 3%)
|
||||
|
||||
@import 'base'
|
||||
@@ -1,32 +0,0 @@
|
||||
$theme-name = 'navy'
|
||||
|
||||
$bg = #161923
|
||||
$fg = #bcbdd0
|
||||
|
||||
$sidebar-bg = #282d3f
|
||||
$sidebar-fg = #c8c9db
|
||||
$sidebar-non-existant = #505274
|
||||
$sidebar-active = #2b79a2
|
||||
$sidebar-spacer = #2d334f
|
||||
|
||||
$scrollbar = $sidebar-fg
|
||||
|
||||
$icons = #737480
|
||||
$icons-hover = #b7b9cc
|
||||
|
||||
$links = #2b79a2
|
||||
|
||||
$inline-code-color = #c5c8c6;
|
||||
|
||||
$theme-popup-bg = #161923
|
||||
$theme-popup-border = #737480
|
||||
$theme-hover = #282e40
|
||||
|
||||
$quote-bg = #262933
|
||||
$quote-border = lighten($quote-bg, 5%)
|
||||
|
||||
$table-border-color = lighten($bg, 5%)
|
||||
$table-header-bg = lighten($bg, 20%)
|
||||
$table-alternate-bg = lighten($bg, 3%)
|
||||
|
||||
@import 'base'
|
||||
@@ -1,32 +0,0 @@
|
||||
$theme-name = 'rust'
|
||||
|
||||
$bg = #e1e1db
|
||||
$fg = #262625
|
||||
|
||||
$sidebar-bg = #3b2e2a
|
||||
$sidebar-fg = #c8c9db
|
||||
$sidebar-non-existant = #505254
|
||||
$sidebar-active = #e69f67
|
||||
$sidebar-spacer = #45373a
|
||||
|
||||
$scrollbar = $sidebar-fg
|
||||
|
||||
$icons = #737480
|
||||
$icons-hover = #262625
|
||||
|
||||
$links = #2b79a2
|
||||
|
||||
$inline-code-color = #6e6b5e;
|
||||
|
||||
$theme-popup-bg = #e1e1db
|
||||
$theme-popup-border = #b38f6b
|
||||
$theme-hover = #99908a
|
||||
|
||||
$quote-bg = #c1c1bb
|
||||
$quote-border = darken($quote-bg, 5%)
|
||||
|
||||
$table-border-color = darken($bg, 5%)
|
||||
$table-header-bg = #b3a497
|
||||
$table-alternate-bg = darken($bg, 3%)
|
||||
|
||||
@import 'base'
|
||||
@@ -1,18 +0,0 @@
|
||||
.tooltiptext {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
|
||||
left: -8px; /* Half of the width of the icon */
|
||||
top: -35px;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px 8px;
|
||||
margin: 5px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.tooltipped .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
$sidebar-width = 300px
|
||||
$page-padding = 15px
|
||||
$content-max-width = 750px
|
||||
$page-plus-sidebar-width = $content-max-width + $sidebar-width + $page-padding * 2
|
||||
@@ -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,21 @@ 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 +53,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 +75,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 +89,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 +105,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 +187,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"),
|
||||
};
|
||||
|
||||
206
src/utils/mod.rs
206
src/utils/mod.rs
@@ -3,12 +3,102 @@
|
||||
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 pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
|
||||
OPTION_ENABLE_TABLES};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use self::string::{RangeArgument, take_lines};
|
||||
pub use self::string::{take_lines, RangeArgument};
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
fn adjust_links(event: Event) -> Event {
|
||||
lazy_static! {
|
||||
static ref HTTP_LINK: Regex = Regex::new("^https?://").unwrap();
|
||||
static ref MD_LINK: Regex = Regex::new("(?P<link>.*).md(?P<anchor>#.*)?").unwrap();
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Start(Tag::Link(dest, title)) => {
|
||||
if !HTTP_LINK.is_match(&dest) {
|
||||
if let Some(caps) = MD_LINK.captures(&dest) {
|
||||
let mut html_link = [&caps["link"], ".html"].concat();
|
||||
|
||||
if let Some(anchor) = caps.name("anchor") {
|
||||
html_link.push_str(anchor.as_str());
|
||||
}
|
||||
|
||||
return Event::Start(Tag::Link(Cow::from(html_link), title));
|
||||
}
|
||||
}
|
||||
|
||||
Event::Start(Tag::Link(dest, title))
|
||||
}
|
||||
_ => event,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
|
||||
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
||||
@@ -21,7 +111,8 @@ pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
||||
let p = Parser::new_ext(text, opts);
|
||||
let mut converter = EventQuoteConverter::new(curly_quotes);
|
||||
let events = p.map(clean_codeblock_headers)
|
||||
.map(|event| converter.convert(event));
|
||||
.map(adjust_links)
|
||||
.map(|event| converter.convert(event));
|
||||
|
||||
html::push_html(&mut s, events);
|
||||
s
|
||||
@@ -73,36 +164,36 @@ fn clean_codeblock_headers(event: Event) -> Event {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn convert_quotes_to_curly(original_text: &str) -> String {
|
||||
// We'll consider the start to be "whitespace".
|
||||
let mut preceded_by_whitespace = true;
|
||||
|
||||
original_text.chars()
|
||||
.map(|original_char| {
|
||||
let converted_char = match original_char {
|
||||
'\'' => {
|
||||
if preceded_by_whitespace {
|
||||
'‘'
|
||||
} else {
|
||||
'’'
|
||||
original_text
|
||||
.chars()
|
||||
.map(|original_char| {
|
||||
let converted_char = match original_char {
|
||||
'\'' => {
|
||||
if preceded_by_whitespace {
|
||||
'‘'
|
||||
} else {
|
||||
'’'
|
||||
}
|
||||
}
|
||||
}
|
||||
'"' => {
|
||||
if preceded_by_whitespace {
|
||||
'“'
|
||||
} else {
|
||||
'”'
|
||||
'"' => {
|
||||
if preceded_by_whitespace {
|
||||
'“'
|
||||
} else {
|
||||
'”'
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => original_char,
|
||||
};
|
||||
_ => original_char,
|
||||
};
|
||||
|
||||
preceded_by_whitespace = original_char.is_whitespace();
|
||||
preceded_by_whitespace = original_char.is_whitespace();
|
||||
|
||||
converted_char
|
||||
})
|
||||
.collect()
|
||||
converted_char
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Prints a "backtrace" of some `Error`.
|
||||
@@ -119,6 +210,26 @@ mod tests {
|
||||
mod render_markdown {
|
||||
use super::super::render_markdown;
|
||||
|
||||
#[test]
|
||||
fn preserves_external_links() {
|
||||
assert_eq!(
|
||||
render_markdown("[example](https://www.rust-lang.org/)", false),
|
||||
"<p><a href=\"https://www.rust-lang.org/\">example</a></p>\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_adjust_markdown_links() {
|
||||
assert_eq!(
|
||||
render_markdown("[example](example.md)", false),
|
||||
"<p><a href=\"example.html\">example</a></p>\n"
|
||||
);
|
||||
assert_eq!(
|
||||
render_markdown("[example_anchor](example.md#anchor)", false),
|
||||
"<p><a href=\"example.html#anchor\">example_anchor</a></p>\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_keep_quotes_straight() {
|
||||
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
|
||||
@@ -212,19 +323,54 @@ 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;
|
||||
|
||||
#[test]
|
||||
fn it_converts_single_quotes() {
|
||||
assert_eq!(convert_quotes_to_curly("'one', 'two'"),
|
||||
"‘one’, ‘two’");
|
||||
assert_eq!(
|
||||
convert_quotes_to_curly("'one', 'two'"),
|
||||
"‘one’, ‘two’"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_converts_double_quotes() {
|
||||
assert_eq!(convert_quotes_to_curly(r#""one", "two""#),
|
||||
"“one”, “two”");
|
||||
assert_eq!(
|
||||
convert_quotes_to_curly(r#""one", "two""#),
|
||||
"“one”, “two”"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
|
||||
use itertools::Itertools;
|
||||
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
|
||||
|
||||
// This trait is already contained in the standard lib, however it is unstable.
|
||||
// TODO: Remove when the `collections_range` feature stabilises
|
||||
@@ -50,7 +50,7 @@ pub fn take_lines<R: RangeArgument<usize>>(s: &str, range: R) -> String {
|
||||
let start = *range.start().unwrap_or(&0);
|
||||
let mut lines = s.lines().skip(start);
|
||||
match range.end() {
|
||||
Some(&end) => lines.take(end.checked_sub(start).unwrap_or(0)).join("\n"),
|
||||
Some(&end) => lines.take(end.saturating_sub(start)).join("\n"),
|
||||
None => lines.join("\n"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
use std::path::Path;
|
||||
use tempdir::TempDir;
|
||||
use mdbook::config::Config;
|
||||
use mdbook::MDBook;
|
||||
use mdbook::renderer::RenderContext;
|
||||
#[cfg(not(windows))]
|
||||
use std::path::Path;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
#[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 mdbook::renderer::RenderContext;
|
||||
use std::fs::File;
|
||||
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user