mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 16:12:33 -05:00
Compare commits
138 Commits
fix-travis
...
v0.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e2e095c26 | ||
|
|
5baaf55abc | ||
|
|
9157f6e32d | ||
|
|
3688f73052 | ||
|
|
3e89e8b1bd | ||
|
|
e08fc148b1 | ||
|
|
d9c1c77aae | ||
|
|
bb2ca4f938 | ||
|
|
42aded9577 | ||
|
|
7fb2d5437a | ||
|
|
4cc3a1333b | ||
|
|
322e8fcf77 | ||
|
|
a8a460545f | ||
|
|
1d69ccae48 | ||
|
|
762d89ebbf | ||
|
|
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 |
27
.travis.yml
27
.travis.yml
@@ -1,7 +1,17 @@
|
||||
language: rust
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
cache:
|
||||
- cargo
|
||||
timeout: 360
|
||||
cargo: true
|
||||
|
||||
before_cache:
|
||||
- chmod -R a+r $HOME/.cargo
|
||||
@@ -9,19 +19,10 @@ before_cache:
|
||||
env:
|
||||
global:
|
||||
- CRATE_NAME=mdbook
|
||||
- TARGET=x86_64-unknown-linux-gnu
|
||||
|
||||
install:
|
||||
- sh ci/install.sh
|
||||
- export PATH=$PATH:$HOME/.cargo/bin
|
||||
|
||||
script:
|
||||
- cargo build --all --no-default-features
|
||||
- cargo build --verbose
|
||||
- cargo test --verbose
|
||||
|
||||
after_success:
|
||||
- bash ci/github_pages.sh
|
||||
- cargo test --all
|
||||
- cargo test --all --no-default-features
|
||||
|
||||
before_deploy:
|
||||
- sh ci/before_deploy.sh
|
||||
@@ -39,8 +40,8 @@ deploy:
|
||||
|
||||
branches:
|
||||
only:
|
||||
- "/^v\\d+\\.\\d+\\.\\d+.*$/"
|
||||
- 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.
|
||||
|
||||
803
Cargo.lock
generated
803
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
40
Cargo.toml
40
Cargo.toml
@@ -1,49 +1,42 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.1.6"
|
||||
authors = ["Mathieu David <mathieudavid@mathieudavid.org>", "Michael-F-Bryan <michaelfbryan@gmail.com>"]
|
||||
version = "0.2.1"
|
||||
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 = "origin"
|
||||
tag-prefix = "v"
|
||||
exclude = ["book-example/*"]
|
||||
|
||||
[dependencies]
|
||||
clap = "2.24"
|
||||
chrono = "0.4"
|
||||
handlebars = "0.32"
|
||||
handlebars = "1.0"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
error-chain = "0.11"
|
||||
error-chain = "0.12"
|
||||
serde_json = "1.0"
|
||||
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"
|
||||
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.6", optional = true }
|
||||
@@ -51,12 +44,9 @@ staticfile = { version = "0.5", optional = true }
|
||||
ws = { version = "0.7", optional = true}
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "2.0", optional = true }
|
||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
||||
ammonia = { version = "1.1", optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
error-chain = "0.11"
|
||||
|
||||
[dev-dependencies]
|
||||
select = "0.4"
|
||||
pretty_assertions = "0.5"
|
||||
@@ -67,12 +57,10 @@ pulldown-cmark-to-cmark = "1.1.0"
|
||||
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"
|
||||
|
||||
@@ -41,7 +41,7 @@ There are multiple ways to install mdBook.
|
||||
|
||||
2. **From Crates.io**
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
|
||||
13
appveyor.yml
13
appveyor.yml
@@ -1,7 +1,6 @@
|
||||
environment:
|
||||
global:
|
||||
PROJECT_NAME: mdBook
|
||||
nodejs_version: "6"
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
@@ -32,22 +31,17 @@ 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
|
||||
- cargo test --all
|
||||
- cargo test --all --no-default-features
|
||||
|
||||
before_deploy:
|
||||
# Generate artifacts for release
|
||||
- cargo build --release
|
||||
- cargo rustc --bin mdbook --release -- -C lto
|
||||
- mkdir staging
|
||||
- copy target\release\mdbook.exe staging
|
||||
- cd staging
|
||||
@@ -67,3 +61,4 @@ deploy:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
# mdBook
|
||||
|
||||
**mdBook** is a command line tool and Rust crate to create books using Markdown files. It's very similar to Gitbook but written in [Rust](http://www.rust-lang.org).
|
||||
**mdBook** is a command line tool and Rust crate to create books using Markdown
|
||||
files. It's very similar to Gitbook but written in
|
||||
[Rust](http://www.rust-lang.org).
|
||||
|
||||
What you are reading serves as an example of the output of mdBook and at the same time as a high-level documentation.
|
||||
What you are reading serves as an example of the output of mdBook and at the
|
||||
same time as a high-level documentation.
|
||||
|
||||
mdBook is free and open source, you can find the source code on [Github](https://github.com/rust-lang-nursery/mdBook). Issues and feature requests can be posted on the [Github Issue tracker](https://github.com/rust-lang-nursery/mdBook/issues).
|
||||
mdBook is free and open source, you can find the source code on
|
||||
[GitHub](https://github.com/rust-lang-nursery/mdBook). Issues and feature
|
||||
requests can be posted on the [GitHub issue
|
||||
tracker](https://github.com/rust-lang-nursery/mdBook/issues).
|
||||
|
||||
## API docs
|
||||
|
||||
Alongside this book you can also read the [API docs](https://docs.rs/mdbook/*/mdbook/) generated by Rustdoc if you would like to use mdBook as a crate or write a new renderer and need a more low-level overview.
|
||||
Alongside this book you can also read the [API
|
||||
docs](https://docs.rs/mdbook/*/mdbook/) generated by Rustdoc if you would like
|
||||
to use mdBook as a crate or write a new renderer and need a more low-level
|
||||
overview.
|
||||
|
||||
## License
|
||||
|
||||
mdBook, all the source code, is released under the [Mozilla Public License v2.0](https://www.mozilla.org/MPL/2.0/)
|
||||
mdBook, all the source code, is released under the [Mozilla Public License
|
||||
v2.0](https://www.mozilla.org/MPL/2.0/).
|
||||
|
||||
@@ -1,24 +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)
|
||||
- [clean](cli/clean.md)
|
||||
- [Format](format/format.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)
|
||||
|
||||
55
book-example/src/cli/README.md
Normal file
55
book-example/src/cli/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Command Line Tool
|
||||
|
||||
mdBook can be used either as a command line tool or a [Rust
|
||||
crate](https://crates.io/crates/mdbook). Let's focus on the command line tool
|
||||
capabilities first.
|
||||
|
||||
## Install From Binaries
|
||||
|
||||
Precompiled binaries are provided for major platforms on a best-effort basis.
|
||||
Visit [the releases page](https://github.com/rust-lang-nursery/mdBook/releases)
|
||||
to download the appropriate version for your platform.
|
||||
|
||||
## Install From Source
|
||||
|
||||
mdBook can also be installed from source
|
||||
|
||||
### Pre-requisite
|
||||
|
||||
mdBook is written in **[Rust](https://www.rust-lang.org/)** and therefore needs
|
||||
to be compiled with **Cargo**. If you haven't already installed Rust, please go
|
||||
ahead and [install it](https://www.rust-lang.org/downloads.html) now.
|
||||
|
||||
### Install Crates.io version
|
||||
|
||||
Installing mdBook is relatively easy if you already have Rust and Cargo
|
||||
installed. You just have to type this snippet in your terminal:
|
||||
|
||||
```bash
|
||||
cargo install mdbook
|
||||
```
|
||||
|
||||
This will fetch the source code for the latest release from
|
||||
[Crates.io](https://crates.io/) and compile it. You will have to add Cargo's
|
||||
`bin` directory to your `PATH`.
|
||||
|
||||
Run `mdbook help` in your terminal to verify if it works. Congratulations, you
|
||||
have installed mdBook!
|
||||
|
||||
|
||||
### Install Git version
|
||||
|
||||
The **[git version](https://github.com/rust-lang-nursery/mdBook)** contains all
|
||||
the latest bug-fixes and features, that will be released in the next version on
|
||||
**Crates.io**, if you can't wait until the next release. You can build the git
|
||||
version yourself. Open your terminal and navigate to the directory of you
|
||||
choice. We need to clone the git repository and then build it with Cargo.
|
||||
|
||||
```bash
|
||||
git clone --depth=1 https://github.com/rust-lang-nursery/mdBook.git
|
||||
cd mdBook
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The executable `mdbook` will be in the `./target/release` folder, this should be
|
||||
added to the path.
|
||||
@@ -6,16 +6,16 @@ The build command is used to render your book:
|
||||
mdbook build
|
||||
```
|
||||
|
||||
It will try to parse your `SUMMARY.md` file to understand the structure of your book
|
||||
and fetch the corresponding files.
|
||||
It will try to parse your `SUMMARY.md` file to understand the structure of your
|
||||
book and fetch the corresponding files.
|
||||
|
||||
The rendered output will maintain the same directory structure as the source for
|
||||
convenience. Large books will therefore remain structured when rendered.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
Like `init`, the `build` command can take a directory as an 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*
|
||||
|
||||
@@ -7,12 +7,21 @@ artifacts.
|
||||
mdbook clean
|
||||
```
|
||||
|
||||
It will try to delete the built book. If a path is provided, it will be used.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
Like `init`, the `clean` command can take a directory as an argument to use
|
||||
instead of the normal build directory.
|
||||
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
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# Command Line Tool
|
||||
|
||||
mdBook can be used either as a command line tool or a [Rust crate](https://crates.io/crates/mdbook).
|
||||
Let's focus on the command line tool capabilities first.
|
||||
|
||||
## Install
|
||||
|
||||
### Pre-requisite
|
||||
|
||||
mdBook is written in **[Rust](https://www.rust-lang.org/)** and therefore needs to be compiled with **Cargo**, because we don't yet offer ready-to-go binaries. If you haven't already installed Rust, please go ahead and [install it](https://www.rust-lang.org/downloads.html) now.
|
||||
|
||||
### Install Crates.io version
|
||||
|
||||
Installing mdBook is relatively easy if you already have Rust and Cargo installed. You just have to type this snippet in your terminal:
|
||||
|
||||
```bash
|
||||
cargo install mdbook
|
||||
```
|
||||
|
||||
This will fetch the source code from [Crates.io](https://crates.io/) and compile it. You will have to add Cargo's `bin` directory to your `PATH`.
|
||||
|
||||
Run `mdbook help` in your terminal to verify if it works. Congratulations, you have installed mdBook!
|
||||
|
||||
|
||||
### Install Git version
|
||||
|
||||
The **[git version](https://github.com/rust-lang-nursery/mdBook)** contains all the latest bug-fixes and features, that will be released in the next version on **Crates.io**, if you can't wait until the next release. You can build the git version yourself. Open your terminal and navigate to the directory of you choice. We need to clone the git repository and then build it with Cargo.
|
||||
|
||||
```bash
|
||||
git clone --depth=1 https://github.com/rust-lang-nursery/mdBook.git
|
||||
cd mdBook
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The executable `mdbook` will be in the `./target/release` folder, this should be added to the path.
|
||||
@@ -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:
|
||||
|
||||
@@ -7,7 +9,8 @@ The `init` command is used like this:
|
||||
mdbook init
|
||||
```
|
||||
|
||||
When using the `init` command for the first time, a couple of files will be set up for you:
|
||||
When using the `init` command for the first time, a couple of files will be set
|
||||
up for you:
|
||||
```bash
|
||||
book-test/
|
||||
├── book
|
||||
@@ -16,30 +19,36 @@ book-test/
|
||||
└── SUMMARY.md
|
||||
```
|
||||
|
||||
- The `src` directory is were you write your book in markdown. It contains all the source files,
|
||||
configuration files, etc.
|
||||
- The `src` directory is were you write your book in markdown. It contains all
|
||||
the source files, 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 `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
|
||||
called `theme` in your source directory so that you can modify it.
|
||||
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
|
||||
specific file, just delete it and the default file will be used.
|
||||
The theme is selectively overwritten, this means that if you don't want to
|
||||
overwrite a specific file, just delete it and the default file will be used.
|
||||
|
||||
@@ -1,40 +1,49 @@
|
||||
# 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 an 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
|
||||
your default web browser after starting the server.
|
||||
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,52 @@
|
||||
# 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 an 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,6 @@ 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.
|
||||
|
||||
56
book-example/src/continuous-integration.md
Normal file
56
book-example/src/continuous-integration.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 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!
|
||||
@@ -1,18 +1,18 @@
|
||||
# For Developers
|
||||
|
||||
While `mdbook` is mainly used as a command line tool, you can also import the
|
||||
While `mdbook` is mainly used as a command line tool, you can also import the
|
||||
underlying library directly and use that to manage a book. It also has a fairly
|
||||
flexible plugin mechanism, allowing you to create your own custom tooling and
|
||||
flexible plugin mechanism, allowing you to create your own custom tooling and
|
||||
consumers (often referred to as *backends*) if you need to do some analysis of
|
||||
the book or render it in a different format.
|
||||
|
||||
The *For Developers* chapters are here to show you the more advanced usage of
|
||||
The *For Developers* chapters are here to show you the more advanced usage of
|
||||
`mdbook`.
|
||||
|
||||
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
|
||||
@@ -32,11 +32,11 @@ The process of rendering a book project goes through several steps.
|
||||
|
||||
The `mdbook` binary is just a wrapper around the `mdbook` crate, exposing its
|
||||
functionality as a command-line program. As such it is quite easy to create your
|
||||
own programs which use `mdbook` internally, adding your own functionality (e.g.
|
||||
own programs which use `mdbook` internally, adding your own functionality (e.g.
|
||||
a custom preprocessor) or tweaking the build process.
|
||||
|
||||
The easiest way to find out how to use the `mdbook` crate is by looking at the
|
||||
[API Docs]. The top level documentation explains how one would use the
|
||||
[API Docs]. The top level documentation explains how one would use the
|
||||
[`MDBook`] type to load and build a book, while the [config] module gives a good
|
||||
explanation on the configuration system.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Alternate Backends
|
||||
|
||||
A "backend" is simply a program which `mdbook` will invoke during the book
|
||||
A "backend" is simply a program which `mdbook` will invoke during the book
|
||||
rendering process. This program is passed a JSON representation of the book and
|
||||
configuration information via `stdin`. Once the backend receives this
|
||||
configuration information via `stdin`. Once the backend receives this
|
||||
information it is free to do whatever it wants.
|
||||
|
||||
There are already several alternate backends on GitHub which can be used as a
|
||||
There are already several alternate backends on GitHub which can be used as a
|
||||
rough example of how this is accomplished in practice.
|
||||
|
||||
- [mdbook-linkcheck] - a simple program for verifying the book doesn't contain
|
||||
@@ -21,7 +21,7 @@ no reason why it couldn't be accomplished using something like Python or Ruby.
|
||||
|
||||
## Setting Up
|
||||
|
||||
First you'll want to create a new binary program and add `mdbook` as a
|
||||
First you'll want to create a new binary program and add `mdbook` as a
|
||||
dependency.
|
||||
|
||||
```
|
||||
@@ -30,8 +30,8 @@ $ cd mdbook-wordcount
|
||||
$ cargo add mdbook
|
||||
```
|
||||
|
||||
When our `mdbook-wordcount` plugin is invoked, `mdbook` will send it a JSON
|
||||
version of [`RenderContext`] via our plugin's `stdin`. For convenience, there's
|
||||
When our `mdbook-wordcount` plugin is invoked, `mdbook` will send it a JSON
|
||||
version of [`RenderContext`] via our plugin's `stdin`. For convenience, there's
|
||||
a [`RenderContext::from_json()`] constructor which will load a `RenderContext`.
|
||||
|
||||
This is all the boilerplate necessary for our backend to load the book.
|
||||
@@ -49,9 +49,9 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** The `RenderContext` contains a `version` field. This lets backends
|
||||
> **Note:** The `RenderContext` contains a `version` field. This lets backends
|
||||
figure out whether they are compatible with the version of `mdbook` it's being
|
||||
called by. This `version` comes directly from the corresponding field in
|
||||
called by. This `version` comes directly from the corresponding field in
|
||||
`mdbook`'s `Cargo.toml`.
|
||||
|
||||
It is recommended that backends use the [`semver`] crate to inspect this field
|
||||
@@ -60,7 +60,7 @@ fn main() {
|
||||
|
||||
## Inspecting the Book
|
||||
|
||||
Now our backend has a copy of the book, lets count how many words are in each
|
||||
Now our backend has a copy of the book, lets count how many words are in each
|
||||
chapter!
|
||||
|
||||
Because the `RenderContext` contains a [`Book`] field (`book`), and a `Book` has
|
||||
@@ -89,8 +89,8 @@ fn count_words(ch: &Chapter) -> usize {
|
||||
|
||||
## Enabling the Backend
|
||||
|
||||
Now we've got the basics running, we want to actually use it. First, install
|
||||
the program.
|
||||
Now we've got the basics running, we want to actually use it. First, install the
|
||||
program.
|
||||
|
||||
```
|
||||
$ cargo install
|
||||
@@ -110,15 +110,14 @@ Then `cd` to the particular book you'd like to count the words of and update its
|
||||
+ [output.wordcount]
|
||||
```
|
||||
|
||||
When it loads a book into memory, `mdbook` will inspect your `book.toml` file
|
||||
to try and figure out which backends to use by looking for all `output.*`
|
||||
tables. If none are provided it'll fall back to using the default HTML
|
||||
renderer.
|
||||
When it loads a book into memory, `mdbook` will inspect your `book.toml` file to
|
||||
try and figure out which backends to use by looking for all `output.*` tables.
|
||||
If none are provided it'll fall back to using the default HTML renderer.
|
||||
|
||||
Notably, this means if you want to add your own custom backend you'll also
|
||||
need to make sure to add the HTML backend, even if its table just stays empty.
|
||||
Notably, this means if you want to add your own custom backend you'll also need
|
||||
to make sure to add the HTML backend, even if its table just stays empty.
|
||||
|
||||
Now you just need to build your book like normal, and everything should *Just
|
||||
Now you just need to build your book like normal, and everything should *Just
|
||||
Work*.
|
||||
|
||||
```
|
||||
@@ -145,11 +144,11 @@ Alternate Backends: 710
|
||||
Contributors: 85
|
||||
```
|
||||
|
||||
The reason we didn't need to specify the full name/path of our `wordcount`
|
||||
backend is because `mdbook` will try to *infer* the program's name via
|
||||
convention. The executable for the `foo` backend is typically called
|
||||
The reason we didn't need to specify the full name/path of our `wordcount`
|
||||
backend is because `mdbook` will try to *infer* the program's name via
|
||||
convention. The executable for the `foo` backend is typically called
|
||||
`mdbook-foo`, with an associated `[output.foo]` entry in the `book.toml`. To
|
||||
explicitly tell `mdbook` what command to invoke (it may require command-line
|
||||
explicitly tell `mdbook` what command to invoke (it may require command-line
|
||||
arguments or be an interpreted script), you can use the `command` field.
|
||||
|
||||
```diff
|
||||
@@ -168,16 +167,16 @@ arguments or be an interpreted script), you can use the `command` field.
|
||||
## Configuration
|
||||
|
||||
Now imagine you don't want to count the number of words on a particular chapter
|
||||
(it might be generated text/code, etc). The canonical way to do this is via
|
||||
the usual `book.toml` configuration file by adding items to your `[output.foo]`
|
||||
(it might be generated text/code, etc). The canonical way to do this is via the
|
||||
usual `book.toml` configuration file by adding items to your `[output.foo]`
|
||||
table.
|
||||
|
||||
The `Config` can be treated roughly as a nested hashmap which lets you call
|
||||
methods like `get()` to access the config's contents, with a
|
||||
`get_deserialized()` convenience method for retrieving a value and
|
||||
automatically deserializing to some arbitrary type `T`.
|
||||
`get_deserialized()` convenience method for retrieving a value and automatically
|
||||
deserializing to some arbitrary type `T`.
|
||||
|
||||
To implement this, we'll create our own serializable `WordcountConfig` struct
|
||||
To implement this, we'll create our own serializable `WordcountConfig` struct
|
||||
which will encapsulate all configuration for this backend.
|
||||
|
||||
First add `serde` and `serde_derive` to your `Cargo.toml`,
|
||||
@@ -229,7 +228,7 @@ and then add a check to make sure we skip ignored chapters.
|
||||
|
||||
## Output and Signalling Failure
|
||||
|
||||
While it's nice to print word counts to the terminal when a book is built, it
|
||||
While it's nice to print word counts to the terminal when a book is built, it
|
||||
might also be a good idea to output them to a file somewhere. `mdbook` tells a
|
||||
backend where it should place any generated output via the `destination` field
|
||||
in [`RenderContext`].
|
||||
@@ -264,7 +263,7 @@ in [`RenderContext`].
|
||||
> so it's always a good idea to create it with `fs::create_dir_all()`.
|
||||
|
||||
There's always the possibility that an error will occur while processing a book
|
||||
(just look at all the `unwrap()`'s we've written already), so `mdbook` will
|
||||
(just look at all the `unwrap()`'s we've written already), so `mdbook` will
|
||||
interpret a non-zero exit code as a rendering failure.
|
||||
|
||||
For example, if we wanted to make sure all chapters have an *even* number of
|
||||
@@ -315,27 +314,27 @@ init: 283
|
||||
init has an odd number of words!
|
||||
2018-01-16 21:21:39 [ERROR] (mdbook::renderer): Renderer exited with non-zero return code.
|
||||
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Error: Rendering failed
|
||||
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Caused By: The "mdbook-wordcount" renderer failed
|
||||
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Caused By: The "mdbook-wordcount" renderer failed
|
||||
```
|
||||
|
||||
As you've probably already noticed, output from the plugin's subprocess is
|
||||
immediately passed through to the user. It is encouraged for plugins to
|
||||
follow the "rule of silence" and only generate output when necessary (e.g. an
|
||||
error in generation or a warning).
|
||||
immediately passed through to the user. It is encouraged for plugins to follow
|
||||
the "rule of silence" and only generate output when necessary (e.g. an error in
|
||||
generation or a warning).
|
||||
|
||||
All environment variables are passed through to the backend, allowing you to
|
||||
use the usual `RUST_LOG` to control logging verbosity.
|
||||
All environment variables are passed through to the backend, allowing you to use
|
||||
the usual `RUST_LOG` to control logging verbosity.
|
||||
|
||||
|
||||
## Wrapping Up
|
||||
|
||||
Although contrived, hopefully this example was enough to show how you'd create
|
||||
an alternate backend for `mdbook`. If you feel it's missing something, don't
|
||||
an alternate backend for `mdbook`. If you feel it's missing something, don't
|
||||
hesitate to create an issue in the [issue tracker] so we can improve the user
|
||||
guide.
|
||||
|
||||
The existing backends mentioned towards the start of this chapter should serve
|
||||
as a good example of how it's done in real life, so feel free to skim through
|
||||
as a good example of how it's done in real life, so feel free to skim through
|
||||
the source code or ask questions.
|
||||
|
||||
|
||||
@@ -348,5 +347,5 @@ the source code or ask questions.
|
||||
[`semver`]: https://crates.io/crates/semver
|
||||
[`Book`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html
|
||||
[`Book::iter()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html#method.iter
|
||||
[`Config`]: http://rust-lang-nursery.github.io/mdBook/mdbook/config/struct.Config.html
|
||||
[`Config`]: http://rust-lang-nursery.github.io/mdBook/mdbook/config/struct.Config.html
|
||||
[issue tracker]: https://github.com/rust-lang-nursery/mdBook/issues
|
||||
|
||||
@@ -33,10 +33,11 @@ pub struct PreprocessorContext {
|
||||
|
||||
## A complete Example
|
||||
|
||||
The magic happens within the `run(...)` method of the [`Preprocessor`][preprocessor-docs] trait implementation.
|
||||
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(...)`:
|
||||
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| {
|
||||
@@ -56,15 +57,15 @@ book.for_each_mut(|item: &mut BookItem| {
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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> {
|
||||
|
||||
@@ -23,8 +23,9 @@ limit-results = 15
|
||||
|
||||
## Supported configuration options
|
||||
|
||||
It is important to note that **any** relative path specified in the in the configuration will
|
||||
always be taken relative from the root of the book where the configuration file is located.
|
||||
It is important to note that **any** relative path specified in the in the
|
||||
configuration will always be taken relative from the root of the book where the
|
||||
configuration file is located.
|
||||
|
||||
|
||||
### General metadata
|
||||
@@ -58,12 +59,26 @@ 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
|
||||
@@ -72,27 +87,26 @@ renderer need to be specified under the TOML table `[output.html]`.
|
||||
|
||||
The following configuration options are available:
|
||||
|
||||
- **theme:** mdBook comes with a default theme and all the resource files
|
||||
needed for it. But if this option is set, mdBook will selectively overwrite
|
||||
the theme files with the ones found in the specified folder.
|
||||
- **curly-quotes:** Convert straight quotes to curly quotes, except for
|
||||
those that occur in code blocks and code spans. Defaults to `false`.
|
||||
- **google-analytics:** If you use Google Analytics, this option lets you
|
||||
enable it by simply specifying your ID in the configuration file.
|
||||
- **additional-css:** If you need to slightly change the appearance of your
|
||||
book without overwriting the whole style, you can specify a set of
|
||||
stylesheets that will be loaded after the default ones where you can
|
||||
surgically change the style.
|
||||
- **theme:** mdBook comes with a default theme and all the resource files needed
|
||||
for it. But if this option is set, mdBook will selectively overwrite the theme
|
||||
files with the ones found in the specified folder.
|
||||
- **curly-quotes:** Convert straight quotes to curly quotes, except for those
|
||||
that occur in code blocks and code spans. Defaults to `false`.
|
||||
- **google-analytics:** If you use Google Analytics, this option lets you enable
|
||||
it by simply specifying your ID in the configuration file.
|
||||
- **additional-css:** If you need to slightly change the appearance of your book
|
||||
without overwriting the whole style, you can specify a set of 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
|
||||
that will be loaded alongside the default one.
|
||||
removing the current behaviour, you can specify a set of JavaScript files that
|
||||
will be loaded alongside the default one.
|
||||
- **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`.
|
||||
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).
|
||||
- **search:** A subtable for configuring the in-browser search functionality.
|
||||
mdBook must be compiled with the `search` feature enabled (on by default).
|
||||
|
||||
Available configuration options for the `[output.html.playpen]` table:
|
||||
|
||||
@@ -104,26 +118,26 @@ Available configuration options for the `[output.html.playpen]` table:
|
||||
|
||||
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.
|
||||
- **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`.
|
||||
- **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`
|
||||
- **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`.
|
||||
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
|
||||
@@ -132,6 +146,11 @@ 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
|
||||
@@ -154,6 +173,7 @@ boost-hierarchy = 1
|
||||
boost-paragraph = 1
|
||||
expand = true
|
||||
heading-split-level = 3
|
||||
copy-js = true
|
||||
```
|
||||
|
||||
|
||||
@@ -164,10 +184,10 @@ 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.
|
||||
|
||||
Variables starting with `MDBOOK_` are used for configuration. The key is
|
||||
created by removing the `MDBOOK_` prefix and turning the resulting
|
||||
string into `kebab-case`. Double underscores (`__`) separate nested
|
||||
keys, while a single underscore (`_`) is replaced with a dash (`-`).
|
||||
Variables starting with `MDBOOK_` are used for configuration. The key is created
|
||||
by removing the `MDBOOK_` prefix and turning the resulting string into
|
||||
`kebab-case`. Double underscores (`__`) separate nested keys, while a single
|
||||
underscore (`_`) is replaced with a dash (`-`).
|
||||
|
||||
For example:
|
||||
|
||||
@@ -177,21 +197,21 @@ For example:
|
||||
- `MDBOOK_FOO_BAR` -> `foo-bar`
|
||||
- `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
|
||||
|
||||
So by setting the `MDBOOK_BOOK__TITLE` environment variable you can
|
||||
override the book's title without needing to touch your `book.toml`.
|
||||
So by setting the `MDBOOK_BOOK__TITLE` environment variable you can override the
|
||||
book's title without needing to touch your `book.toml`.
|
||||
|
||||
> **Note:** To facilitate setting more complex config items, the value
|
||||
> of an environment variable is first parsed as JSON, falling back to a
|
||||
> string if the parse fails.
|
||||
> **Note:** To facilitate setting more complex config items, the value of an
|
||||
> environment variable is first parsed as JSON, falling back to a string if the
|
||||
> parse fails.
|
||||
>
|
||||
> This means, if you so desired, you could override all book metadata
|
||||
> when building the book with something like
|
||||
> This means, if you so desired, you could override all book metadata when
|
||||
> building the book with something like
|
||||
>
|
||||
> ```text
|
||||
> $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
|
||||
> $ mdbook build
|
||||
> ```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
# MathJax Support
|
||||
|
||||
mdBook has optional support for math equations through [MathJax](https://www.mathjax.org/).
|
||||
mdBook has optional support for math equations through
|
||||
[MathJax](https://www.mathjax.org/).
|
||||
|
||||
To enable MathJax, you need to add the `mathjax-support` key to your `book.toml` under the `output.html` section.
|
||||
To enable MathJax, you need to add the `mathjax-support` key to your `book.toml`
|
||||
under the `output.html` section.
|
||||
|
||||
```toml
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
```
|
||||
|
||||
>**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:** 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:
|
||||
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:
|
||||
```
|
||||
\\( \int x dx = \frac{x^2}{2} + C \\)
|
||||
```
|
||||
|
||||
### Block equations
|
||||
Block equations are delimited by `\\[` and `\\]`. To render the following equation
|
||||
Block equations are delimited by `\\[` and `\\]`. To render the following
|
||||
equation
|
||||
|
||||
\\[ \mu = \frac{1}{N} \sum_{i=0} x_i \\]
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that lets 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() {
|
||||
@@ -34,7 +35,10 @@ With the following syntax, you can include files into your book:
|
||||
|
||||
The path to the file has to be relative from the current source file.
|
||||
|
||||
Usually, this command is used for including code snippets and examples. In this case, oftens one would include a specific part of the file e.g. which only contains the relevant lines for the example. We support four different modes of partial includes:
|
||||
Usually, this command is used for including code snippets and examples. In this
|
||||
case, oftens one would include a specific part of the file e.g. which only
|
||||
contains the relevant lines for the example. We support four different modes of
|
||||
partial includes:
|
||||
|
||||
```hbs
|
||||
\{{#include file.rs:2}}
|
||||
@@ -43,7 +47,11 @@ Usually, this command is used for including code snippets and examples. In this
|
||||
\{{#include file.rs:2:10}}
|
||||
```
|
||||
|
||||
The first command only includes the second line from file `file.rs`. The second command includes all lines up to line 10, i.e. the lines from 11 till the end of the file are omitted. The third command includes all lines from line 2, i.e. the first line is omitted. The last command includes the excerpt of `file.rs` consisting of lines 2 to 10.
|
||||
The first command only includes the second line from file `file.rs`. The second
|
||||
command includes all lines up to line 10, i.e. the lines from 11 till the end of
|
||||
the file are omitted. The third command includes all lines from line 2, i.e. the
|
||||
first line is omitted. The last command includes the excerpt of `file.rs`
|
||||
consisting of lines 2 to 10.
|
||||
|
||||
## Inserting runnable Rust files
|
||||
|
||||
@@ -55,7 +63,9 @@ 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 sent to the [Rust Playpen] to be compiled and run. The result is sent 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:
|
||||
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
# SUMMARY.md
|
||||
|
||||
The summary file is used by mdBook to know what chapters to include,
|
||||
in what order they should appear, what their hierarchy is and where the source files are.
|
||||
Without this file, there is no book.
|
||||
The summary file is used by mdBook to know what chapters to include, in what
|
||||
order they should appear, what their hierarchy is and where the source files
|
||||
are. Without this file, there is no book.
|
||||
|
||||
Even though `SUMMARY.md` is a markdown file, the formatting is very strict to
|
||||
allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
|
||||
|
||||
#### Allowed elements
|
||||
|
||||
1. ***Title*** It's common practice to begin with a title, generally
|
||||
<code class="language-markdown"># Summary</code>.
|
||||
But it is not mandatory, the parser just ignores it. So you can too
|
||||
if you feel like it.
|
||||
1. ***Title*** It's common practice to begin with a title, generally <code
|
||||
class="language-markdown"># Summary</code>. But it is not mandatory, the
|
||||
parser just ignores it. So you can too if you feel like it.
|
||||
|
||||
2. ***Prefix Chapter*** Before the main numbered chapters you can add a couple of elements that will not be numbered. This is useful for
|
||||
forewords, introductions, etc. There are however some constraints. You can not nest prefix chapters, they should all be on the root level. And you can not add prefix chapters once you have added numbered chapters.
|
||||
2. ***Prefix Chapter*** Before the main numbered chapters you can add a couple
|
||||
of elements that will not be numbered. This is useful for forewords,
|
||||
introductions, etc. There are however some constraints. You can not nest
|
||||
prefix chapters, they should all be on the root level. And you can not add
|
||||
prefix chapters once you have added numbered chapters.
|
||||
```markdown
|
||||
[Title of prefix element](relative/path/to/markdown.md)
|
||||
```
|
||||
|
||||
3. ***Numbered Chapter*** Numbered chapters are the main content of the book, they will be numbered and can be nested,
|
||||
resulting in a nice hierarchy (chapters, sub-chapters, etc.)
|
||||
3. ***Numbered Chapter*** Numbered chapters are the main content of the book,
|
||||
they will be numbered and can be nested, resulting in a nice hierarchy
|
||||
(chapters, sub-chapters, etc.)
|
||||
```markdown
|
||||
- [Title of the Chapter](relative/path/to/markdown.md)
|
||||
```
|
||||
You can either use `-` or `*` to indicate a numbered chapter.
|
||||
|
||||
4. ***Suffix Chapter*** After the numbered chapters you can add a couple of non-numbered chapters. They are the same as prefix chapters but come after the numbered chapters instead of before.
|
||||
4. ***Suffix Chapter*** After the numbered chapters you can add a couple of
|
||||
non-numbered chapters. They are the same as prefix chapters but come after
|
||||
the numbered chapters instead of before.
|
||||
|
||||
All other elements are unsupported and will be ignored at best or result in an error.
|
||||
All other elements are unsupported and will be ignored at best or result in an
|
||||
error.
|
||||
|
||||
34
book-example/src/format/theme/README.md
Normal file
34
book-example/src/format/theme/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Theme
|
||||
|
||||
The default renderer uses a [handlebars](http://handlebarsjs.com/) template to
|
||||
render your markdown files and comes with a default theme included in the mdBook
|
||||
binary.
|
||||
|
||||
The theme is totally customizable, you can selectively replace every file from
|
||||
the theme by your own by adding a `theme` directory next to `src` folder in your
|
||||
project root. Create a new file with the name of the file you want to override
|
||||
and now that file will be used instead of the default file.
|
||||
|
||||
Here are the files you can override:
|
||||
|
||||
- ***index.hbs*** is the handlebars template.
|
||||
- ***book.css*** is the style used in the output. If you want to change the
|
||||
design of your book, this is probably the file you want to modify. Sometimes
|
||||
in conjunction with `index.hbs` when you want to radically change the layout.
|
||||
- ***book.js*** is mostly used to add client side functionality, like hiding /
|
||||
un-hiding the sidebar, changing the theme, ...
|
||||
- ***highlight.js*** is the JavaScript that is used to highlight code snippets,
|
||||
you should not need to modify this.
|
||||
- ***highlight.css*** is the theme used for the code highlighting
|
||||
- ***favicon.png*** the favicon that will be used
|
||||
|
||||
Generally, when you want to tweak the theme, you don't need to override all the
|
||||
files. If you only need changes in the stylesheet, there is no point in
|
||||
overriding all the other files. Because custom files take precedence over
|
||||
built-in ones, they will not get updated with new fixes / features.
|
||||
|
||||
**Note:** When you override a file, it is possible that you break some
|
||||
functionality. Therefore I recommend to use the file from the default theme as
|
||||
template and only add / modify what you need. You can copy the default theme
|
||||
into your source directory automatically by using `mdbook init --theme` just
|
||||
remove the files you don't want to override.
|
||||
@@ -1,13 +1,16 @@
|
||||
# Editor
|
||||
|
||||
In addition to providing runnable code playpens, mdBook optionally allows them to be editable. In order to enable editable code blocks, the following needs to be added to the ***book.toml***:
|
||||
In addition to providing runnable code playpens, mdBook optionally allows them
|
||||
to be editable. In order to enable editable code blocks, the following needs to
|
||||
be added to the ***book.toml***:
|
||||
|
||||
```toml
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
```
|
||||
|
||||
To make a specific block available for editing, the attribute `editable` needs to be added to it:
|
||||
To make a specific block available for editing, the attribute `editable` needs
|
||||
to be added to it:
|
||||
|
||||
<pre><code class="language-markdown">```rust,editable
|
||||
fn main() {
|
||||
@@ -29,7 +32,8 @@ Note the new `Undo Changes` button in the editable playpens.
|
||||
|
||||
## Customizing the Editor
|
||||
|
||||
By default, the editor is the [Ace](https://ace.c9.io/) editor, but, if desired, the functionality may be overriden by providing a different folder:
|
||||
By default, the editor is the [Ace](https://ace.c9.io/) editor, but, if desired,
|
||||
the functionality may be overriden by providing a different folder:
|
||||
|
||||
```toml
|
||||
[output.html.playpen]
|
||||
@@ -37,4 +41,6 @@ editable = true
|
||||
editor = "/path/to/editor"
|
||||
```
|
||||
|
||||
Note that for the editor changes to function correctly, the `book.js` inside of the `theme` folder will need to be overriden as it has some couplings with the default Ace editor.
|
||||
Note that for the editor changes to function correctly, the `book.js` inside of
|
||||
the `theme` folder will need to be overriden as it has some couplings with the
|
||||
default Ace editor.
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# index.hbs
|
||||
|
||||
`index.hbs` is the handlebars template that is used to render the book.
|
||||
The markdown files are processed to html and then injected in that template.
|
||||
`index.hbs` is the handlebars template that is used to render the book. The
|
||||
markdown files are processed to html and then injected in that template.
|
||||
|
||||
If you want to change the layout or style of your book, chances are that you will
|
||||
have to modify this template a little bit. Here is what you need to know.
|
||||
If you want to change the layout or style of your book, chances are that you
|
||||
will have to modify this template a little bit. Here is what you need to know.
|
||||
|
||||
## Data
|
||||
|
||||
A lot of data is exposed to the handlebars template with the "context".
|
||||
In the handlebars template you can access this information by using
|
||||
A lot of data is exposed to the handlebars template with the "context". In the
|
||||
handlebars template you can access this information by using
|
||||
|
||||
```handlebars
|
||||
{{name_of_property}}
|
||||
@@ -17,27 +17,33 @@ In the handlebars template you can access this information by using
|
||||
|
||||
Here is a list of the properties that are exposed:
|
||||
|
||||
- ***language*** Language of the book in the form `en`. To use in <code class="language-html">\<html lang="{{ language }}"></code> for example.
|
||||
At the moment it is hardcoded.
|
||||
- ***language*** Language of the book in the form `en`. To use in <code
|
||||
class="language-html">\<html lang="{{ language }}"></code> for example. At the
|
||||
moment it is hardcoded.
|
||||
- ***title*** Title of the book, as specified in `book.toml`
|
||||
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
||||
|
||||
- ***path*** Relative path to the original markdown file from the source directory
|
||||
- ***path*** Relative path to the original markdown file from the source
|
||||
directory
|
||||
- ***content*** This is the rendered markdown.
|
||||
- ***path_to_root*** This is a path containing exclusively `../`'s that points to the root of the book from the current file.
|
||||
Since the original directory structure is maintained, it is useful to prepend relative links with this `path_to_root`.
|
||||
- ***path_to_root*** This is a path containing exclusively `../`'s that points
|
||||
to the root of the book from the current file. Since the original directory
|
||||
structure is maintained, it is useful to prepend relative links with this
|
||||
`path_to_root`.
|
||||
|
||||
- ***chapters*** Is an array of dictionaries of the form
|
||||
```json
|
||||
{"section": "1.2.1", "name": "name of this chapter", "path": "dir/markdown.md"}
|
||||
```
|
||||
containing all the chapters of the book. It is used for example to construct the table of contents (sidebar).
|
||||
containing all the chapters of the book. It is used for example to construct
|
||||
the table of contents (sidebar).
|
||||
|
||||
## Handlebars Helpers
|
||||
|
||||
In addition to the properties you can access, there are some handlebars helpers at your disposal.
|
||||
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 +74,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 +93,5 @@ 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)*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Syntax Highlighting
|
||||
|
||||
For syntax highlighting I use [Highlight.js](https://highlightjs.org) with a custom theme.
|
||||
For syntax highlighting I use [Highlight.js](https://highlightjs.org) with a
|
||||
custom theme.
|
||||
|
||||
Automatic language detection has been turned off, so you will probably want to
|
||||
specify the programming language you use like this
|
||||
@@ -12,19 +13,23 @@ fn main() {
|
||||
```</code></pre>
|
||||
|
||||
## Custom theme
|
||||
Like the rest of the theme, the files used for syntax highlighting can be overridden with your own.
|
||||
Like the rest of the theme, the files used for syntax highlighting can be
|
||||
overridden with your own.
|
||||
|
||||
- ***highlight.js*** normally you shouldn't have to overwrite this file, unless you want to use a more recent version.
|
||||
- ***highlight.js*** normally you shouldn't have to overwrite this file, unless
|
||||
you want to use a more recent version.
|
||||
- ***highlight.css*** theme used by highlight.js for syntax highlighting.
|
||||
|
||||
If you want to use another theme for `highlight.js` download it from their website, or make it yourself,
|
||||
rename it to `highlight.css` and put it in `src/theme` (or the equivalent if you changed your source folder)
|
||||
If you want to use another theme for `highlight.js` download it from their
|
||||
website, or make it yourself, rename it to `highlight.css` and put it in
|
||||
`src/theme` (or the equivalent if you changed your source folder)
|
||||
|
||||
Now your theme will be used instead of the default theme.
|
||||
|
||||
## 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
|
||||
@@ -47,13 +52,18 @@ Will render as
|
||||
# }
|
||||
```
|
||||
|
||||
**At the moment, this only works for code examples that are annotated with `rust`. Because it would collide with semantics of some programming languages. In the future, we want to make this configurable through the `book.toml` so that everyone can benefit from it.**
|
||||
**At the moment, this only works for code examples that are annotated with
|
||||
`rust`. Because it would collide with semantics of some programming languages.
|
||||
In the future, we want to make this configurable through the `book.toml` so that
|
||||
everyone can benefit from it.**
|
||||
|
||||
|
||||
## Improve default theme
|
||||
|
||||
If you think the default theme doesn't look quite right for a specific language, or could be improved.
|
||||
Feel free to [submit a new issue](https://github.com/rust-lang-nursery/mdBook/issues) explaining what you have in mind and I will take a look at it.
|
||||
If you think the default theme doesn't look quite right for a specific language,
|
||||
or could be improved. Feel free to [submit a new
|
||||
issue](https://github.com/rust-lang-nursery/mdBook/issues) explaining what you
|
||||
have in mind and I will take a look at it.
|
||||
|
||||
You could also create a pull-request with the proposed improvements.
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# Theme
|
||||
|
||||
The default renderer uses a [handlebars](http://handlebarsjs.com/) template to render your markdown files and comes with a default theme
|
||||
included in the mdBook binary.
|
||||
|
||||
The theme is totally customizable, you can selectively replace every file from the theme by your own by adding a
|
||||
`theme` directory next to `src` folder in your project root. Create a new file with the name of the file you want to override
|
||||
and now that file will be used instead of the default file.
|
||||
|
||||
Here are the files you can override:
|
||||
|
||||
- ***index.hbs*** is the handlebars template.
|
||||
- ***book.css*** is the style used in the output. If you want to change the design of your book, this is probably the file you want to modify. Sometimes in conjunction with `index.hbs` when you want to radically change the layout.
|
||||
- ***book.js*** is mostly used to add client side functionality, like hiding / un-hiding the sidebar, changing the theme, ...
|
||||
- ***highlight.js*** is the JavaScript that is used to highlight code snippets, you should not need to modify this.
|
||||
- ***highlight.css*** is the theme used for the code highlighting
|
||||
- ***favicon.png*** the favicon that will be used
|
||||
|
||||
Generally, when you want to tweak the theme, you don't need to override all the files. If you only need changes in the stylesheet,
|
||||
there is no point in overriding all the other files. Because custom files take precedence over built-in ones, they will not get updated with new fixes / features.
|
||||
|
||||
**Note:** When you override a file, it is possible that you break some functionality. Therefore I recommend to use the file from the default theme as template and only add / modify what you need. You can copy the default theme into your source directory automatically by using `mdbook init --theme` just remove the files you don't want to override.
|
||||
@@ -1,8 +1,7 @@
|
||||
# Contributors
|
||||
|
||||
Here is a list of the contributors who have helped improving mdBook. Big shout-out to them!
|
||||
|
||||
If you have contributed to mdBook and I forgot to add you, don't hesitate to add yourself to the list. If you are in the list, feel free to add your real name & contact information if you wish.
|
||||
Here is a list of the contributors who have helped improving mdBook. Big
|
||||
shout-out to them!
|
||||
|
||||
- [mdinger](https://github.com/mdinger)
|
||||
- Kevin ([kbknapp](https://github.com/kbknapp))
|
||||
@@ -12,7 +11,10 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add
|
||||
- [funnkill](https://github.com/funkill)
|
||||
- Fu Gangqiang ([FuGangqiang](https://github.com/FuGangqiang))
|
||||
- [Michael-F-Bryan](https://github.com/Michael-F-Bryan)
|
||||
- [Chris Spiegel](https://github.com/cspiegel)
|
||||
- Chris Spiegel ([cspiegel](https://github.com/cspiegel))
|
||||
- [projektir](https://github.com/projektir)
|
||||
- [Phaiax](https://github.com/Phaiax)
|
||||
- [Matt Ickstadt](https://github.com/mattico)
|
||||
- Matt Ickstadt ([mattico](https://github.com/mattico))
|
||||
- Weihang Lo ([@weihanglo](https://github.com/weihanglo))
|
||||
|
||||
If you feel you're missing from this list, feel free to add yourself in a PR.
|
||||
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,12 @@ main() {
|
||||
;;
|
||||
esac
|
||||
|
||||
# This will slow down the build, but is necessary to not run out of disk space
|
||||
cargo clean
|
||||
|
||||
cargo rustc --bin mdbook --target $TARGET --release -- -C lto
|
||||
|
||||
cp target/release/mdbook $stage/
|
||||
cp target/$TARGET/release/mdbook $stage/
|
||||
|
||||
cd $stage
|
||||
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
|
||||
|
||||
@@ -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}"
|
||||
@@ -1,47 +0,0 @@
|
||||
set -ex
|
||||
|
||||
main() {
|
||||
local target=
|
||||
if [ $TRAVIS_OS_NAME = linux ]; then
|
||||
target=x86_64-unknown-linux-musl
|
||||
sort=sort
|
||||
else
|
||||
target=x86_64-apple-darwin
|
||||
sort=gsort # for `sort --sort-version`, from brew's coreutils.
|
||||
fi
|
||||
|
||||
# Builds for iOS are done on OSX, but require the specific target to be
|
||||
# installed.
|
||||
case $TARGET in
|
||||
aarch64-apple-ios)
|
||||
rustup target install aarch64-apple-ios
|
||||
;;
|
||||
armv7-apple-ios)
|
||||
rustup target install armv7-apple-ios
|
||||
;;
|
||||
armv7s-apple-ios)
|
||||
rustup target install armv7s-apple-ios
|
||||
;;
|
||||
i386-apple-ios)
|
||||
rustup target install i386-apple-ios
|
||||
;;
|
||||
x86_64-apple-ios)
|
||||
rustup target install x86_64-apple-ios
|
||||
;;
|
||||
esac
|
||||
|
||||
# This fetches latest stable release
|
||||
local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \
|
||||
| cut -d/ -f3 \
|
||||
| grep -E '^v[0.1.0-9.]+$' \
|
||||
| $sort --version-sort \
|
||||
| tail -n1)
|
||||
curl -LSfs https://japaric.github.io/trust/install.sh | \
|
||||
sh -s -- \
|
||||
--force \
|
||||
--git japaric/cross \
|
||||
--tag $tag \
|
||||
--target $target
|
||||
}
|
||||
|
||||
main
|
||||
@@ -3,15 +3,15 @@ extern crate mdbook;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate pulldown_cmark_to_cmark;
|
||||
|
||||
use mdbook::errors::{Error, Result};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::book::{Book, BookItem, Chapter};
|
||||
use mdbook::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::ffi::OsString;
|
||||
use std::env::{args, args_os};
|
||||
use std::ffi::OsString;
|
||||
use std::process;
|
||||
|
||||
struct Deemphasize;
|
||||
|
||||
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"
|
||||
|
||||
117
src/bin/serve.rs
117
src/bin/serve.rs
@@ -1,117 +0,0 @@
|
||||
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};
|
||||
#[cfg(feature = "watch")]
|
||||
use watch;
|
||||
|
||||
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.")
|
||||
.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)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"-a, --address=[address] 'Address that the browser can reach the websocket server \
|
||||
from{n}(Defaults to the interface address)'",
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Open the book server in a web browser'")
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
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 open_browser = args.is_present("open");
|
||||
|
||||
let address = format!("{}:{}", interface, port);
|
||||
let ws_address = format!("{}:{}", interface, ws_port);
|
||||
|
||||
let livereload_url = format!("ws://{}:{}", public_address, ws_port);
|
||||
book.config
|
||||
.set("output.html.livereload-url", &livereload_url)?;
|
||||
|
||||
book.build()?;
|
||||
|
||||
let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html")));
|
||||
chain.link_after(ErrorRecover);
|
||||
let _iron = Iron::new(chain)
|
||||
.http(&*address)
|
||||
.chain_err(|| "Unable to launch the server")?;
|
||||
|
||||
let ws_server =
|
||||
ws::WebSocket::new(|_| |_| Ok(())).chain_err(|| "Unable to start the websocket")?;
|
||||
|
||||
let broadcaster = ws_server.broadcaster();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
ws_server.listen(&*ws_address).unwrap();
|
||||
});
|
||||
|
||||
let serving_url = format!("http://{}", address);
|
||||
info!("Serving on: {}", serving_url);
|
||||
|
||||
if open_browser {
|
||||
open(serving_url);
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
watch::trigger_on_change(&mut book, move |path, book_dir| {
|
||||
info!("File changed: {:?}", path);
|
||||
info!("Building book...");
|
||||
|
||||
// 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| {
|
||||
b.config.set("output.html.livereload-url", &livereload_url)?;
|
||||
Ok(b)
|
||||
})
|
||||
.and_then(|b| b.build());
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to load the book");
|
||||
utils::log_backtrace(&e);
|
||||
} else {
|
||||
let _ = broadcaster.send("reload");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl AfterMiddleware for ErrorRecover {
|
||||
fn catch(&self, _: &mut Request, err: IronError) -> IronResult<Response> {
|
||||
match err.response.status {
|
||||
// each error will result in 404 response
|
||||
Some(_) => Ok(err.response.set(status::NotFound)),
|
||||
_ => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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;
|
||||
@@ -245,7 +245,8 @@ fn load_chapter<P: AsRef<Path>>(
|
||||
ch.number = link.number.clone();
|
||||
|
||||
sub_item_parents.push(link.name.clone());
|
||||
let sub_items = link.nested_items
|
||||
let sub_items = link
|
||||
.nested_items
|
||||
.iter()
|
||||
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
@@ -297,8 +298,8 @@ impl Display for Chapter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::{TempDir, Builder as TempFileBuilder};
|
||||
use std::io::Write;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
const DUMMY_SRC: &'static str = "
|
||||
# Dummy Chapter
|
||||
@@ -404,14 +405,12 @@ 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()
|
||||
};
|
||||
|
||||
@@ -477,12 +476,12 @@ And here is some \
|
||||
assert_eq!(got.len(), 5);
|
||||
|
||||
// checking the chapter names are in the order should be sufficient here...
|
||||
let chapter_names: Vec<String> = got.into_iter()
|
||||
let chapter_names: Vec<String> = got
|
||||
.into_iter()
|
||||
.filter_map(|i| match *i {
|
||||
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
}).collect();
|
||||
let should_be: Vec<_> = vec![
|
||||
String::from("Chapter 1"),
|
||||
String::from("Hello World"),
|
||||
@@ -535,13 +534,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()
|
||||
};
|
||||
|
||||
@@ -556,13 +553,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)]
|
||||
@@ -110,7 +110,8 @@ impl BookBuilder {
|
||||
fn copy_across_theme(&self) -> Result<()> {
|
||||
debug!("Copying theme");
|
||||
|
||||
let themedir = self.config
|
||||
let themedir = self
|
||||
.config
|
||||
.html_config()
|
||||
.and_then(|html| html.theme)
|
||||
.unwrap_or_else(|| self.config.book.src.join("theme"));
|
||||
@@ -127,8 +128,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 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 = TempFileBuilder::new().prefix("mdbook").tempdir()?;
|
||||
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 {
|
||||
@@ -322,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()),
|
||||
};
|
||||
|
||||
@@ -339,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),
|
||||
}
|
||||
}
|
||||
@@ -403,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
|
||||
@@ -412,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, 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> {
|
||||
@@ -220,11 +221,14 @@ impl<'a> SummaryParser<'a> {
|
||||
fn parse(mut self) -> Result<Summary> {
|
||||
let title = self.parse_title();
|
||||
|
||||
let prefix_chapters = self.parse_affix(true)
|
||||
let prefix_chapters = self
|
||||
.parse_affix(true)
|
||||
.chain_err(|| "There was an error parsing the prefix chapters")?;
|
||||
let numbered_chapters = self.parse_numbered()
|
||||
let numbered_chapters = self
|
||||
.parse_numbered()
|
||||
.chain_err(|| "There was an error parsing the numbered chapters")?;
|
||||
let suffix_chapters = self.parse_affix(false)
|
||||
let suffix_chapters = self
|
||||
.parse_affix(false)
|
||||
.chain_err(|| "There was an error parsing the suffix chapters")?;
|
||||
|
||||
Ok(Summary {
|
||||
@@ -471,8 +475,7 @@ fn stringify_events(events: Vec<Event>) -> String {
|
||||
.filter_map(|t| match t {
|
||||
Event::Text(text) => Some(text.into_owned()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
|
||||
@@ -659,14 +662,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,19 @@
|
||||
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)'",
|
||||
)
|
||||
.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 'Opens the compiled book in a web browser'")
|
||||
}
|
||||
|
||||
// Build command implementation
|
||||
@@ -24,7 +22,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()?;
|
||||
@@ -1,17 +1,19 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::*;
|
||||
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("Delete built book")
|
||||
.about("Deletes a built book")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'The directory of built 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] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +23,7 @@ pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
|
||||
let book = MDBook::load(&book_dir)?;
|
||||
|
||||
let dir_to_remove = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => PathBuf::from(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")?;
|
||||
@@ -1,22 +1,21 @@
|
||||
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 std::process::Command;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::utils;
|
||||
use mdbook::config;
|
||||
use get_book_dir;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
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
|
||||
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;
|
||||
149
src/cmd/serve.rs
Normal file
149
src/cmd/serve.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
extern crate iron;
|
||||
extern crate staticfile;
|
||||
extern crate ws;
|
||||
|
||||
use self::iron::{
|
||||
status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set,
|
||||
};
|
||||
#[cfg(feature = "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("Serves a book at http://localhost:3000, and rebuilds it on changes")
|
||||
.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("hostname")
|
||||
.short("n")
|
||||
.long("hostname")
|
||||
.takes_value(true)
|
||||
.default_value("localhost")
|
||||
.empty_values(false)
|
||||
.help("Hostname to listen on for HTTP connections"),
|
||||
)
|
||||
.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
|
||||
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();
|
||||
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!("{}:{}", 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")));
|
||||
chain.link_after(ErrorRecover);
|
||||
let _iron = Iron::new(chain)
|
||||
.http(&*address)
|
||||
.chain_err(|| "Unable to launch the server")?;
|
||||
|
||||
let ws_server =
|
||||
ws::WebSocket::new(|_| |_| Ok(())).chain_err(|| "Unable to start the websocket")?;
|
||||
|
||||
let broadcaster = ws_server.broadcaster();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
ws_server.listen(&*ws_address).unwrap();
|
||||
});
|
||||
|
||||
let serving_url = format!("http://{}", address);
|
||||
info!("Serving on: {}", serving_url);
|
||||
|
||||
if open_browser {
|
||||
open(serving_url);
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
watch::trigger_on_change(&mut book, move |path, book_dir| {
|
||||
info!("File changed: {:?}", path);
|
||||
info!("Building book...");
|
||||
|
||||
// FIXME: This area is really ugly because we need to re-set livereload :(
|
||||
|
||||
let result = MDBook::load(&book_dir)
|
||||
.and_then(|mut b| {
|
||||
b.config
|
||||
.set("output.html.livereload-url", &livereload_url)?;
|
||||
Ok(b)
|
||||
}).and_then(|b| b.build());
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to load the book");
|
||||
utils::log_backtrace(&e);
|
||||
} else {
|
||||
let _ = broadcaster.send("reload");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl AfterMiddleware for ErrorRecover {
|
||||
fn catch(&self, _: &mut Request, err: IronError) -> IronResult<Response> {
|
||||
match err.response.status {
|
||||
// each error will result in 404 response
|
||||
Some(_) => Ok(err.response.set(status::NotFound)),
|
||||
_ => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/cmd/test.rs
Normal file
45
src/cmd/test.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
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,26 @@
|
||||
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 +51,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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -463,9 +464,11 @@ impl Default for Playpen {
|
||||
#[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`,
|
||||
/// 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`.
|
||||
@@ -494,6 +497,7 @@ 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,
|
||||
@@ -530,11 +534,7 @@ trait Updateable<'de>: Serialize + Deserialize<'de> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Updateable<'de> for T
|
||||
where
|
||||
T: Serialize + Deserialize<'de>,
|
||||
{
|
||||
}
|
||||
impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -107,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 {
|
||||
|
||||
@@ -8,61 +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 clean;
|
||||
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::ArgRequiredElseHelp)
|
||||
.after_help("For more information about a specific command, \
|
||||
try `mdbook <command> --help`\n\
|
||||
Source code for mdbook available \
|
||||
at: https://github.com/rust-lang-nursery/mdBook")
|
||||
.subcommand(init::make_subcommand())
|
||||
.subcommand(build::make_subcommand())
|
||||
.subcommand(test::make_subcommand())
|
||||
.subcommand(clean::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),
|
||||
("clean", Some(sub_matches)) => clean::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!(),
|
||||
};
|
||||
|
||||
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,15 +1,14 @@
|
||||
use book::{Book, BookItem, Chapter};
|
||||
use book::{Book, BookItem};
|
||||
use config::{Config, HtmlConfig, Playpen};
|
||||
use errors::*;
|
||||
use renderer::{RenderContext, Renderer};
|
||||
use renderer::html_handlebars::helpers;
|
||||
use theme::{self, Theme, playpen_editor};
|
||||
use renderer::{RenderContext, Renderer};
|
||||
use theme::{self, playpen_editor, Theme};
|
||||
use utils;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Read;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use handlebars::Handlebars;
|
||||
@@ -38,14 +37,11 @@ impl HtmlHandlebars {
|
||||
print_content.push_str(&content);
|
||||
|
||||
// Update the context with data for this file
|
||||
let path = ch.path
|
||||
let path = ch
|
||||
.path
|
||||
.to_str()
|
||||
.chain_err(|| "Could not convert path to str")?;
|
||||
let filepath = Path::new(&ch.path)
|
||||
.with_extension("html");
|
||||
let filepathstr = filepath.to_str()
|
||||
.chain_err(|| "Could not convert HTML path to str")?;
|
||||
let filepathstr = utils::fs::normalize_path(filepathstr);
|
||||
let filepath = Path::new(&ch.path).with_extension("html");
|
||||
|
||||
// "print.html" is used for the print page.
|
||||
if ch.path == Path::new("print.md") {
|
||||
@@ -55,10 +51,11 @@ impl HtmlHandlebars {
|
||||
// Non-lexical lifetimes needed :'(
|
||||
let title: String;
|
||||
{
|
||||
let book_title = ctx.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
let book_title = ctx
|
||||
.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
title = ch.name.clone() + " - " + book_title;
|
||||
}
|
||||
|
||||
@@ -66,25 +63,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 rendered = self.post_process(
|
||||
rendered,
|
||||
&filepathstr,
|
||||
&ctx.html_config.playpen,
|
||||
);
|
||||
let rendered = self.post_process(rendered, &ctx.html_config.playpen);
|
||||
|
||||
// Write to file
|
||||
debug!("Creating {} ✓", filepathstr);
|
||||
utils::fs::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(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -93,41 +98,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");
|
||||
|
||||
utils::fs::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);
|
||||
|
||||
@@ -142,8 +115,17 @@ impl HtmlHandlebars {
|
||||
) -> Result<()> {
|
||||
use utils::fs::write_file;
|
||||
|
||||
write_file(
|
||||
destination,
|
||||
".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, "book.css", &theme.css)?;
|
||||
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)?;
|
||||
@@ -152,37 +134,37 @@ impl HtmlHandlebars {
|
||||
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/css/font-awesome.css",
|
||||
"FontAwesome/css/font-awesome.css",
|
||||
theme::FONT_AWESOME,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
"FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
"FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
"FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/FontAwesome.ttf",
|
||||
"FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
|
||||
@@ -195,7 +177,8 @@ impl HtmlHandlebars {
|
||||
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,
|
||||
write_file(
|
||||
destination,
|
||||
"theme-tomorrow_night.js",
|
||||
playpen_editor::THEME_TOMORROW_NIGHT_JS,
|
||||
)?;
|
||||
@@ -205,28 +188,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");
|
||||
@@ -257,6 +254,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"
|
||||
@@ -273,9 +289,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");
|
||||
@@ -304,9 +330,7 @@ impl Renderer for HtmlHandlebars {
|
||||
is_index: is_index,
|
||||
html_config: html_config.clone(),
|
||||
};
|
||||
self.render_item(item,
|
||||
ctx,
|
||||
&mut print_content)?;
|
||||
self.render_item(item, ctx, &mut print_content)?;
|
||||
is_index = false;
|
||||
}
|
||||
|
||||
@@ -320,11 +344,9 @@ impl Renderer for HtmlHandlebars {
|
||||
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);
|
||||
|
||||
utils::fs::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");
|
||||
@@ -335,7 +357,12 @@ impl Renderer for HtmlHandlebars {
|
||||
|
||||
// Render search index
|
||||
#[cfg(feature = "search")]
|
||||
super::search::create_files(&html_config.search.unwrap_or_default(), &destination, &book)?;
|
||||
{
|
||||
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"])?;
|
||||
@@ -344,14 +371,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));
|
||||
@@ -371,13 +409,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));
|
||||
@@ -389,12 +422,13 @@ 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));
|
||||
@@ -406,16 +440,20 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
|
||||
|
||||
let search = html_config.search.clone();
|
||||
if cfg!(feature = "search") {
|
||||
data.insert("search_enabled".to_owned(), json!(true));
|
||||
if search.unwrap_or_default().copy_js {
|
||||
data.insert("search_js".to_owned(), json!(true));
|
||||
}
|
||||
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")
|
||||
warn!(
|
||||
"please reinstall with `cargo install mdbook --force --features search`to use the \
|
||||
search feature"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
let mut chapters = vec![];
|
||||
|
||||
for item in book.iter() {
|
||||
@@ -429,7 +467,8 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
|
||||
}
|
||||
|
||||
chapter.insert("name".to_owned(), json!(ch.name));
|
||||
let path = ch.path
|
||||
let path = ch
|
||||
.path
|
||||
.to_str()
|
||||
.chain_err(|| "Could not convert path to str")?;
|
||||
chapter.insert("path".to_owned(), json!(path));
|
||||
@@ -450,26 +489,27 @@ 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 {
|
||||
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);
|
||||
@@ -482,34 +522,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
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -520,51 +539,55 @@ 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) {
|
||||
@@ -605,37 +628,32 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
pub mod toc;
|
||||
pub mod navigation;
|
||||
pub mod toc;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
|
||||
use serde_json;
|
||||
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
|
||||
|
||||
use utils;
|
||||
|
||||
type StringMap = BTreeMap<String, String>;
|
||||
|
||||
@@ -43,15 +45,20 @@ impl Target {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_chapter(rc: &mut RenderContext, target: Target) -> Result<Option<StringMap>, RenderError> {
|
||||
fn find_chapter(
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
target: Target,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
debug!("Get data from context");
|
||||
|
||||
let chapters = rc.evaluate_absolute("chapters", true).and_then(|c| {
|
||||
let chapters = rc.evaluate_absolute(ctx, "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", true)?
|
||||
let base_path = rc
|
||||
.evaluate_absolute(ctx, "path", true)?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
@@ -81,12 +88,24 @@ fn find_chapter(rc: &mut RenderContext, target: Target) -> Result<Option<StringM
|
||||
fn render(
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
chapter: &StringMap,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("Creating BTreeMap to inject in context");
|
||||
|
||||
let mut context = BTreeMap::new();
|
||||
let base_path = rc
|
||||
.evaluate_absolute(ctx, "path", false)?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
context.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&base_path)),
|
||||
);
|
||||
|
||||
chapter
|
||||
.get("name")
|
||||
@@ -109,28 +128,41 @@ fn render(
|
||||
_h.template()
|
||||
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
||||
.and_then(|t| {
|
||||
let mut local_rc = rc.with_context(Context::wraps(&context)?);
|
||||
t.render(r, &mut local_rc)
|
||||
let mut local_rc = rc.new_for_block();
|
||||
let local_ctx = Context::wraps(&context)?;
|
||||
t.render(r, &local_ctx, &mut local_rc, out)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
pub fn previous(
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("previous (handlebars helper)");
|
||||
|
||||
if let Some(previous) = find_chapter(rc, Target::Previous)? {
|
||||
render(_h, r, rc, &previous)?;
|
||||
if let Some(previous) = find_chapter(ctx, rc, Target::Previous)? {
|
||||
render(_h, r, ctx, rc, out, &previous)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
pub fn next(
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("next (handlebars helper)");
|
||||
|
||||
if let Some(next) = find_chapter(rc, Target::Next)? {
|
||||
render(_h, r, rc, &next)?;
|
||||
if let Some(next) = find_chapter(ctx, rc, Target::Next)? {
|
||||
render(_h, r, ctx, rc, out, &next)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, Helper, HelperDef, RenderContext, RenderError};
|
||||
use utils;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
|
||||
use pulldown_cmark::{html, Event, Parser, Tag};
|
||||
use serde_json;
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -12,27 +14,35 @@ pub struct RenderToc {
|
||||
}
|
||||
|
||||
impl HelperDef for RenderToc {
|
||||
fn call(&self, _h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
_h: &Helper,
|
||||
_: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
) -> Result<(), RenderError> {
|
||||
// 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", true).and_then(|c| {
|
||||
let chapters = rc.evaluate_absolute(ctx, "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", true)?
|
||||
let current = rc
|
||||
.evaluate_absolute(ctx, "path", true)?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
rc.writer.write_all(b"<ol class=\"chapter\">")?;
|
||||
out.write("<ol class=\"chapter\">")?;
|
||||
|
||||
let mut current_level = 1;
|
||||
|
||||
for item in chapters {
|
||||
// Spacer
|
||||
if item.get("spacer").is_some() {
|
||||
rc.writer.write_all(b"<li class=\"spacer\"></li>")?;
|
||||
out.write("<li class=\"spacer\"></li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -44,30 +54,30 @@ impl HelperDef for RenderToc {
|
||||
|
||||
if level > current_level {
|
||||
while level > current_level {
|
||||
rc.writer.write_all(b"<li>")?;
|
||||
rc.writer.write_all(b"<ol class=\"section\">")?;
|
||||
out.write("<li>")?;
|
||||
out.write("<ol class=\"section\">")?;
|
||||
current_level += 1;
|
||||
}
|
||||
rc.writer.write_all(b"<li>")?;
|
||||
out.write("<li>")?;
|
||||
} else if level < current_level {
|
||||
while level < current_level {
|
||||
rc.writer.write_all(b"</ol>")?;
|
||||
rc.writer.write_all(b"</li>")?;
|
||||
out.write("</ol>")?;
|
||||
out.write("</li>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
rc.writer.write_all(b"<li>")?;
|
||||
out.write("<li>")?;
|
||||
} else {
|
||||
rc.writer.write_all(b"<li")?;
|
||||
out.write("<li")?;
|
||||
if item.get("section").is_none() {
|
||||
rc.writer.write_all(b" class=\"affix\"")?;
|
||||
out.write(" class=\"affix\"")?;
|
||||
}
|
||||
rc.writer.write_all(b">")?;
|
||||
out.write(">")?;
|
||||
}
|
||||
|
||||
// Link
|
||||
let path_exists = if let Some(path) = item.get("path") {
|
||||
if !path.is_empty() {
|
||||
rc.writer.write_all(b"<a href=\"")?;
|
||||
out.write("<a href=\"")?;
|
||||
|
||||
let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)"))
|
||||
.with_extension("html")
|
||||
@@ -77,14 +87,15 @@ impl HelperDef for RenderToc {
|
||||
.replace("\\", "/");
|
||||
|
||||
// Add link
|
||||
rc.writer.write_all(tmp.as_bytes())?;
|
||||
rc.writer.write_all(b"\"")?;
|
||||
out.write(&utils::fs::path_to_root(¤t))?;
|
||||
out.write(&tmp)?;
|
||||
out.write("\"")?;
|
||||
|
||||
if path == ¤t {
|
||||
rc.writer.write_all(b" class=\"active\"")?;
|
||||
out.write(" class=\"active\"")?;
|
||||
}
|
||||
|
||||
rc.writer.write_all(b">")?;
|
||||
out.write(">")?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -96,9 +107,9 @@ impl HelperDef for RenderToc {
|
||||
if !self.no_section_label {
|
||||
// Section does not necessarily exist
|
||||
if let Some(section) = item.get("section") {
|
||||
rc.writer.write_all(b"<strong aria-hidden=\"true\">")?;
|
||||
rc.writer.write_all(section.as_bytes())?;
|
||||
rc.writer.write_all(b"</strong> ")?;
|
||||
out.write("<strong aria-hidden=\"true\">")?;
|
||||
out.write(§ion)?;
|
||||
out.write("</strong> ")?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,22 +130,22 @@ impl HelperDef for RenderToc {
|
||||
html::push_html(&mut markdown_parsed_name, parser);
|
||||
|
||||
// write to the handlebars template
|
||||
rc.writer.write_all(markdown_parsed_name.as_bytes())?;
|
||||
out.write(&markdown_parsed_name)?;
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
rc.writer.write_all(b"</a>")?;
|
||||
out.write("</a>")?;
|
||||
}
|
||||
|
||||
rc.writer.write_all(b"</li>")?;
|
||||
out.write("</li>")?;
|
||||
}
|
||||
while current_level > 1 {
|
||||
rc.writer.write_all(b"</ol>")?;
|
||||
rc.writer.write_all(b"</li>")?;
|
||||
out.write("</ol>")?;
|
||||
out.write("</li>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
|
||||
rc.writer.write_all(b"</ol>")?;
|
||||
out.write("</ol>")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,29 +5,38 @@ use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use self::elasticlunr::Index;
|
||||
use pulldown_cmark::*;
|
||||
use serde_json;
|
||||
use self::elasticlunr::Index;
|
||||
|
||||
use book::{Book, BookItem};
|
||||
use config::Search;
|
||||
use errors::*;
|
||||
use utils;
|
||||
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, item)?;
|
||||
render_item(&mut index, &search_config, &mut doc_urls, item)?;
|
||||
}
|
||||
|
||||
let index = write_to_js(index, &search_config)?;
|
||||
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.js", index.as_bytes())?;
|
||||
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)?;
|
||||
@@ -38,18 +47,22 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||
}
|
||||
|
||||
/// Uses the given arguments to construct a search document, then inserts it to the given index.
|
||||
fn add_doc<'a>(
|
||||
fn add_doc(
|
||||
index: &mut Index,
|
||||
anchor_base: &'a str,
|
||||
doc_urls: &mut Vec<String>,
|
||||
anchor_base: &str,
|
||||
section_id: &Option<String>,
|
||||
items: &[&str],
|
||||
) {
|
||||
let doc_ref: Cow<'a, str> = if let &Some(ref id) = section_id {
|
||||
format!("{}#{}", anchor_base, id).into()
|
||||
let url = if let &Some(ref id) = section_id {
|
||||
Cow::Owned(format!("{}#{}", anchor_base, id))
|
||||
} else {
|
||||
anchor_base.into()
|
||||
Cow::Borrowed(anchor_base)
|
||||
};
|
||||
let doc_ref = utils::collapse_whitespace(doc_ref.trim());
|
||||
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);
|
||||
}
|
||||
@@ -58,6 +71,7 @@ fn add_doc<'a>(
|
||||
fn render_item(
|
||||
index: &mut Index,
|
||||
search_config: &Search,
|
||||
doc_urls: &mut Vec<String>,
|
||||
item: &BookItem,
|
||||
) -> Result<()> {
|
||||
let chapter = match item {
|
||||
@@ -92,6 +106,7 @@ fn render_item(
|
||||
// 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(" » ")],
|
||||
@@ -144,6 +159,7 @@ fn render_item(
|
||||
// Make sure the last section is added to the index
|
||||
add_doc(
|
||||
index,
|
||||
doc_urls,
|
||||
&anchor_base,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
@@ -153,12 +169,9 @@ fn render_item(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Exports the index and search options to a JS script which stores the index in `window.search`.
|
||||
/// Using a JS script is a workaround for CORS in `file://` URIs. It also removes the need for
|
||||
/// downloading/parsing JSON in JS.
|
||||
fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
|
||||
use std::collections::BTreeMap;
|
||||
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 {
|
||||
@@ -169,9 +182,11 @@ fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
|
||||
#[derive(Serialize)]
|
||||
struct SearchindexJson {
|
||||
/// The options used for displaying search results
|
||||
resultsoptions: ResultsOptions,
|
||||
results_options: ResultsOptions,
|
||||
/// The searchoptions for elasticlunr.js
|
||||
searchoptions: SearchOptions,
|
||||
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,
|
||||
}
|
||||
@@ -185,7 +200,7 @@ fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
|
||||
opt.boost = Some(search_config.boost_hierarchy);
|
||||
fields.insert("breadcrumbs".into(), opt);
|
||||
|
||||
let searchoptions = SearchOptions {
|
||||
let search_options = SearchOptions {
|
||||
bool: if search_config.use_boolean_and {
|
||||
SearchBool::And
|
||||
} else {
|
||||
@@ -195,19 +210,24 @@ fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
|
||||
fields,
|
||||
};
|
||||
|
||||
let resultsoptions = ResultsOptions {
|
||||
let results_options = ResultsOptions {
|
||||
limit_results: search_config.limit_results,
|
||||
teaser_word_count: search_config.teaser_word_count,
|
||||
};
|
||||
|
||||
let json_contents = SearchindexJson {
|
||||
resultsoptions,
|
||||
searchoptions,
|
||||
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(format!("window.search = {};", json_contents))
|
||||
Ok(json_contents)
|
||||
}
|
||||
|
||||
fn clean_html(html: &str) -> 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");
|
||||
|
||||
@@ -157,22 +157,27 @@ impl Renderer for CmdRenderer {
|
||||
|
||||
let _ = fs::create_dir_all(&ctx.destination);
|
||||
|
||||
let mut child = match self.compose_command()?
|
||||
let mut child = match self
|
||||
.compose_command()?
|
||||
.stdin(Stdio::piped())
|
||||
.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 |
1454
src/theme/book.css
1454
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,9 +300,9 @@ 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() {
|
||||
@@ -373,7 +383,7 @@ function playpen_text(playpen) {
|
||||
|
||||
themePopup.addEventListener('focusout', function(e) {
|
||||
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
|
||||
if (!!e.relatedTarget && !themePopup.contains(e.relatedTarget)) {
|
||||
if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
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: hsl(210, 25%, 8%);
|
||||
--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: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
|
||||
--table-border-color: hsl(210, 25%, 13%);
|
||||
--table-header-bg: hsl(210, 25%, 28%);
|
||||
--table-alternate-bg: hsl(210, 25%, 11%);
|
||||
|
||||
--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: hsl(200, 7%, 8%);
|
||||
--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: hsl(234, 21%, 18%);
|
||||
--quote-border: hsl(234, 21%, 23%);
|
||||
|
||||
--table-border-color: hsl(200, 7%, 13%);
|
||||
--table-header-bg: hsl(200, 7%, 28%);
|
||||
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||
|
||||
--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: hsl(0, 0%, 100%);
|
||||
--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: hsl(197, 37%, 96%);
|
||||
--quote-border: hsl(197, 37%, 91%);
|
||||
|
||||
--table-border-color: hsl(0, 0%, 95%);
|
||||
--table-header-bg: hsl(0, 0%, 80%);
|
||||
--table-alternate-bg: hsl(0, 0%, 97%);
|
||||
|
||||
--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: hsl(226, 23%, 11%);
|
||||
--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: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
|
||||
--table-border-color: hsl(226, 23%, 16%);
|
||||
--table-header-bg: hsl(226, 23%, 31%);
|
||||
--table-alternate-bg: hsl(226, 23%, 14%);
|
||||
|
||||
--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: hsl(60, 9%, 87%);
|
||||
--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: hsl(60, 5%, 75%);
|
||||
--quote-border: hsl(60, 5%, 70%);
|
||||
|
||||
--table-border-color: hsl(60, 9%, 82%);
|
||||
--table-header-bg: #b3a497;
|
||||
--table-alternate-bg: hsl(60, 9%, 84%);
|
||||
|
||||
--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,5 +1,5 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}" class="sidebar-visible">
|
||||
<html lang="{{ language }}" class="sidebar-visible no-js">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
@@ -9,49 +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>
|
||||
|
||||
<noscript>
|
||||
<style type="text/css">
|
||||
.javascript-only {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
|
||||
</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 {
|
||||
@@ -74,7 +61,7 @@
|
||||
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 -->
|
||||
@@ -99,7 +86,7 @@
|
||||
{{> header}}
|
||||
<div id="menu-bar" class="menu-bar">
|
||||
<div id="menu-bar-sticky-container">
|
||||
<div class="left-buttons javascript-only">
|
||||
<div class="left-buttons">
|
||||
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
@@ -123,7 +110,7 @@
|
||||
<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>
|
||||
@@ -131,13 +118,15 @@
|
||||
</div>
|
||||
|
||||
{{#if search_enabled}}
|
||||
<div id="searchbar-outer" class="searchbar-outer">
|
||||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
</div>
|
||||
<div id="searchresults-outer" class="searchresults-outer">
|
||||
<div class="searchresults-header" id="searchresults-header"></div>
|
||||
<ul id="searchresults">
|
||||
</ul>
|
||||
<div 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}}
|
||||
|
||||
@@ -158,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}}
|
||||
@@ -176,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}}
|
||||
@@ -190,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">
|
||||
@@ -221,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
|
||||
@@ -239,37 +216,45 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if playpen_js}}
|
||||
<script src="ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="editor.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="mode-rust.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="theme-dawn.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
|
||||
<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_enabled}}
|
||||
<script src="searchindex.js" type="text/javascript" charset="utf-8"></script>
|
||||
{{/if}}
|
||||
{{#if search_js}}
|
||||
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||
<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>
|
||||
|
||||
@@ -5,15 +5,18 @@ pub mod playpen_editor;
|
||||
#[cfg(feature = "search")]
|
||||
pub mod searcher;
|
||||
|
||||
use std::path::Path;
|
||||
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");
|
||||
@@ -21,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
|
||||
@@ -45,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>,
|
||||
@@ -73,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 {
|
||||
@@ -102,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(),
|
||||
@@ -130,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 tempfile::Builder as TempFileBuilder;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
|
||||
#[test]
|
||||
fn theme_uses_defaults_with_nonexistent_src_dir() {
|
||||
@@ -150,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 = TempFileBuilder::new().prefix("mdbook").tempdir().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());
|
||||
@@ -172,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,15 +1,24 @@
|
||||
"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 his search.
|
||||
// event handlers while the user is typing their search.
|
||||
|
||||
if (!Mark || !elasticlunr) {
|
||||
return;
|
||||
}
|
||||
|
||||
var searchbar = document.getElementById('searchbar'),
|
||||
|
||||
//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'),
|
||||
@@ -18,11 +27,12 @@ window.search = window.search || {};
|
||||
content = document.getElementById('content'),
|
||||
|
||||
searchindex = null,
|
||||
resultsoptions = {
|
||||
doc_urls = [],
|
||||
results_options = {
|
||||
teaser_word_count: 30,
|
||||
limit_results: 30,
|
||||
},
|
||||
searchoptions = {
|
||||
search_options = {
|
||||
bool: "AND",
|
||||
expand: true,
|
||||
fields: {
|
||||
@@ -130,12 +140,12 @@ window.search = window.search || {};
|
||||
teaser_count++;
|
||||
|
||||
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
|
||||
var url = result.ref.split("#");
|
||||
var url = doc_urls[result.ref].split("#");
|
||||
if (url.length == 1) { // no anchor found
|
||||
url.push("");
|
||||
}
|
||||
|
||||
return '<a href="' + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
|
||||
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>';
|
||||
@@ -187,7 +197,7 @@ window.search = window.search || {};
|
||||
}
|
||||
|
||||
var window_weight = [];
|
||||
var window_size = Math.min(weighted.length, resultsoptions.teaser_word_count);
|
||||
var window_size = Math.min(weighted.length, results_options.teaser_word_count);
|
||||
|
||||
var cur_sum = 0;
|
||||
for (var wordindex = 0; wordindex < window_size; wordindex++) {
|
||||
@@ -237,17 +247,21 @@ window.search = window.search || {};
|
||||
return teaser_split.join('');
|
||||
}
|
||||
|
||||
function init() {
|
||||
resultsoptions = window.search.resultsoptions;
|
||||
searchoptions = window.search.searchoptions;
|
||||
searchindex = elasticlunr.Index.load(window.search.index);
|
||||
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);
|
||||
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();
|
||||
@@ -297,94 +311,84 @@ window.search = window.search || {};
|
||||
|
||||
// Eventhandler for keyevents on `document`
|
||||
function globalKeyHandler(e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea') { return; }
|
||||
|
||||
if (e.keyCode == ESCAPE_KEYCODE) {
|
||||
if (e.keyCode === ESCAPE_KEYCODE) {
|
||||
e.preventDefault();
|
||||
searchbar.classList.remove("active");
|
||||
setSearchUrlParameters("",
|
||||
(searchbar.value.trim() != "") ? "push" : "replace");
|
||||
(searchbar.value.trim() !== "") ? "push" : "replace");
|
||||
if (hasFocus()) {
|
||||
unfocusSearchbar();
|
||||
}
|
||||
showSearch(false);
|
||||
marker.unmark();
|
||||
return;
|
||||
}
|
||||
if (!hasFocus() && e.keyCode == SEARCH_HOTKEY_KEYCODE) {
|
||||
} else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) {
|
||||
e.preventDefault();
|
||||
showSearch(true);
|
||||
window.scrollTo(0, 0);
|
||||
searchbar.focus();
|
||||
return;
|
||||
}
|
||||
if (hasFocus() && e.keyCode == DOWN_KEYCODE) {
|
||||
searchbar.select();
|
||||
} else if (hasFocus() && e.keyCode === DOWN_KEYCODE) {
|
||||
e.preventDefault();
|
||||
unfocusSearchbar();
|
||||
searchresults.children('li').first().classList.add("focus");
|
||||
return;
|
||||
}
|
||||
if (!hasFocus() && (e.keyCode == DOWN_KEYCODE
|
||||
|| e.keyCode == UP_KEYCODE
|
||||
|| e.keyCode == SELECT_KEYCODE)) {
|
||||
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 current_focus = search.searchresults.find("li.focus");
|
||||
if (current_focus.length == 0) return;
|
||||
var focused = searchresults.querySelector("li.focus");
|
||||
if (!focused) return;
|
||||
e.preventDefault();
|
||||
if (e.keyCode == DOWN_KEYCODE) {
|
||||
var next = current_focus.next()
|
||||
if (next.length > 0) {
|
||||
current_focus.classList.remove("focus");
|
||||
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) {
|
||||
current_focus.classList.remove("focus");
|
||||
var prev = current_focus.prev();
|
||||
if (prev.length == 0) {
|
||||
searchbar.focus();
|
||||
} else {
|
||||
} else if (e.keyCode === UP_KEYCODE) {
|
||||
focused.classList.remove("focus");
|
||||
var prev = focused.previousElementSibling;
|
||||
if (prev) {
|
||||
prev.classList.add("focus");
|
||||
} else {
|
||||
searchbar.select();
|
||||
}
|
||||
} else {
|
||||
window.location = current_focus.children('a').attr('href');
|
||||
} else { // SELECT_KEYCODE
|
||||
window.location.assign(focused.querySelector('a'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showSearch(yes) {
|
||||
if (yes) {
|
||||
searchbar_outer.style.display = 'block';
|
||||
content.style.display = 'none';
|
||||
search_wrap.classList.remove('hidden');
|
||||
searchicon.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
content.style.display = 'block';
|
||||
searchbar_outer.style.display = 'none';
|
||||
searchresults_outer.style.display = 'none';
|
||||
searchbar.value = '';
|
||||
removeChildren(searchresults);
|
||||
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) {
|
||||
searchbar_outer.style.display = 'block';
|
||||
content.style.display = 'none';
|
||||
searchresults_outer.style.display = 'block';
|
||||
searchresults_outer.classList.remove('hidden');
|
||||
} else {
|
||||
content.style.display = 'block';
|
||||
searchresults_outer.style.display = 'none';
|
||||
searchresults_outer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Eventhandler for search icon
|
||||
function searchIconClickHandler() {
|
||||
if (searchbar_outer.style.display === 'block') {
|
||||
showSearch(false);
|
||||
} else {
|
||||
if (search_wrap.classList.contains('hidden')) {
|
||||
showSearch(true);
|
||||
window.scrollTo(0, 0);
|
||||
searchbar.focus();
|
||||
searchbar.select();
|
||||
} else {
|
||||
showSearch(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,8 +443,8 @@ window.search = window.search || {};
|
||||
if (searchindex == null) { return; }
|
||||
|
||||
// Do the actual search
|
||||
var results = searchindex.search(searchterm, searchoptions);
|
||||
var resultcount = Math.min(results.length, resultsoptions.limit_results);
|
||||
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);
|
||||
@@ -458,7 +462,16 @@ window.search = window.search || {};
|
||||
showResults(true);
|
||||
}
|
||||
|
||||
init();
|
||||
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,12 +0,0 @@
|
||||
@import "nib"
|
||||
|
||||
@import 'general'
|
||||
@import 'sidebar'
|
||||
@import 'page'
|
||||
@import 'menu'
|
||||
@import 'nav-icons'
|
||||
@import 'theme-popup'
|
||||
@import 'themes'
|
||||
@import 'print'
|
||||
@import 'tooltip'
|
||||
@import 'searchbar'
|
||||
@@ -1,72 +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 }
|
||||
|
||||
a.header:target h1:before,
|
||||
a.header:target h2:before,
|
||||
a.header:target h3:before,
|
||||
a.header:target h4:before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-left: -30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
|
||||
td {
|
||||
padding: 3px 20px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
thead {
|
||||
td { font-weight: 700; }
|
||||
}
|
||||
}
|
||||
|
||||
:not(.footnote-definition) + .footnote-definition,
|
||||
.footnote-definition + :not(.footnote-definition) {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.footnote-definition {
|
||||
font-size: 0.9em;
|
||||
margin: 0.5em 0;
|
||||
|
||||
p { display: inline; }
|
||||
}
|
||||
@@ -1,42 +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
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -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,51 +0,0 @@
|
||||
@require 'variables'
|
||||
|
||||
.page-wrapper {
|
||||
box-sizing: border-box
|
||||
left: 0
|
||||
position: absolute
|
||||
right: 0
|
||||
top: 0
|
||||
bottom: 0
|
||||
|
||||
// Animation: slide away
|
||||
transition: padding-left 0.5s, margin-left 0.5s, left 0.5s
|
||||
}
|
||||
|
||||
.sidebar-visible .page-wrapper {
|
||||
left: $sidebar-width
|
||||
}
|
||||
|
||||
.page {
|
||||
outline: 0
|
||||
padding: 0 $page-padding
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative
|
||||
top: 0
|
||||
bottom: 0
|
||||
overflow-y: auto
|
||||
right: 0
|
||||
left: 0
|
||||
padding: 0 15px
|
||||
padding-bottom: 50px
|
||||
|
||||
main {
|
||||
margin-left: auto
|
||||
margin-right: auto
|
||||
max-width: $content-max-width
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
&:hover { text-decoration: underline; }
|
||||
}
|
||||
|
||||
img { max-width: 100%; }
|
||||
}
|
||||
|
||||
.sidebar-visible .content {
|
||||
position: absolute
|
||||
top: 52px
|
||||
}
|
||||
@@ -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,67 +0,0 @@
|
||||
@require 'variables'
|
||||
|
||||
#searchresults a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
mark {
|
||||
border-radius: 2px;
|
||||
padding: 0 3px 1px 3px;
|
||||
margin: 0 -3px -1px -3px;
|
||||
transition: background-color 300ms linear;
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
background-color: rgba(0,0,0,0) !important
|
||||
}
|
||||
|
||||
.searchbar-outer {
|
||||
display: none;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: $content-max-width;
|
||||
}
|
||||
|
||||
#searchbar {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 5px auto 0px auto;
|
||||
padding: 10px 16px;
|
||||
transition: box-shadow 300ms ease-in-out;
|
||||
}
|
||||
|
||||
.searchresults-header {
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
padding: 18px 0 0 5px;
|
||||
}
|
||||
|
||||
.searchresults-outer {
|
||||
display: none;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: $content-max-width;
|
||||
}
|
||||
|
||||
ul#searchresults {
|
||||
list-style: none;
|
||||
padding-left: 20px;
|
||||
|
||||
li {
|
||||
margin: 10px 0px;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
span.teaser {
|
||||
display: block;
|
||||
clear: both;
|
||||
margin: 5px 0 0 20px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
span.teaser em {
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +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
|
||||
overscroll-behavior-y: contain;
|
||||
|
||||
// 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: 0
|
||||
text-decoration: none
|
||||
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) { padding: 5px 0; }
|
||||
&:hover { text-decoration: none }
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 100%
|
||||
height: 3px
|
||||
margin: 5px 0px
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) { margin: 10px 0; }
|
||||
}
|
||||
}
|
||||
|
||||
.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,41 +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%)
|
||||
|
||||
$searchbar-border-color = #848484
|
||||
$searchbar-bg = #424242
|
||||
$searchbar-fg = #fff
|
||||
$searchbar-shadow-color = #d4c89f
|
||||
$searchresults-header-fg = #666
|
||||
$searchresults-border-color = #888
|
||||
$searchresults-li-bg = #252932
|
||||
$search-mark-bg = #e3b171
|
||||
|
||||
@import 'base'
|
||||
@@ -1,222 +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
|
||||
}
|
||||
|
||||
#searchresults a,
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
#searchbar {
|
||||
border: 1px solid $searchbar-border-color;
|
||||
border-radius: 3px;
|
||||
background-color: $searchbar-bg;
|
||||
color: $searchbar-fg
|
||||
|
||||
&:focus, &.active {
|
||||
box-shadow: 0 0 3px $searchbar-shadow-color;
|
||||
}
|
||||
}
|
||||
|
||||
.searchresults-header {
|
||||
color: $searchresults-header-fg;
|
||||
}
|
||||
|
||||
.searchresults-outer {
|
||||
border-bottom: 1px dashed $searchresults-border-color;
|
||||
}
|
||||
|
||||
ul#searchresults li.focus {
|
||||
background-color: $searchresults-li-bg;
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: $search-mark-bg;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +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%)
|
||||
|
||||
$searchbar-border-color = #aaa
|
||||
$searchbar-bg = #b7b7b7
|
||||
$searchbar-fg = #000
|
||||
$searchbar-shadow-color = #aaa
|
||||
$searchresults-header-fg = #666
|
||||
$searchresults-border-color = #98a3ad
|
||||
$searchresults-li-bg = #2b2b2f
|
||||
$search-mark-bg = #355c7d
|
||||
|
||||
@import 'base'
|
||||
@@ -1,5 +0,0 @@
|
||||
@import 'light'
|
||||
@import 'coal'
|
||||
@import 'navy'
|
||||
@import 'rust'
|
||||
@import 'ayu'
|
||||
@@ -1,41 +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%)
|
||||
|
||||
$searchbar-border-color = #aaa
|
||||
$searchbar-bg = #fafafa
|
||||
$searchbar-fg = #000
|
||||
$searchbar-shadow-color = #aaa
|
||||
$searchresults-header-fg = #666
|
||||
$searchresults-border-color = #888
|
||||
$searchresults-li-bg = #e4f2fe
|
||||
$search-mark-bg = #a2cff5
|
||||
|
||||
@import 'base'
|
||||
@@ -1,41 +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%)
|
||||
|
||||
$searchbar-border-color = #aaa
|
||||
$searchbar-bg = #aeaec6
|
||||
$searchbar-fg = #000
|
||||
$searchbar-shadow-color = #aaa
|
||||
$searchresults-header-fg = #5f5f71
|
||||
$searchresults-border-color = #5c5c68
|
||||
$searchresults-li-bg = #242430
|
||||
$search-mark-bg = #a2cff5
|
||||
|
||||
@import 'base'
|
||||
@@ -1,41 +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%)
|
||||
|
||||
$searchbar-border-color = #aaa
|
||||
$searchbar-bg = #fafafa
|
||||
$searchbar-fg = #000
|
||||
$searchbar-shadow-color = #aaa
|
||||
$searchresults-header-fg = #666
|
||||
$searchresults-border-color = #888
|
||||
$searchresults-li-bg = #dec2a2
|
||||
$search-mark-bg = #e69f67
|
||||
|
||||
@import 'base'
|
||||
@@ -1,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
|
||||
@@ -25,16 +25,10 @@ pub fn normalize_path(path: &str) -> 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<()> {
|
||||
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())
|
||||
create_file(&path)?.write_all(content).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Takes a path and returns a path containing just enough `../` to point to
|
||||
|
||||
171
src/utils/mod.rs
171
src/utils/mod.rs
@@ -5,12 +5,13 @@ 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> {
|
||||
@@ -32,10 +33,13 @@ pub fn normalize_id(content: &str) -> String {
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
}).collect::<String>();
|
||||
// Ensure that the first character is [A-Za-z]
|
||||
if ret.chars().next().map_or(false, |c| !c.is_ascii_alphabetic()) {
|
||||
if ret
|
||||
.chars()
|
||||
.next()
|
||||
.map_or(false, |c| !c.is_ascii_alphabetic())
|
||||
{
|
||||
ret.insert(0, 'a');
|
||||
}
|
||||
ret
|
||||
@@ -47,17 +51,19 @@ 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>",
|
||||
"<",
|
||||
">",
|
||||
"&",
|
||||
"'",
|
||||
"""];
|
||||
const REPL_SUB: &[&str] = &[
|
||||
"<em>",
|
||||
"</em>",
|
||||
"<code>",
|
||||
"</code>",
|
||||
"<strong>",
|
||||
"</strong>",
|
||||
"<",
|
||||
">",
|
||||
"&",
|
||||
"'",
|
||||
""",
|
||||
];
|
||||
for sub in REPL_SUB {
|
||||
content = content.replace(sub, "");
|
||||
}
|
||||
@@ -68,6 +74,32 @@ pub fn id_from_content(content: &str) -> String {
|
||||
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 {
|
||||
let mut s = String::with_capacity(text.len() * 3 / 2);
|
||||
@@ -78,8 +110,10 @@ 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));
|
||||
let events = p
|
||||
.map(clean_codeblock_headers)
|
||||
.map(adjust_links)
|
||||
.map(|event| converter.convert(event));
|
||||
|
||||
html::push_html(&mut s, events);
|
||||
s
|
||||
@@ -131,36 +165,35 @@ 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`.
|
||||
@@ -177,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");
|
||||
@@ -275,18 +328,26 @@ more text with spaces
|
||||
|
||||
#[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");
|
||||
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("`--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(""), "");
|
||||
@@ -298,14 +359,18 @@ more text with spaces
|
||||
|
||||
#[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]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user