Compare commits

..

64 Commits

Author SHA1 Message Date
Michael Bryan
b9ff0e8a77 (cargo-release) version 0.1.8 2018-07-09 18:39:35 +08:00
Michael Bryan
0bda57175d Metadata for cargo-release should be in release.toml (#722) 2018-07-09 18:36:55 +08:00
Steve Klabnik
6287e6a44f Merge pull request #715 from fitzgen/continuous-integration-instructions
user guide: Add instructions for running `mdbook` in CI
2018-07-03 10:01:19 -07:00
Corey Farwell
953d3821b6 Merge pull request #701 from kubo39/saturating_sub
Use `saturating_sub` instead of `checked_sub.unwrap_or`
2018-06-27 07:40:20 -04:00
Nick Fitzgerald
488ace15ff user guide: Add instructions for running mdbook in CI
This adds instructions for building and testing books in CI on every PR and
push, as well as instructions for how to automatically deploy to gh-pages on
successful CI runs on `master`.

Fixes #714
2018-06-22 10:31:49 -07:00
Hiroki Noda
289028850f Use saturating_sub instead of checked_sub.unwrap_or 2018-06-04 01:42:09 +09:00
Andrew Gauger
2a55ff62f3 Recursively apply preprocessor (#682) 2018-05-20 18:36:19 +08:00
Mathieu David
6bf86806e4 Merge pull request #691 from phansch/update_regex
Update Regex to 1.0.0
2018-05-18 16:58:01 +02:00
Philipp Hansch
90bd7207ec Add minimum required Rust version to README 2018-05-18 09:04:41 +02:00
Mathieu David
27b29fdaf2 Merge pull request #696 from mattico/fix-theme-dir
Fix default theme dir selection
2018-05-16 21:58:41 +02:00
Matt Ickstadt
154e0fb308 Rustfmt 2018-05-16 12:08:23 -05:00
Matt Ickstadt
0de177a344 Add a warning for possible theme directories which will no longer be used 2018-05-16 12:06:55 -05:00
Mathieu David
f154b2fb65 Merge pull request #697 from mattico/fix-fontawesome-gh-pages
Fix FontAwesome directory missing from Github Pages sites
2018-05-15 23:23:09 +02:00
Matt Ickstadt
d7759fbf4d Remove underscore from FontAwesome directory 2018-05-15 12:34:44 -05:00
Matt Ickstadt
f84e670edd Add a .nojekyll file
to allow users to have other files with leading underscore names.
2018-05-15 12:34:44 -05:00
Matt Ickstadt
9a9c625319 Fix default theme dir selection 2018-05-14 14:52:29 -05:00
Tim Ryan
b9ca108fca Support reproducible builds by forcing window.search to use stable key ordering. (#692) 2018-05-14 18:22:21 +08:00
Philipp Hansch
e99dc51fb3 Update Regex to 1.0.0 2018-05-13 12:35:17 +02:00
Mathieu David
7ee5b6643b Merge pull request #689 from gnzlbg/patch-1
remove removed rustfmt options
2018-05-10 10:31:15 +02:00
gnzlbg
42781bcd6b remove removed rustfmt options
Closes #688
2018-05-09 15:29:46 +02:00
Weihang Lo
41d372de26 Update documentation for preprocessor (#686) 2018-05-06 23:48:11 +08:00
Weihang Lo
69599646e7 Add index preprocessor (#685)
* Add index preprocessor

README.md is a de facto index file in markdown-based documentation.
Hence, we respect to README.md and convert it into index.html.

* Fix warning for unused variables

* Update tests for config

* Match file stem case-insensitively for IndexPreprocessor

* Add tests for IndexPreprocessor

* Update book example to fit index preprocessor
2018-05-04 19:41:28 +08:00
Matt Ickstadt
69fef40e57 Improve print output (#680)
* Update print styles for new sidebar behavior

* Hide copy icons in print output

* Wait for mathjax rendering to complete before printing

* Remove old wrapping css
Browsers this old are already hilariously broken, so we don't need these fallbacks.

* Change mathjax script type
Chrome won't execute this if it's not marked as js

* Ensure page has rendered before printing
In certain situations Chrome willl fire window.onLoad before it's
done rendering. Add a 100ms delay to work around this.
2018-05-01 20:29:34 +08:00
Michael Bryan
a323620e02 (cargo-release) start next development iteration 0.1.8-alpha.0 2018-04-23 07:42:43 +08:00
Michael Bryan
ea0b835b38 (cargo-release) version 0.1.7 2018-04-23 07:41:31 +08:00
Mathieu David
58f0f3b0f2 Merge pull request #672 from mattico/update-deps
Update deps
2018-04-22 22:58:35 +02:00
Matt Ickstadt
e7a61efb39 Fix warning 2018-04-22 13:01:10 -05:00
Matt Ickstadt
d48bc29373 Update dependencies 2018-04-22 13:01:05 -05:00
Michael Bryan
72f154bee4 (cargo-release) start next development iteration 0.1.7-alpha.0 2018-04-22 00:45:07 +08:00
Matt Brubeck
1c71eaa964 Put the search bar into an HTML form (#669)
This enables "Add a keyword for this search" in the contex menu for the
search field, in Firefox and other browsers.
2018-04-21 23:27:51 +08:00
Matt Ickstadt
c195aa990d Update dependencies (#670) 2018-04-21 23:22:05 +08:00
Mathieu David
34bdcaf8b3 Merge pull request #660 from THeK3nger/doc-mathjax-note
Add a note in MathJax documentation
2018-04-16 02:24:49 +02:00
Michael Bryan
41399fc29c Revert "Fixes the search box overlapping with content when first shown (#666)" (#667)
This reverts commit 7f82a197b9.
2018-04-11 10:23:56 +08:00
Michael Bryan
7f82a197b9 Fixes the search box overlapping with content when first shown (#666) 2018-04-10 22:02:27 +08:00
Gwen Lofman
71d44933f0 Replace his with their in reference to reader (#665)
The reader should not be assumed male; I'm a developer and user,
I'm not male.  Makes documentation's language gender neutral to
make it more welcoming to people that do not use he/him pronouns.
2018-04-10 07:02:53 +08:00
Matt Ickstadt
f01bf88e69 Fix several theme issues (#648)
* Don't hide page content when displaying search

* Decrease sidebar animation time

* Fix search key event handler
which wasn't completely de-jqueryified.

* Avoid reflowing page content on small screens
This reduces jank caused by reflowing the page text while animating the
sidebar, and it looks nicer.

* Don't use HTMLParentNode.prepend()
since edge doesn't support it yet

* Don't animate menu border bottom color
since it's the same color as the background, which isn't animated.

* Small CSS improvments
- Remove invalid `pointer: cursor` style
- Disable transitions for noscript to stop page from spazzing on every load
- Add `cursor: pointer` to mark
- Disable `cursor: pointer` on noscript menu-title

* JS fixes

- Load MathJax async
- Always use local fontawesome and clipboard.js
- Move js class to html element to make theme switching easier

* Give the print button a bit more margin
2018-04-09 12:10:44 +08:00
Michael Bryan
b5ea84c60d Remove unnecessary travis jobs (#664)
* Removed all the unnecessary CI jobs

* Updated dependencies

* Removed a deprecation warning
2018-04-07 15:47:08 +08:00
Nils
148c806e34 Prevent search from triggering when editing code (#653) 2018-04-07 06:31:51 +08:00
Davide Aversa
38279deed7 Add a note in MathJax documentation 2018-04-03 10:17:06 +02:00
Bastien Orivel
55f7ed1c37 Replace tempdir by tempfile (#650)
The former has been deprecated in favor of the latter
2018-03-27 07:47:37 +08:00
Anders Rasmussen
eb0f7179ab Use git config to get author name in mdbook init (#649)
* Use `git config` to get author name in `mdbook init`

* Return `None` if `git` command fails

* Use `.ok()?` to convert from Result to Option and return early if `None`
2018-03-26 22:37:11 +08:00
Matt Ickstadt
5fb3675151 Update elasticlunr-rs (#646)
* Update dependencies

* Use config structs from elasticlunr-rs

* Update searchindex fixture
2018-03-20 20:22:35 +08:00
Michael Bryan
77b4f6a940 (cargo-release) start next development iteration 0.1.6-alpha.0 2018-03-16 07:38:05 +08:00
Michael Bryan
6308da699a (cargo-release) version 0.1.5 2018-03-16 07:36:40 +08:00
Guillaume Gomez
62a727c041 Fix search (#645) 2018-03-16 07:37:08 +08:00
Michael Bryan
3bc5d907f4 Merge pull request #641 from rust-lang-nursery/pre-release
Pre release
2018-03-15 09:24:26 +08:00
Michael Bryan
3cd12e7092 (cargo-release) start next development iteration 0.1.5-alpha.0 2018-03-15 09:22:59 +08:00
Michael Bryan
c8bbfd4bc1 (cargo-release) version 0.1.4 2018-03-15 09:21:49 +08:00
Michael Bryan
d48a27f94f Updated the appveyor CI image 2018-03-15 09:19:58 +08:00
Michael Bryan
8c456666ff Added a stability warning to the API docs 2018-03-14 23:48:57 +08:00
Michael Bryan
48b0f547c5 Updated serde dependencies 2018-03-14 23:48:57 +08:00
Michael Bryan
867fbfec05 Updated the call site for handlebars rendering 2018-03-14 23:48:56 +08:00
Michael Bryan
951c873df6 Updated deps 2018-03-14 23:48:56 +08:00
Michael Bryan
4af155e963 Exposed the sections inside a book (#642) 2018-03-14 23:47:17 +08:00
Dylan Maccora
07719a8e0e Adding for content to book.toml on init (#627)
* Obtaining author name from gitconfig

* Writing theme to config on init

* Addressing a FIXME came across

* Add request for book title.
2018-03-14 23:27:56 +08:00
Guillaume Gomez
cc92d665ca Improve css so anchor don't go under sidebar (#638) 2018-03-14 23:23:55 +08:00
Pawel Duzinkiewicz
b86533b2a1 pulldown-cmark updated to 0.1.2, fixmes removed, new cargo.lock generated. (#639) 2018-03-11 22:17:38 +08:00
Matt Ickstadt
b2ad669c61 Search with Elasticlunr, updated (#604)
* Add search with elasticlunr.js

This commit adds search functionality to mdBook, based on work done by @phaiax. The in-browser search code uses elasticlunr.js to execute the search, using an index generated at book build time by elasticlunr-rs.

* Add generator comment
Someone on Reddit was wondering how the rust book was generated and said they checked the source. Thought I'd put this here. Might be a good idea to have a little footer "made with mdBook", but this'll do for now.

* Remove search/editor file override behavior

* Use for loop for book iterator

* Improve HTML regex

* Fix search CORS in file URIs

* Use ammonia to sanitize HTML

* Filter html5ever log messages
2018-03-07 21:02:06 +08:00
Sebastian Thiel
bb043ef660 Add complete preprocessor example (#629)
* First version of preprocessor example, with quicli

It seems it's not worth it right now.

* Remove quicli, just to simplify everything

* Finish de-emphasise example

* Finish preprocessor example in book

* Rename preprocessor type

* Apply changes requested in review

* Update preprocessor docs with latest code

[skip CI]
2018-02-24 18:14:52 +08:00
Sorin Davidoi
82aef1bc3f fix(theme/book): Workaround focusout bug in macOS and iOS (#630) 2018-02-24 17:23:45 +08:00
Dylan Maccora
38c883e1ef Changing clap settings (#624) 2018-02-18 15:10:47 +08:00
Sorin Davidoi
8a00a004d8 Handle some cases when JavaScript is disabled (#614)
* feat(theme/index): Assume the sidebar is initially visible

In case the inline script does not execute, the fallback is to show the sidebar.

* feat(theme/index): Hide sidebar toggle and theme selector buttons when JavaScript is disabled

Makes no sense to show them in this case since they do not work.
2018-02-18 15:05:15 +08:00
Bulat Musin
6af77a7792 Update documentation to reflect addition of clean subcommand. (#607)
* update documentation

Update README.md and User Guide to reflect addition of `clean`
subcommand. Do minor spelling fixes too.

* fix grammar in `clean` documentation
2018-02-18 15:04:04 +08:00
Michael Bryan
b5ca820345 (cargo-release) start next development iteration 0.1.4-alpha.0 2018-02-16 07:47:35 +08:00
88 changed files with 5285 additions and 1023 deletions

4
.gitattributes vendored
View File

@@ -2,3 +2,7 @@
* text=auto eol=lf
*.rs rust
*.woff -text
*.ttf -text
*.otf -text
*.png -text

View File

@@ -1,87 +1,46 @@
# Based on the "trust" template v0.1.1
# https://github.com/japaric/trust/tree/v0.1.1
dist: trusty
language: rust
services: docker
sudo: required
cache: cargo
cache:
- cargo
before_cache:
- chmod -R a+r $HOME/.cargo
- chmod -R a+r $HOME/.cargo
env:
global:
- CRATE_NAME=mdbook
- secure: DPzSRXyfRIVTibv1wOKFeGekXlL8sumGEZxpeq911MpLlrndOKmOo5Ibi3JD8fbUOsE9A/5spj4B2KQNjhbplH+Cp26oEikjuNAA6cA/b2+/TMoC3i0klAYpVopBBV3FFna0gLP+q6t6fzG2v9TJrvmmVav6KVX6ylPNvD/LoReCjrkpgLIQuAQ6dSQNor9uV+EVt4plKhhkiS28DlYdgmTvNb5g4dzOhs8hoWty72J765VYWEDDC8qXn6N9GyrhsC3dhjASGn+1QDSCADYdbG9nrRlb4CZhrfcgOnHhAFva363kshg9HtCphigMgQy2oZXk4nLWK90/HuaPPkVj+N/lpIYjtiHOunToZJfIb0MWzyVI+7+I7WR6n6XbhLCPMe/sPXHHQ3HhQhZZ9xv7CDx9IkYJQBcF3LC+9kzJRi4QT0UTqrxcO3ncgXwvholP8Vg2KKPqFcbuyLPzbvr/o8zIilvLUFAEoDPfTEwSAC4BCzaGkFQVWzhWkgw8Pe1ckOEYFkZ0VLBuCpEiz+x45sbBL1SnnO5xhpjmdc572ZyW7ZmAABw1VfiWhhBWg4WGSf8lLnDHhNA36Qon34pnME/xpJQtWoo7ZZkkzvzYP/oW88/0UIMWDSOYKz7MijXlbNUggwAwUhrLzXDuB71HUKfPreFubfUxbOpu+OtTcOQ=
matrix:
include:
# Android
- env: TARGET=arm-linux-androideabi DISABLE_TESTS=1
# Linux
- env: TARGET=aarch64-unknown-linux-gnu
- env: TARGET=arm-unknown-linux-gnueabi
- env: TARGET=i686-unknown-linux-gnu
- env: TARGET=x86_64-unknown-linux-gnu
- env: TARGET=x86_64-unknown-linux-musl
# Mac
- env: TARGET=i686-apple-darwin
os: osx
- env: TARGET=x86_64-apple-darwin
os: osx
# BSD
- env: TARGET=i686-unknown-freebsd DISABLE_TESTS=1
- env: TARGET=x86_64-unknown-freebsd DISABLE_TESTS=1
- env: TARGET=x86_64-unknown-netbsd DISABLE_TESTS=1
# Other channels
- env: TARGET=x86_64-unknown-linux-gnu
rust: beta
- env: TARGET=x86_64-apple-darwin
os: osx
rust: beta
- env: TARGET=x86_64-unknown-linux-gnu
rust: nightly
- env: TARGET=x86_64-apple-darwin
os: osx
rust: nightly
before_install:
- set -e
- rustup self update
- CRATE_NAME=mdbook
- TARGET=x86_64-unknown-linux-gnu
install:
- sh ci/install.sh
- source ~/.cargo/env || true
- sh ci/install.sh
- export PATH=$PATH:$HOME/.cargo/bin
script:
- bash ci/script.sh
- cargo build --all --no-default-features
- cargo build --verbose
- cargo test --verbose
after_success:
- bash ci/github_pages.sh
- bash ci/github_pages.sh
before_deploy:
- sh ci/before_deploy.sh
- sh ci/before_deploy.sh
deploy:
provider: releases
api_key:
- secure: cURRWBr034iqBz/ifD7uOunBfNR30YxIXfgLX0osWz+iafkVbhDGYYz9sBmRraqO2P7L2koEXMADVb/md1kI2+ykiq/ml+l9zuEAZPVmvSGUN7ZD+7s+lu3l5OBPG5z175T+b2q2q2m8XVR7TW20ra4QbE0bq06KAoOyjSgQVBTSCYsL9uTsGwiVRMEqqJT/BmKhKJNkpGsTKyBSKkOXvfeAAbE260vXUDEN9TYdJ3fvteRrpwLX56ee64gIZUq0RjDc4SKIEqilM6iUtNMvurqaewYNGkiXKRruV6BPCHxEHo6NNT46kOJLBJTf7gZw//dWhSoWpg9P0gdAnPWm407kSa3F7aJ1eRShAFQ4BLyfz9efTqm+jP3fOp7Mm7igSh9w6caSRuOnSsUf5+raRQ8E5Y9HsWGzzpZQk24Fx9EGZ04EeDSdpZAFz+jcbMpHf8t2p4CEx0CCNwYvKx6EydMKbMF5QteQ8SQkXNLhv7Rz2OgtXWYZPRVCMfQfOplsi2InsLCrQxTgwh+6u654SqVSgaHG+IncEAxBrdWy4rHcg7qereUcKfcY3k96vaDxdn/T2c00Ig0aNFR91YnixGMd6J6tQgDcRK9jh6fUm1CCBE9hT+pNUmtgYKuWBoLZexUZFFnfuBed0WciBot1bGDDamndqKq0jJiAzg+GMHk=
- secure: cURRWBr034iqBz/ifD7uOunBfNR30YxIXfgLX0osWz+iafkVbhDGYYz9sBmRraqO2P7L2koEXMADVb/md1kI2+ykiq/ml+l9zuEAZPVmvSGUN7ZD+7s+lu3l5OBPG5z175T+b2q2q2m8XVR7TW20ra4QbE0bq06KAoOyjSgQVBTSCYsL9uTsGwiVRMEqqJT/BmKhKJNkpGsTKyBSKkOXvfeAAbE260vXUDEN9TYdJ3fvteRrpwLX56ee64gIZUq0RjDc4SKIEqilM6iUtNMvurqaewYNGkiXKRruV6BPCHxEHo6NNT46kOJLBJTf7gZw//dWhSoWpg9P0gdAnPWm407kSa3F7aJ1eRShAFQ4BLyfz9efTqm+jP3fOp7Mm7igSh9w6caSRuOnSsUf5+raRQ8E5Y9HsWGzzpZQk24Fx9EGZ04EeDSdpZAFz+jcbMpHf8t2p4CEx0CCNwYvKx6EydMKbMF5QteQ8SQkXNLhv7Rz2OgtXWYZPRVCMfQfOplsi2InsLCrQxTgwh+6u654SqVSgaHG+IncEAxBrdWy4rHcg7qereUcKfcY3k96vaDxdn/T2c00Ig0aNFR91YnixGMd6J6tQgDcRK9jh6fUm1CCBE9hT+pNUmtgYKuWBoLZexUZFFnfuBed0WciBot1bGDDamndqKq0jJiAzg+GMHk=
file_glob: true
file: "$CRATE_NAME-$TRAVIS_TAG-$TARGET.*"
on:
condition: "$TRAVIS_RUST_VERSION = stable"
tags: true
provider: releases
skip_cleanup: true
branches:
only:
- "/^v\\d+\\.\\d+\\.\\d+.*$/"
- master
- "/^v\\d+\\.\\d+\\.\\d+.*$/"
- master
notifications:
email:

641
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
[package]
name = "mdbook"
version = "0.1.3"
version = "0.1.8"
authors = ["Mathieu David <mathieudavid@mathieudavid.org>", "Michael-F-Bryan <michaelfbryan@gmail.com>"]
description = "create books from markdown files (like Gitbook)"
description = "Create books from markdown files"
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
repository = "https://github.com/rust-lang-nursery/mdBook"
keywords = ["book", "gitbook", "rustbook", "markdown"]
@@ -14,28 +14,23 @@ exclude = [
"src/theme/stylus/**",
]
[package.metadata.release]
sign-commit = true
push-remote = "upstream"
tag-prefix = "v"
[dependencies]
clap = "2.24"
chrono = "0.4"
handlebars = "0.29"
handlebars = "0.32"
serde = "1.0"
serde_derive = "1.0"
error-chain = "0.11"
serde_json = "1.0"
pulldown-cmark = "0.1"
pulldown-cmark = "0.1.2"
lazy_static = "1.0"
log = "0.4"
env_logger = "0.5.0-rc.1"
toml = "0.4"
memchr = "2.0"
open = "1.1"
regex = "0.2.1"
tempdir = "0.3.4"
regex = "1.0.0"
tempfile = "3.0"
itertools = "0.7"
shlex = "0.1"
toml-query = "0.6"
@@ -46,25 +41,31 @@ time = { version = "0.1.34", optional = true }
crossbeam = { version = "0.3", optional = true }
# Serve feature
iron = { version = "0.5", optional = true }
staticfile = { version = "0.4", optional = true }
iron = { version = "0.6", optional = true }
staticfile = { version = "0.5", optional = true }
ws = { version = "0.7", optional = true}
# Search feature
elasticlunr-rs = { version = "2.2", 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.4"
pretty_assertions = "0.5"
walkdir = "2.0"
pulldown-cmark-to-cmark = "1.1.0"
[features]
default = ["output", "watch", "serve"]
default = ["output", "watch", "serve", "search"]
debug = []
output = []
regenerate-css = []
watch = ["notify", "time", "crossbeam"]
serve = ["iron", "staticfile", "ws"]
search = ["elasticlunr-rs", "ammonia"]
[[bin]]
doc = false

View File

@@ -10,7 +10,7 @@
<tr>
<td><strong>Windows</strong></td>
<td>
<a href="https://ci.appveyor.com/project/azerupi/mdbook/"><img src="https://ci.appveyor.com/api/projects/status/o38racsnbcospyc8/branch/master?svg=true"></a>
<a href="https://ci.appveyor.com/project/rust-lang-libs/mdbook"><img src="https://ci.appveyor.com/api/projects/status/ysyke2rvo85sni55?svg=true"></a>
</td>
</tr>
<tr>
@@ -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:
```
@@ -141,6 +141,10 @@ explanation, check out the [User Guide].
`http://localhost:3000` (port is changeable) and reloads the browser when a
change occurs.
- `mdbook clean`
Delete directory in which generated book is located.
### As a library

View File

@@ -8,3 +8,12 @@ mathjax-support = true
[output.html.playpen]
editable = true
[output.html.search]
limit-results = 20
use-boolean-and = true
boost-title = 2
boost-hierarchy = 2
boost-paragraph = 1
expand = true
heading-split-level = 2

View File

@@ -1,23 +1,27 @@
# Summary
- [mdBook](README.md)
- [Command Line Tool](cli/cli-tool.md)
- [Command Line Tool](cli/README.md)
- [init](cli/init.md)
- [build](cli/build.md)
- [watch](cli/watch.md)
- [serve](cli/serve.md)
- [test](cli/test.md)
- [Format](format/format.md)
- [clean](cli/clean.md)
- [Format](format/README.md)
- [SUMMARY.md](format/summary.md)
- [Configuration](format/config.md)
- [Theme](format/theme/theme.md)
- [Theme](format/theme/README.md)
- [index.hbs](format/theme/index-hbs.md)
- [Syntax highlighting](format/theme/syntax-highlighting.md)
- [Editor](format/theme/editor.md)
- [MathJax Support](format/mathjax.md)
- [mdBook specific features](format/mdbook.md)
- [For Developers](for_developers/index.md)
- [Continuous Integration](continuous-integration.md)
- [For Developers](for_developers/README.md)
- [Preprocessors](for_developers/preprocessors.md)
- [Alternate Backends](for_developers/backends.md)
-----------
[Contributors](misc/contributors.md)

View File

@@ -14,8 +14,8 @@ convenience. Large books will therefore remain structured when rendered.
#### Specify a directory
Like `init`, the `build` command can take a directory as argument to use instead of the
current working directory.
Like `init`, the `build` command can take a directory as an argument to use
instead of the current working directory.
```bash
mdbook build path/to/book

View File

@@ -0,0 +1,21 @@
# The clean command
The clean command is used to delete the generated book and any other build
artifacts.
```bash
mdbook clean
```
It will try to delete the built book. If a path is provided, it will be used.
#### Specify a directory
Like `init`, the `clean` command can take a directory as an argument to use
instead of the normal build directory.
```bash
mdbook clean --dest-dir=path/to/book
```
`path/to/book` could be absolute or relative.

View File

@@ -6,8 +6,8 @@ This preferred by many for writing books with mdbook because it allows for you t
#### Specify a directory
Like `watch`, `serve` can take a directory as argument to use instead of the
current working directory.
Like `watch`, `serve` can take a directory as an argument to use instead of
the current working directory.
```bash
mdbook serve path/to/book

View File

@@ -5,8 +5,8 @@ You could repeatedly issue `mdbook build` every time a file is changed. But usin
#### Specify a directory
Like `init` and `build`, `watch` can take a directory as argument to use instead of the
current working directory.
Like `init` and `build`, `watch` can take a directory as an argument to use
instead of the current working directory.
```bash
mdbook watch path/to/book

View File

@@ -0,0 +1,55 @@
# 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 oauth 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, add this snippet to your `.travis.yml`:
```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!

View File

@@ -5,13 +5,13 @@ book is loaded and before it gets rendered, allowing you to update and mutate
the book. Possible use cases are:
- Creating custom helpers like `\{{#include /path/to/file.md}}`
- Updating links so `[some chapter](some_chapter.md)` is automatically changed
- Updating links so `[some chapter](some_chapter.md)` is automatically changed
to `[some chapter](some_chapter.html)` for the HTML renderer
- Substituting in latex-style expressions (`$$ \frac{1}{3} $$`) with their
- Substituting in latex-style expressions (`$$ \frac{1}{3} $$`) with their
mathjax equivalents
## Implementing a Preprocessor
## Implementing a Preprocessor
A preprocessor is represented by the `Preprocessor` trait.
@@ -29,4 +29,68 @@ pub struct PreprocessorContext {
pub root: PathBuf,
pub config: Config,
}
```
```
## A complete Example
The magic happens within the `run(...)` method of the [`Preprocessor`][preprocessor-docs] trait implementation.
As direct access to the chapters is not possible, you will probably end up iterating
them using `for_each_mut(...)`:
```rust
book.for_each_mut(|item: &mut BookItem| {
if let BookItem::Chapter(ref mut chapter) = *item {
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
res = Some(
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
Ok(md) => {
chapter.content = md;
Ok(())
}
Err(err) => Err(err),
},
);
}
});
```
The `chapter.content` is just a markdown formatted string, and you will have to
process it in some way. Even though it's entirely possible to implement some sort of
manual find & replace operation, if that feels too unsafe you can use [`pulldown-cmark`][pc]
to parse the string into events and work on them instead.
Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events back to
a string.
The following code block shows how to remove all emphasis from markdown, and do so
safely.
```rust
fn remove_emphasis(num_removed_items: &mut i32, chapter: &mut Chapter) -> Result<String> {
let mut buf = String::with_capacity(chapter.content.len());
let events = Parser::new(&chapter.content).filter(|e| {
let should_keep = match *e {
Event::Start(Tag::Emphasis)
| Event::Start(Tag::Strong)
| Event::End(Tag::Emphasis)
| Event::End(Tag::Strong) => false,
_ => true,
};
if !should_keep {
*num_removed_items += 1;
}
should_keep
});
cmark(events, &mut buf, None)
.map(|_| buf)
.map_err(|err| Error::from(format!("Markdown serialization failed: {}", err)))
}
```
For everything else, have a look [at the complete example][example].
[preprocessor-docs]: https://docs.rs/mdbook/0.1.3/mdbook/preprocess/trait.Preprocessor.html
[pc]: https://crates.io/crates/pulldown-cmark
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs

View File

@@ -16,6 +16,9 @@ create-missing = false
[output.html]
additional-css = ["custom.css"]
[output.html.search]
limit-results = 15
```
## Supported configuration options
@@ -55,12 +58,21 @@ This controls the build process of your book.
will be created when the book is built (i.e. `create-missing = true`). If this
is `false` then the build process will instead exit with an error if any files
do not exist.
- **preprocess:** Specify which preprocessors to be applied. Default is `["links", "index"]`. To disable default preprocessors, pass an empty array `[]` in.
The following preprocessors are available and included by default:
- `links`: Expand the `{{# playpen}}` and `{{# include}}` handlebars helpers in a chapter.
- `index`: Convert all chapter files named `README.md` into `index.md`. That is to say, all `README.md` would be rendered to an index file `index.html` in the rendered book.
**book.toml**
```toml
[build]
build-dir = "build"
create-missing = false
preprocess = ["links", "index"]
```
### HTML renderer options
@@ -81,20 +93,59 @@ The following configuration options are available:
stylesheets that will be loaded after the default ones where you can
surgically change the style.
- **additional-js:** If you need to add some behaviour to your book without
removing the current behaviour, you can specify a set of javascript files
removing the current behaviour, you can specify a set of JavaScript files
that will be loaded alongside the default one.
- **playpen:** A subtable for configuring various playpen settings.
- **no-section-label**: mdBook by defaults adds section label in table of
- **no-section-label:** mdBook by defaults adds section label in table of
contents column. For example, "1.", "2.1". Set this option to true to
disable those labels. Defaults to `false`.
- **playpen:** A subtable for configuring various playpen settings.
- **search:** A subtable for configuring the in-browser search
functionality. mdBook must be compiled with the `search` feature enabled
(on by default).
**book.toml**
Available configuration options for the `[output.html.playpen]` table:
- **editable:** Allow editing the source code. Defaults to `false`.
- **copy-js:** Copy JavaScript files for the editor to the output directory.
Defaults to `true`.
[Ace]: https://ace.c9.io/
Available configuration options for the `[output.html.search]` table:
- **limit-results:** The maximum number of search results. Defaults to `30`.
- **teaser-word-count:** The number of words used for a search result teaser.
Defaults to `30`.
- **use-boolean-and:** Define the logical link between multiple search words.
If true, all search words must appear in each result. Defaults to `true`.
- **boost-title:** Boost factor for the search result score if a search word
appears in the header. Defaults to `2`.
- **boost-hierarchy:** Boost factor for the search result score if a search
word appears in the hierarchy. The hierarchy contains all titles of the
parent documents and all parent headings. Defaults to `1`.
- **boost-paragraph:** Boost factor for the search result score if a search
word appears in the text. Defaults to `1`.
- **expand:** True if search should match longer results e.g. search `micro`
should match `microwave`. Defaults to `true`.
- **heading-split-level:** Search results will link to a section of the document
which contains the result. Documents are split into sections by headings
this level or less.
Defaults to `3`. (`### This is a level 3 heading`)
- **copy-js:** Copy JavaScript files for the search implementation to the
output directory. Defaults to `true`.
This shows all available options in the **book.toml**:
```toml
[book]
title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
[build]
build-dir = "book"
create-missing = true
preprocess = ["links", "index"]
[output.html]
theme = "my-theme"
curly-quotes = true
@@ -105,6 +156,18 @@ additional-js = ["custom.js"]
[output.html.playpen]
editor = "./path/to/editor"
editable = false
[output.html.search]
enable = true
searcher = "./path/to/searcher"
limit-results = 30
teaser-word-count = 30
use-boolean-and = true
boost-title = 2
boost-hierarchy = 1
boost-paragraph = 1
expand = true
heading-split-level = 3
```
@@ -145,4 +208,4 @@ override the book's title without needing to touch your `book.toml`.
The latter case may be useful in situations where `mdbook` is invoked
from a script or CI, where it sometimes isn't possible to update the
`book.toml` before building.
`book.toml` before building.

View File

@@ -9,9 +9,13 @@ To enable MathJax, you need to add the `mathjax-support` key to your `book.toml`
mathjax-support = true
```
>**Note:**
>**Note:**
The usual delimiters MathJax uses are not yet supported. You can't currently use `$$ ... $$` as delimiters and the `\[ ... \]` delimiters need an extra backslash to work. Hopefully this limitation will be lifted soon.
>**Note:**
> When you use double backslashes in MathJax blocks (for example in commands such as `\begin{cases} \frac 1 2 \\ \frac 3 4 \end{cases}`) you need to add _two extra_ backslashes (e.g., `\begin{cases} \frac 1 2 \\\\ \frac 3 4 \end{cases}`).
### Inline equations
Inline equations are delimited by `\\(` and `\\)`. So for example, to render the following inline equation \\( \int x dx = \frac{x^2}{2} + C \\) you would write the following:
```

View File

@@ -14,3 +14,6 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add
- [Michael-F-Bryan](https://github.com/Michael-F-Bryan)
- [Chris Spiegel](https://github.com/cspiegel)
- [projektir](https://github.com/projektir)
- [Phaiax](https://github.com/Phaiax)
- [Matt Ickstadt](https://github.com/mattico)
- Weihang Lo ([@weihanglo](https://github.com/weihanglo))

View File

@@ -15,11 +15,9 @@ main() {
;;
esac
test -f Cargo.lock || cargo generate-lockfile
cargo rustc --bin mdbook --target $TARGET --release -- -C lto
cross rustc --bin mdbook --target $TARGET --release -- -C lto
cp target/$TARGET/release/mdbook $stage/
cp target/release/mdbook $stage/
cd $stage
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
@@ -28,4 +26,4 @@ main() {
rm -rf $stage
}
main
main

View File

@@ -1,20 +0,0 @@
# This script takes care of testing your crate
set -ex
main() {
cross build --target $TARGET --all
cross build --target $TARGET --all --release
if [ ! -z $DISABLE_TESTS ]; then
return
fi
cross test --target $TARGET
cross test --target $TARGET --release
}
# we don't run the "test phase" when doing deploys
if [ -z $TRAVIS_TAG ]; then
main
fi

94
examples/de-emphasize.rs Normal file
View File

@@ -0,0 +1,94 @@
//! This program removes all forms of emphasis from the markdown of the book.
extern crate mdbook;
extern crate pulldown_cmark;
extern crate pulldown_cmark_to_cmark;
use mdbook::errors::{Error, Result};
use mdbook::MDBook;
use mdbook::book::{Book, BookItem, Chapter};
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use pulldown_cmark::{Event, Parser, Tag};
use pulldown_cmark_to_cmark::fmt::cmark;
use std::ffi::OsString;
use std::env::{args, args_os};
use std::process;
struct Deemphasize;
impl Preprocessor for Deemphasize {
fn name(&self) -> &str {
"md-links-to-html-links"
}
fn run(&self, _ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
eprintln!("Running '{}' preprocessor", self.name());
let mut res: Option<_> = None;
let mut num_removed_items = 0;
book.for_each_mut(|item: &mut BookItem| {
if let Some(Err(_)) = res {
return;
}
if let BookItem::Chapter(ref mut chapter) = *item {
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
res = Some(
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
Ok(md) => {
chapter.content = md;
Ok(())
}
Err(err) => Err(err),
},
);
}
});
eprintln!(
"{}: removed {} events from markdown stream.",
self.name(),
num_removed_items
);
match res {
Some(res) => res,
None => Ok(()),
}
}
}
fn do_it(book: OsString) -> Result<()> {
let mut book = MDBook::load(book)?;
book.with_preprecessor(Deemphasize);
book.build()
}
fn main() {
if args_os().count() != 2 {
eprintln!("USAGE: {} <book>", args().next().expect("executable"));
return;
}
if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) {
eprintln!("{}", e);
process::exit(1);
}
}
impl Deemphasize {
fn remove_emphasis(num_removed_items: &mut i32, chapter: &mut Chapter) -> Result<String> {
let mut buf = String::with_capacity(chapter.content.len());
let events = Parser::new(&chapter.content).filter(|e| {
let should_keep = match *e {
Event::Start(Tag::Emphasis)
| Event::Start(Tag::Strong)
| Event::End(Tag::Emphasis)
| Event::End(Tag::Strong) => false,
_ => true,
};
if !should_keep {
*num_removed_items += 1;
}
should_keep
});
cmark(events, &mut buf, None)
.map(|_| buf)
.map_err(|err| Error::from(format!("Markdown serialization failed: {}", err)))
}
}

3
release.toml Normal file
View File

@@ -0,0 +1,3 @@
sign-commit = true
push-remote = "origin"
tag-prefix = "v"

View File

@@ -1,7 +1 @@
array_layout = "Visual"
chain_indent = "Visual"
fn_args_layout = "Visual"
fn_call_style = "Visual"
format_strings = true
generics_indent = "Visual"

View File

@@ -1,8 +1,10 @@
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::config;
use get_book_dir;
// Create clap subcommand arguments
@@ -20,9 +22,11 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args);
let mut builder = MDBook::init(&book_dir);
let mut config = config::Config::default();
// If flag `--theme` is present, copy theme to src
if args.is_present("theme") {
config.set("output.html.theme", "src/theme")?;
// Skip this if `--force` is present
if !args.is_present("force") {
// Print warning
@@ -38,6 +42,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
if confirm() {
builder.copy_theme(true);
}
} else {
builder.copy_theme(true);
}
}
@@ -47,13 +53,49 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
builder.create_gitignore(true);
}
config.book.title = request_book_title();
if let Some(author) = get_author_name() {
debug!("Obtained user name from gitconfig: {:?}", author);
config.book.authors.push(author);
builder.with_config(config);
}
builder.build()?;
println!("\nAll done, no errors...");
Ok(())
}
// Simple function that user comfirmation
/// Obtains author name from git config file by running the `git config` command.
fn get_author_name() -> Option<String> {
let output = Command::new("git")
.args(&["config", "--get", "user.name"])
.output()
.ok()?;
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).trim().to_owned())
} else {
None
}
}
/// Request book title from user and return if provided.
fn request_book_title() -> Option<String> {
println!("What title would you like to give the book? ");
io::stdout().flush().unwrap();
let mut resp = String::new();
io::stdin().read_line(&mut resp).unwrap();
let resp = resp.trim();
if resp.is_empty() {
None
} else {
Some(resp.into())
}
}
// Simple function for user confirmation
fn confirm() -> bool {
io::stdout().flush().unwrap();
let mut s = String::new();

View File

@@ -1,6 +1,6 @@
extern crate chrono;
#[macro_use]
extern crate clap;
extern crate chrono;
extern crate env_logger;
extern crate error_chain;
#[macro_use]
@@ -38,7 +38,7 @@ fn main() {
.author("Mathieu David <mathieudavid@mathieudavid.org>")
// Get the version from our Cargo.toml using clap's crate_version!() macro
.version(concat!("v",crate_version!()))
.setting(AppSettings::SubcommandRequired)
.setting(AppSettings::ArgRequiredElseHelp)
.after_help("For more information about a specific command, \
try `mdbook <command> --help`\n\
Source code for mdbook available \
@@ -77,11 +77,14 @@ fn init_logger() {
let mut builder = Builder::new();
builder.format(|formatter, record| {
writeln!(formatter, "{} [{}] ({}): {}",
Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
record.target(),
record.args())
writeln!(
formatter,
"{} [{}] ({}): {}",
Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
record.target(),
record.args()
)
});
if let Ok(var) = env::var("RUST_LOG") {
@@ -89,6 +92,8 @@ fn init_logger() {
} else {
// if no RUST_LOG provided, default to logging at the Info level
builder.filter(None, LevelFilter::Info);
// Filter extraneous html5ever not-implemented messages
builder.filter(Some("html5ever"), LevelFilter::Error);
}
builder.init();

View File

@@ -71,7 +71,8 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Book {
/// The sections in this book.
sections: Vec<BookItem>,
pub sections: Vec<BookItem>,
__non_exhaustive: (),
}
impl Book {
@@ -152,15 +153,23 @@ pub struct Chapter {
pub sub_items: Vec<BookItem>,
/// The chapter's location, relative to the `SUMMARY.md` file.
pub path: PathBuf,
/// An ordered list of the names of each chapter above this one, in the hierarchy.
pub parent_names: Vec<String>,
}
impl Chapter {
/// Create a new chapter with the provided content.
pub fn new<P: Into<PathBuf>>(name: &str, content: String, path: P) -> Chapter {
pub fn new<P: Into<PathBuf>>(
name: &str,
content: String,
path: P,
parent_names: Vec<String>,
) -> Chapter {
Chapter {
name: name.to_string(),
content: content,
path: path.into(),
parent_names: parent_names,
..Default::default()
}
}
@@ -183,21 +192,34 @@ fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<
let mut chapters = Vec::new();
for summary_item in summary_items {
let chapter = load_summary_item(summary_item, src_dir)?;
let chapter = load_summary_item(summary_item, src_dir, Vec::new())?;
chapters.push(chapter);
}
Ok(Book { sections: chapters })
Ok(Book {
sections: chapters,
__non_exhaustive: (),
})
}
fn load_summary_item<P: AsRef<Path>>(item: &SummaryItem, src_dir: P) -> Result<BookItem> {
fn load_summary_item<P: AsRef<Path>>(
item: &SummaryItem,
src_dir: P,
parent_names: Vec<String>,
) -> Result<BookItem> {
match *item {
SummaryItem::Separator => Ok(BookItem::Separator),
SummaryItem::Link(ref link) => load_chapter(link, src_dir).map(|c| BookItem::Chapter(c)),
SummaryItem::Link(ref link) => {
load_chapter(link, src_dir, parent_names).map(|c| BookItem::Chapter(c))
}
}
}
fn load_chapter<P: AsRef<Path>>(link: &Link, src_dir: P) -> Result<Chapter> {
fn load_chapter<P: AsRef<Path>>(
link: &Link,
src_dir: P,
parent_names: Vec<String>,
) -> Result<Chapter> {
debug!("Loading {} ({})", link.name, link.location.display());
let src_dir = src_dir.as_ref();
@@ -218,12 +240,14 @@ fn load_chapter<P: AsRef<Path>>(link: &Link, src_dir: P) -> Result<Chapter> {
.strip_prefix(&src_dir)
.expect("Chapters are always inside a book");
let mut ch = Chapter::new(&link.name, content, stripped);
let mut sub_item_parents = parent_names.clone();
let mut ch = Chapter::new(&link.name, content, stripped, parent_names);
ch.number = link.number.clone();
sub_item_parents.push(link.name.clone());
let sub_items = link.nested_items
.iter()
.map(|i| load_summary_item(i, src_dir))
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
.collect::<Result<Vec<_>>>()?;
ch.sub_items = sub_items;
@@ -273,7 +297,7 @@ impl Display for Chapter {
#[cfg(test)]
mod tests {
use super::*;
use tempdir::TempDir;
use tempfile::{TempDir, Builder as TempFileBuilder};
use std::io::Write;
const DUMMY_SRC: &'static str = "
@@ -287,7 +311,7 @@ And here is some \
/// Create a dummy `Link` in a temporary directory.
fn dummy_link() -> (Link, TempDir) {
let temp = TempDir::new("book").unwrap();
let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let chapter_path = temp.path().join("chapter_1.md");
File::create(&chapter_path)
@@ -324,9 +348,14 @@ And here is some \
#[test]
fn load_a_single_chapter_from_disk() {
let (link, temp_dir) = dummy_link();
let should_be = Chapter::new("Chapter 1", DUMMY_SRC.to_string(), "chapter_1.md");
let should_be = Chapter::new(
"Chapter 1",
DUMMY_SRC.to_string(),
"chapter_1.md",
Vec::new(),
);
let got = load_chapter(&link, temp_dir.path()).unwrap();
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
@@ -334,7 +363,7 @@ And here is some \
fn cant_load_a_nonexistent_chapter() {
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
let got = load_chapter(&link, "");
let got = load_chapter(&link, "", Vec::new());
assert!(got.is_err());
}
@@ -347,6 +376,7 @@ And here is some \
content: String::from("Hello World!"),
number: Some(SectionNumber(vec![1, 2])),
path: PathBuf::from("second.md"),
parent_names: vec![String::from("Chapter 1")],
sub_items: Vec::new(),
};
let should_be = BookItem::Chapter(Chapter {
@@ -354,6 +384,7 @@ And here is some \
content: String::from(DUMMY_SRC),
number: None,
path: PathBuf::from("chapter_1.md"),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(nested.clone()),
BookItem::Separator,
@@ -361,7 +392,7 @@ And here is some \
],
});
let got = load_summary_item(&SummaryItem::Link(root), temp.path()).unwrap();
let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
@@ -381,6 +412,7 @@ And here is some \
..Default::default()
}),
],
..Default::default()
};
let got = load_book_from_disk(&summary, temp.path()).unwrap();
@@ -399,6 +431,7 @@ And here is some \
}),
BookItem::Separator,
],
..Default::default()
};
let should_be: Vec<_> = book.sections.iter().collect();
@@ -417,22 +450,26 @@ And here is some \
content: String::from(DUMMY_SRC),
number: None,
path: PathBuf::from("Chapter_1/index.md"),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(Chapter::new(
"Hello World",
String::new(),
"Chapter_1/hello.md",
Vec::new(),
)),
BookItem::Separator,
BookItem::Chapter(Chapter::new(
"Goodbye World",
String::new(),
"Chapter_1/goodbye.md",
Vec::new(),
)),
],
}),
BookItem::Separator,
],
..Default::default()
};
let got: Vec<_> = book.iter().collect();
@@ -464,22 +501,26 @@ And here is some \
content: String::from(DUMMY_SRC),
number: None,
path: PathBuf::from("Chapter_1/index.md"),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(Chapter::new(
"Hello World",
String::new(),
"Chapter_1/hello.md",
Vec::new(),
)),
BookItem::Separator,
BookItem::Chapter(Chapter::new(
"Goodbye World",
String::new(),
"Chapter_1/goodbye.md",
Vec::new(),
)),
],
}),
BookItem::Separator,
],
..Default::default()
};
let num_items = book.iter().count();

View File

@@ -16,12 +16,17 @@ pub use self::init::BookBuilder;
use std::path::PathBuf;
use std::io::Write;
use std::process::Command;
use tempdir::TempDir;
use tempfile::Builder as TempFileBuilder;
use toml::Value;
use utils;
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
use preprocess::{LinkPreprocessor, Preprocessor, PreprocessorContext};
use preprocess::{
LinkPreprocessor,
IndexPreprocessor,
Preprocessor,
PreprocessorContext
};
use errors::*;
use config::Config;
@@ -213,11 +218,12 @@ impl MDBook {
.flat_map(|x| vec![x.0, x.1])
.collect();
let temp_dir = TempDir::new("mdbook")?;
let temp_dir = TempFileBuilder::new().prefix("mdbook").tempdir()?;
let preprocess_context = PreprocessorContext::new(self.root.clone(), self.config.clone());
LinkPreprocessor::new().run(&preprocess_context, &mut self.book)?;
IndexPreprocessor::new().run(&preprocess_context, &mut self.book)?;
for item in self.iter() {
if let BookItem::Chapter(ref ch) = *item {
@@ -288,13 +294,12 @@ impl MDBook {
self.root.join(&self.config.book.src)
}
// FIXME: This really belongs as part of the `HtmlConfig`.
#[doc(hidden)]
/// Get the directory containing the theme resources for the book.
pub fn theme_dir(&self) -> PathBuf {
match self.config.html_config().and_then(|h| h.theme) {
Some(d) => self.root.join(d),
None => self.root.join("theme"),
}
self.config
.html_config()
.unwrap_or_default()
.theme_dir(&self.root)
}
}
@@ -323,15 +328,19 @@ fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
}
fn default_preprocessors() -> Vec<Box<Preprocessor>> {
vec![Box::new(LinkPreprocessor::new())]
vec![
Box::new(LinkPreprocessor::new()),
Box::new(IndexPreprocessor::new()),
]
}
/// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
let preprocess_list = match config.build.preprocess {
Some(ref p) => p,
// If no preprocessor field is set, default to the LinkPreprocessor. This allows you
// to disable the LinkPreprocessor by setting "preprocess" to an empty list.
// If no preprocessor field is set, default to the LinkPreprocessor and
// IndexPreprocessor. This allows you to disable default preprocessors
// by setting "preprocess" to an empty list.
None => return Ok(default_preprocessors()),
};
@@ -340,6 +349,7 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
for key in preprocess_list {
match key.as_ref() {
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
_ => bail!("{:?} is not a recognised preprocessor", key),
}
}
@@ -404,7 +414,7 @@ mod tests {
}
#[test]
fn config_defaults_to_link_preprocessor_if_not_set() {
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
let cfg = Config::default();
// make sure we haven't got anything in the `output` table
@@ -413,8 +423,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]

View File

@@ -3,7 +3,7 @@ use std::iter::FromIterator;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use memchr::{self, Memchr};
use pulldown_cmark::{self, Alignment, Event, Tag};
use pulldown_cmark::{self, Event, Tag};
use errors::*;
/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be
@@ -313,24 +313,15 @@ impl<'a> SummaryParser<'a> {
break;
}
Some(Event::Start(other_tag)) => {
// FIXME: Remove this when google/pulldown_cmark#120 lands (new patch release)
// replace with `other_tag == Tag::Rule`
if tag_eq(&other_tag, &Tag::Rule) {
if other_tag == Tag::Rule {
items.push(SummaryItem::Separator);
}
trace!("Skipping contents of {:?}", other_tag);
// Skip over the contents of this tag
while let Some(event) = self.next_event() {
// FIXME: Remove this when google/pulldown_cmark#120 lands (new patch release)
// and replace the nested if-let with:
// if next == Event::End(other_tag.clone()) {
// break;
// }
if let Event::End(tag) = event {
if tag_eq(&tag, &other_tag) {
break;
}
if event == Event::End(other_tag.clone()) {
break;
}
}
@@ -484,43 +475,6 @@ fn stringify_events(events: Vec<Event>) -> String {
.collect()
}
// FIXME: Remove this when google/pulldown_cmark#120 lands (new patch release)
fn tag_eq(left: &Tag, right: &Tag) -> bool {
match (left, right) {
(&Tag::Paragraph, &Tag::Paragraph) => true,
(&Tag::Rule, &Tag::Rule) => true,
(&Tag::Header(a), &Tag::Header(b)) => a == b,
(&Tag::BlockQuote, &Tag::BlockQuote) => true,
(&Tag::CodeBlock(ref a), &Tag::CodeBlock(ref b)) => a == b,
(&Tag::List(ref a), &Tag::List(ref b)) => a == b,
(&Tag::Item, &Tag::Item) => true,
(&Tag::FootnoteDefinition(ref a), &Tag::FootnoteDefinition(ref b)) => a == b,
(&Tag::Table(ref a), &Tag::Table(ref b)) => {
a.iter().zip(b.iter()).all(|(l, r)| alignment_eq(*l, *r))
}
(&Tag::TableHead, &Tag::TableHead) => true,
(&Tag::TableRow, &Tag::TableRow) => true,
(&Tag::TableCell, &Tag::TableCell) => true,
(&Tag::Emphasis, &Tag::Emphasis) => true,
(&Tag::Strong, &Tag::Strong) => true,
(&Tag::Code, &Tag::Code) => true,
(&Tag::Link(ref a_1, ref a_2), &Tag::Link(ref b_1, ref b_2)) => a_1 == b_1 && a_2 == b_2,
(&Tag::Image(ref a_1, ref a_2), &Tag::Image(ref b_1, ref b_2)) => a_1 == b_1 && a_2 == b_2,
_ => false,
}
}
// FIXME: Remove this when google/pulldown_cmark#120 lands (new patch release)
fn alignment_eq(left: Alignment, right: Alignment) -> bool {
match (left, right) {
(Alignment::None, Alignment::None) => true,
(Alignment::Left, Alignment::Left) => true,
(Alignment::Center, Alignment::Center) => true,
(Alignment::Right, Alignment::Right) => true,
_ => false,
}
}
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
/// a pretty `Display` impl.
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]

View File

@@ -1,13 +1,13 @@
//! Mdbook's configuration system.
//!
//!
//! The main entrypoint of the `config` module is the `Config` struct. This acts
//! essentially as a bag of configuration information, with a couple
//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support
//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support
//! for arbitrary data which is exposed to plugins and alternate backends.
//!
//!
//!
//!
//! # Examples
//!
//!
//! ```rust
//! # extern crate mdbook;
//! # use mdbook::errors::*;
@@ -15,31 +15,31 @@
//! use std::path::PathBuf;
//! use mdbook::Config;
//! use toml::Value;
//!
//!
//! # fn run() -> Result<()> {
//! let src = r#"
//! [book]
//! title = "My Book"
//! authors = ["Michael-F-Bryan"]
//!
//!
//! [build]
//! src = "out"
//!
//!
//! [other-table.foo]
//! bar = 123
//! "#;
//!
//!
//! // load the `Config` from a toml string
//! let mut cfg = Config::from_str(src)?;
//!
//!
//! // retrieve a nested value
//! let bar = cfg.get("other-table.foo.bar").cloned();
//! assert_eq!(bar, Some(Value::Integer(123)));
//!
//!
//! // Set the `output.html.theme` directory
//! assert!(cfg.get("output.html").is_none());
//! cfg.set("output.html.theme", "./themes");
//!
//!
//! // then load it again, automatically deserializing to a `PathBuf`.
//! let got: PathBuf = cfg.get_deserialized("output.html.theme")?;
//! assert_eq!(got, PathBuf::from("./themes"));
@@ -383,7 +383,6 @@ pub struct BuildConfig {
pub create_missing: bool,
/// Which preprocessors should be applied
pub preprocess: Option<Vec<String>>,
}
impl Default for BuildConfig {
@@ -410,7 +409,7 @@ pub struct HtmlConfig {
pub google_analytics: Option<String>,
/// Additional CSS stylesheets to include in the rendered page's `<head>`.
pub additional_css: Vec<PathBuf>,
/// Additional JS scripts to include at the bottom of the rendered page's
/// Additional JS scripts to include at the bottom of the rendered page's
/// `<body>`.
pub additional_js: Vec<PathBuf>,
/// Playpen settings.
@@ -425,25 +424,85 @@ pub struct HtmlConfig {
pub livereload_url: Option<String>,
/// Should section labels be rendered?
pub no_section_label: bool,
/// Search settings. If `None`, the default will be used.
pub search: Option<Search>,
}
impl HtmlConfig {
/// Returns the directory of theme from the provided root directory. If the
/// directory is not present it will append the default directory of "theme"
pub fn theme_dir(&self, root: &PathBuf) -> PathBuf {
match self.theme {
Some(ref d) => root.join(d),
None => root.join("theme"),
}
}
}
/// Configuration for tweaking how the the HTML renderer handles the playpen.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Playpen {
/// The path to the editor to use. Defaults to the [Ace Editor].
///
/// [Ace Editor]: https://ace.c9.io/
pub editor: PathBuf,
/// Should playpen snippets be editable? Defaults to `false`.
/// Should playpen snippets be editable? Default: `false`.
pub editable: bool,
/// Copy JavaScript files for the editor to the output directory?
/// Default: `true`.
pub copy_js: bool,
}
impl Default for Playpen {
fn default() -> Playpen {
Playpen {
editor: PathBuf::from("ace"),
editable: false,
copy_js: true,
}
}
}
/// Configuration of the search functionality of the HTML renderer.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Search {
/// Maximum number of visible results. Default: `30`.
pub limit_results: u32,
/// The number of words used for a search result teaser. Default: `30`,
pub teaser_word_count: u32,
/// Define the logical link between multiple search words.
/// If true, all search words must appear in each result. Default: `true`.
pub use_boolean_and: bool,
/// Boost factor for the search result score if a search word appears in the header.
/// Default: `2`.
pub boost_title: u8,
/// Boost factor for the search result score if a search word appears in the hierarchy.
/// The hierarchy contains all titles of the parent documents and all parent headings.
/// Default: `1`.
pub boost_hierarchy: u8,
/// Boost factor for the search result score if a search word appears in the text.
/// Default: `1`.
pub boost_paragraph: u8,
/// True if the searchword `micro` should match `microwave`. Default: `true`.
pub expand: bool,
/// Documents are split into smaller parts, seperated by headings. This defines, until which
/// level of heading documents should be split. Default: `3`. (`### This is a level 3 heading`)
pub heading_split_level: u8,
/// Copy JavaScript files for the search functionality to the output directory?
/// Default: `true`.
pub copy_js: bool,
}
impl Default for Search {
fn default() -> Search {
// Please update the documentation of `Search` when changing values!
Search {
limit_results: 30,
teaser_word_count: 30,
use_boolean_and: false,
boost_title: 2,
boost_hierarchy: 1,
boost_paragraph: 1,
expand: true,
heading_split_level: 3,
copy_js: true,
}
}
}
@@ -520,12 +579,14 @@ mod tests {
let build_should_be = BuildConfig {
build_dir: PathBuf::from("outputs"),
create_missing: false,
preprocess: Some(vec!["first_preprocessor".to_string(),
"second_preprocessor".to_string()]),
preprocess: Some(vec![
"first_preprocessor".to_string(),
"second_preprocessor".to_string(),
]),
};
let playpen_should_be = Playpen {
editable: true,
editor: PathBuf::from("ace"),
copy_js: true,
};
let html_should_be = HtmlConfig {
curly_quotes: true,

View File

@@ -1,7 +1,8 @@
//! # mdBook
//!
//! **mdBook** is similar to GitBook but implemented in Rust.
//! It offers a command line interface, but can also be used as a regular crate.
//! **mdBook** is a tool for rendering a collection of markdown documents into
//! a form more suitable for end users like HTML or EPUB. It offers a command
//! line interface, but this crate can be used if more control is required.
//!
//! This is the API doc, the [user guide] is also available if you want
//! information about the command line tool, format, structure etc. It is also
@@ -15,6 +16,12 @@
//! - Accessing the public API to help create a new Renderer
//! - ...
//!
//! > **Note:** While we try to ensure `mdbook`'s command-line interface and
//! > behaviour are backwards compatible, the tool's internals are still
//! > evolving and being iterated on. If you wish to prevent accidental
//! > breakages it is recommended to pin any tools building on top of the
//! > `mdbook` crate to a specific release.
//!
//! # Examples
//!
//! If creating a new book from scratch, you'll want to get a `BookBuilder` via
@@ -52,20 +59,20 @@
//!
//! ## Implementing a new Backend
//!
//! `mdbook` has a fairly flexible mechanism for creating additional backends
//! `mdbook` has a fairly flexible mechanism for creating additional backends
//! for your book. The general idea is you'll add an extra table in the book's
//! `book.toml` which specifies an executable to be invoked by `mdbook`. This
//! executable will then be called during a build, with an in-memory
//! executable will then be called during a build, with an in-memory
//! representation ([`RenderContext`]) of the book being passed to the
//! subprocess via `stdin`.
//!
//! The [`RenderContext`] gives the backend access to the contents of
//! subprocess via `stdin`.
//!
//! The [`RenderContext`] gives the backend access to the contents of
//! `book.toml` and lets it know which directory all generated artefacts should
//! be placed in. For a much more in-depth explanation, consult the [relevant
//! chapter] in the *For Developers* section of the user guide.
//!
//! To make creating a backend easier, the `mdbook` crate can be imported
//! directly, making deserializing the `RenderContext` easy and giving you
//!
//! To make creating a backend easier, the `mdbook` crate can be imported
//! directly, making deserializing the `RenderContext` easy and giving you
//! access to the various methods for working with the [`Config`].
//!
//! [user guide]: https://rust-lang-nursery.github.io/mdBook/
@@ -92,7 +99,7 @@ extern crate serde_derive;
#[macro_use]
extern crate serde_json;
extern crate shlex;
extern crate tempdir;
extern crate tempfile;
extern crate toml;
extern crate toml_query;
@@ -122,6 +129,7 @@ pub mod errors {
HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"];
HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"];
Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
SerdeJson(::serde_json::Error) #[doc = "JSON conversion failed"];
}
links {

91
src/preprocess/index.rs Normal file
View File

@@ -0,0 +1,91 @@
use std::path::Path;
use regex::Regex;
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));
}
}

View File

@@ -9,6 +9,7 @@ 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.
@@ -36,7 +37,7 @@ impl Preprocessor for LinkPreprocessor {
.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 +46,12 @@ impl Preprocessor for LinkPreprocessor {
}
}
fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> String {
fn replace_all<P: AsRef<Path>>(s: &str, path: P, source: &P, depth: usize) -> String {
// 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 +60,15 @@ 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.to_path_buf(), depth + 1));
}
}
else {
error!("Stack depth exceeded in {}. Check for cyclic includes",
source.display());
}
previous_end_index = playpen.end_index;
}
Err(e) => {
@@ -84,6 +94,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 +122,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());

View File

@@ -1,8 +1,10 @@
//! Book preprocessing.
pub use self::links::LinkPreprocessor;
pub use self::index::IndexPreprocessor;
mod links;
mod index;
use book::Book;
use config::Config;
@@ -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<()>;
}
}

View File

@@ -1,21 +1,19 @@
use renderer::html_handlebars::helpers;
use renderer::{RenderContext, Renderer};
use book::{Book, BookItem, Chapter};
use config::{Config, HtmlConfig, Playpen};
use {theme, utils};
use theme::{playpen_editor, Theme};
use errors::*;
use regex::{Captures, Regex};
use renderer::html_handlebars::helpers;
use renderer::{RenderContext, Renderer};
use theme::{self, playpen_editor, Theme};
use utils;
#[allow(unused_imports)] use std::ascii::AsciiExt;
use std::path::{Path, PathBuf};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
use handlebars::Handlebars;
use regex::{Captures, Regex};
use serde_json;
#[derive(Default)]
@@ -26,23 +24,10 @@ impl HtmlHandlebars {
HtmlHandlebars
}
fn write_file<P: AsRef<Path>>(
&self,
build_dir: &Path,
filename: P,
content: &[u8],
) -> Result<()> {
let path = build_dir.join(filename);
utils::fs::create_file(&path)?
.write_all(content)
.map_err(|e| e.into())
}
fn render_item(
&self,
item: &BookItem,
mut ctx: RenderItemContext,
item: &BookItem,
mut ctx: RenderItemContext,
print_content: &mut String,
) -> Result<()> {
// FIXME: This should be made DRY-er and rely less on mutable state
@@ -56,6 +41,11 @@ impl HtmlHandlebars {
let path = ch.path
.to_str()
.chain_err(|| "Could not convert path to str")?;
let filepath = Path::new(&ch.path).with_extension("html");
let filepathstr = filepath
.to_str()
.chain_err(|| "Could not convert HTML path to str")?;
let filepathstr = utils::fs::normalize_path(filepathstr);
// "print.html" is used for the print page.
if ch.path == Path::new("print.md") {
@@ -66,9 +56,9 @@ impl HtmlHandlebars {
let title: String;
{
let book_title = ctx.data
.get("book_title")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
.get("book_title")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
title = ch.name.clone() + " - " + book_title;
}
@@ -76,25 +66,20 @@ impl HtmlHandlebars {
ctx.data.insert("content".to_owned(), json!(content));
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
ctx.data.insert("title".to_owned(), json!(title));
ctx.data.insert("path_to_root".to_owned(),
json!(utils::fs::path_to_root(&ch.path)));
ctx.data.insert(
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(&ch.path)),
);
// Render the handlebars template with the data
debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?;
let filepath = Path::new(&ch.path).with_extension("html");
let rendered = self.post_process(
rendered,
&normalize_path(filepath.to_str().ok_or_else(|| {
Error::from(format!("Bad file name: {}", filepath.display()))
})?),
&ctx.html_config.playpen,
);
let rendered = self.post_process(rendered, &filepathstr, &ctx.html_config.playpen);
// Write to file
debug!("Creating {} ✓", filepath.display());
self.write_file(&ctx.destination, filepath, &rendered.into_bytes())?;
debug!("Creating {} ✓", filepathstr);
utils::fs::write_file(&ctx.destination, &filepath, &rendered.into_bytes())?;
if ctx.is_index {
self.render_index(ch, &ctx.destination)?;
@@ -118,12 +103,13 @@ impl HtmlHandlebars {
// 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");
content = content
.lines()
.filter(|line| !line.contains("<base href="))
.collect::<Vec<&str>>()
.join("\n");
self.write_file(destination, "index.html", content.as_bytes())?;
utils::fs::write_file(destination, "index.html", content.as_bytes())?;
debug!(
"Creating index.html from {} ✓",
@@ -134,11 +120,7 @@ impl HtmlHandlebars {
}
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
fn post_process(&self,
rendered: String,
filepath: &str,
playpen_config: &Playpen)
-> String {
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);
let rendered = fix_code_blocks(&rendered);
@@ -153,63 +135,68 @@ impl HtmlHandlebars {
theme: &Theme,
html_config: &HtmlConfig,
) -> Result<()> {
self.write_file(destination, "book.js", &theme.js)?;
self.write_file(destination, "book.css", &theme.css)?;
self.write_file(destination, "favicon.png", &theme.favicon)?;
self.write_file(destination, "highlight.css", &theme.highlight_css)?;
self.write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
self.write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
self.write_file(destination, "highlight.js", &theme.highlight_js)?;
self.write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
self.write_file(
use utils::fs::write_file;
write_file(destination, ".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, "favicon.png", &theme.favicon)?;
write_file(destination, "highlight.css", &theme.highlight_css)?;
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
write_file(destination, "highlight.js", &theme.highlight_js)?;
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
write_file(
destination,
"_FontAwesome/css/font-awesome.css",
"FontAwesome/css/font-awesome.css",
theme::FONT_AWESOME,
)?;
self.write_file(
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.eot",
"FontAwesome/fonts/fontawesome-webfont.eot",
theme::FONT_AWESOME_EOT,
)?;
self.write_file(
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.svg",
"FontAwesome/fonts/fontawesome-webfont.svg",
theme::FONT_AWESOME_SVG,
)?;
self.write_file(
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.ttf",
"FontAwesome/fonts/fontawesome-webfont.ttf",
theme::FONT_AWESOME_TTF,
)?;
self.write_file(
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.woff",
"FontAwesome/fonts/fontawesome-webfont.woff",
theme::FONT_AWESOME_WOFF,
)?;
self.write_file(
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.woff2",
"FontAwesome/fonts/fontawesome-webfont.woff2",
theme::FONT_AWESOME_WOFF2,
)?;
self.write_file(
write_file(
destination,
"_FontAwesome/fonts/FontAwesome.ttf",
"FontAwesome/fonts/FontAwesome.ttf",
theme::FONT_AWESOME_TTF,
)?;
let playpen_config = &html_config.playpen;
// Ace is a very large dependency, so only load it when requested
if playpen_config.editable {
if playpen_config.editable && playpen_config.copy_js {
// Load the editor
let editor = playpen_editor::PlaypenEditor::new(&playpen_config.editor);
self.write_file(destination, "editor.js", &editor.js)?;
self.write_file(destination, "ace.js", &editor.ace_js)?;
self.write_file(destination, "mode-rust.js", &editor.mode_rust_js)?;
self.write_file(destination, "theme-dawn.js", &editor.theme_dawn_js)?;
self.write_file(destination,
write_file(destination, "editor.js", playpen_editor::JS)?;
write_file(destination, "ace.js", playpen_editor::ACE_JS)?;
write_file(destination, "mode-rust.js", playpen_editor::MODE_RUST_JS)?;
write_file(destination, "theme-dawn.js", playpen_editor::THEME_DAWN_JS)?;
write_file(
destination,
"theme-tomorrow_night.js",
&editor.theme_tomorrow_night_js,
playpen_editor::THEME_TOMORROW_NIGHT_JS,
)?;
}
@@ -217,28 +204,42 @@ impl HtmlHandlebars {
}
/// Update the context with data for this file
fn configure_print_version(&self,
data: &mut serde_json::Map<String, serde_json::Value>,
print_content: &str) {
fn configure_print_version(
&self,
data: &mut serde_json::Map<String, serde_json::Value>,
print_content: &str,
) {
// Make sure that the Print chapter does not display the title from
// the last rendered chapter by removing it from its context
data.remove("title");
data.insert("is_print".to_owned(), json!(true));
data.insert("path".to_owned(), json!("print.md"));
data.insert("content".to_owned(), json!(print_content));
data.insert("path_to_root".to_owned(),
json!(utils::fs::path_to_root(Path::new("print.md"))));
data.insert(
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(Path::new("print.md"))),
);
}
fn register_hbs_helpers(&self, handlebars: &mut Handlebars, html_config: &HtmlConfig) {
handlebars.register_helper("toc", Box::new(helpers::toc::RenderToc {no_section_label: html_config.no_section_label}));
handlebars.register_helper(
"toc",
Box::new(helpers::toc::RenderToc {
no_section_label: html_config.no_section_label,
}),
);
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
handlebars.register_helper("next", Box::new(helpers::navigation::next));
}
/// Copy across any additional CSS and JavaScript files which the book
/// has been configured to use.
fn copy_additional_css_and_js(&self, html: &HtmlConfig, root: &Path, destination: &Path) -> Result<()> {
fn copy_additional_css_and_js(
&self,
html: &HtmlConfig,
root: &Path,
destination: &Path,
) -> Result<()> {
let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
debug!("Copying additional CSS and JS");
@@ -269,6 +270,25 @@ impl HtmlHandlebars {
}
}
// TODO(mattico): Remove some time after the 0.1.8 release
fn maybe_wrong_theme_dir(dir: &Path) -> Result<bool> {
fn entry_is_maybe_book_file(entry: fs::DirEntry) -> Result<bool> {
Ok(entry.file_type()?.is_file()
&& entry.path().extension().map_or(false, |ext| ext == "md"))
}
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
if entry_is_maybe_book_file(entry?).unwrap_or(false) {
return Ok(false);
}
}
Ok(true)
} else {
Ok(false)
}
}
impl Renderer for HtmlHandlebars {
fn name(&self) -> &str {
"html"
@@ -285,9 +305,19 @@ impl Renderer for HtmlHandlebars {
let theme_dir = match html_config.theme {
Some(ref theme) => theme.to_path_buf(),
None => src_dir.join("theme"),
None => ctx.root.join("theme"),
};
if html_config.theme.is_none()
&& maybe_wrong_theme_dir(&src_dir.join("theme")).unwrap_or(false)
{
warn!(
"Previous versions of mdBook erroneously accepted `./src/theme` as an automatic \
theme directory"
);
warn!("Please move your theme files to `./theme` for them to continue being used");
}
let theme = theme::Theme::new(theme_dir);
debug!("Register the index handlebars template");
@@ -307,15 +337,17 @@ impl Renderer for HtmlHandlebars {
fs::create_dir_all(&destination)
.chain_err(|| "Unexpected error when constructing destination path")?;
for (i, item) in book.iter().enumerate() {
let mut is_index = true;
for item in book.iter() {
let ctx = RenderItemContext {
handlebars: &handlebars,
destination: destination.to_path_buf(),
data: data.clone(),
is_index: i == 0,
is_index: is_index,
html_config: html_config.clone(),
};
self.render_item(item, ctx, &mut print_content)?;
is_index = false;
}
// Print version
@@ -326,14 +358,11 @@ impl Renderer for HtmlHandlebars {
// Render the handlebars template with the data
debug!("Render template");
let rendered = handlebars.render("index", &data)?;
let rendered = self.post_process(rendered,
"print.html",
&html_config.playpen);
let rendered = self.post_process(rendered, "print.html", &html_config.playpen);
self.write_file(&destination, "print.html", &rendered.into_bytes())?;
utils::fs::write_file(&destination, "print.html", &rendered.into_bytes())?;
debug!("Creating print.html ✓");
debug!("Copy static files");
@@ -342,6 +371,10 @@ impl Renderer for HtmlHandlebars {
self.copy_additional_css_and_js(&html_config, &ctx.root, &destination)
.chain_err(|| "Unable to copy across additional CSS and JS")?;
// Render search index
#[cfg(feature = "search")]
super::search::create_files(&html_config.search.unwrap_or_default(), &destination, &book)?;
// Copy all remaining files
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
@@ -349,14 +382,25 @@ impl Renderer for HtmlHandlebars {
}
}
fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig) -> Result<serde_json::Map<String, serde_json::Value>> {
fn make_data(
root: &Path,
book: &Book,
config: &Config,
html_config: &HtmlConfig,
) -> Result<serde_json::Map<String, serde_json::Value>> {
trace!("make_data");
let html = config.html_config().unwrap_or_default();
let mut data = serde_json::Map::new();
data.insert("language".to_owned(), json!("en"));
data.insert("book_title".to_owned(), json!(config.book.title.clone().unwrap_or_default()));
data.insert("description".to_owned(), json!(config.book.description.clone().unwrap_or_default()));
data.insert(
"book_title".to_owned(),
json!(config.book.title.clone().unwrap_or_default()),
);
data.insert(
"description".to_owned(),
json!(config.book.description.clone().unwrap_or_default()),
);
data.insert("favicon".to_owned(), json!("favicon.png"));
if let Some(ref livereload) = html_config.livereload_url {
data.insert("livereload".to_owned(), json!(livereload));
@@ -376,13 +420,8 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
let mut css = Vec::new();
for style in &html.additional_css {
match style.strip_prefix(root) {
Ok(p) => {
css.push(p.to_str().expect("Could not convert to str"))
},
Err(_) => {
css.push(style.to_str()
.expect("Could not convert to str"))
}
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
Err(_) => css.push(style.to_str().expect("Could not convert to str")),
}
}
data.insert("additional_css".to_owned(), json!(css));
@@ -394,25 +433,34 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
for script in &html.additional_js {
match script.strip_prefix(root) {
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
Err(_) => {
js.push(script.file_name()
.expect("File has a file name")
.to_str()
.expect("Could not convert to str"))
}
Err(_) => js.push(
script
.file_name()
.expect("File has a file name")
.to_str()
.expect("Could not convert to str"),
),
}
}
data.insert("additional_js".to_owned(), json!(js));
}
if html.playpen.editable {
data.insert("playpens_editable".to_owned(), json!(true));
data.insert("editor_js".to_owned(), json!("editor.js"));
data.insert("ace_js".to_owned(), json!("ace.js"));
data.insert("mode_rust_js".to_owned(), json!("mode-rust.js"));
data.insert("theme_dawn_js".to_owned(), json!("theme-dawn.js"));
data.insert("theme_tomorrow_night_js".to_owned(),
json!("theme-tomorrow_night.js"));
if html.playpen.editable && html.playpen.copy_js {
data.insert("playpen_js".to_owned(), json!(true));
}
let search = html_config.search.clone();
if cfg!(feature = "search") {
data.insert("search_enabled".to_owned(), json!(true));
if search.unwrap_or_default().copy_js {
data.insert("search_js".to_owned(), json!(true));
}
} else if search.is_some() {
warn!("mdBook compiled without search support, ignoring `output.html.search` table");
warn!(
"please reinstall with `cargo install mdbook --force --features search`to use the \
search feature"
)
}
let mut chapters = vec![];
@@ -453,23 +501,26 @@ fn build_header_links(html: &str, filepath: &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, filepath)
})
.into_owned()
}
/// Wraps a single header tag with a link, making sure each tag gets its own
/// unique ID by appending an auto-incremented number (if necessary).
fn wrap_header_with_link(level: usize,
content: &str,
id_counter: &mut HashMap<String, usize>,
filepath: &str)
-> String {
let raw_id = id_from_content(content);
fn wrap_header_with_link(
level: usize,
content: &str,
id_counter: &mut HashMap<String, usize>,
filepath: &str,
) -> String {
let raw_id = utils::id_from_content(content);
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
@@ -489,53 +540,28 @@ fn wrap_header_with_link(level: usize,
)
}
/// Generate an id for use with anchors which is derived from a "normalised"
/// string.
fn id_from_content(content: &str) -> String {
let mut content = content.to_string();
// Skip any tags or html-encoded stuff
const REPL_SUB: &[&str] = &["<em>",
"</em>",
"<code>",
"</code>",
"<strong>",
"</strong>",
"&lt;",
"&gt;",
"&amp;",
"&#39;",
"&quot;"];
for sub in REPL_SUB {
content = content.replace(sub, "");
}
// Remove spaces and hastags indicating a header
let trimmed = content.trim().trim_left_matches('#').trim();
normalize_id(trimmed)
}
// anchors to the same page (href="#anchor") do not work because of
// <base href="../"> pointing to the root folder. This function *fixes*
// that in a very inelegant way
fn fix_anchor_links(html: &str, filepath: &str) -> String {
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
regex.replace_all(html, |caps: &Captures| {
let before = &caps[1];
let anchor = &caps[2];
let after = &caps[3];
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}>",
format!(
"<a{before}href=\"{filepath}#{anchor}\"{after}>",
before = before,
filepath = filepath,
anchor = anchor,
after = after)
})
.into_owned()
after = after
)
})
.into_owned()
}
// The rust book uses annotations for rustdoc to test code snippets,
// like the following:
// ```rust,should_panic
@@ -546,53 +572,54 @@ 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("mdbook-runnable")
{
format!("<pre class=\"playpen\">{}</pre>", text)
} else {
// we need to inject our own main
let (attrs, code) = partition_source(code);
// wrap the contents in an external pre block
if playpen_config.editable && classes.contains("editable")
|| text.contains("fn main") || text.contains("quick_main!")
{
format!("<pre class=\"playpen\">{}</pre>", text)
} else {
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!("<pre class=\"playpen\"><code class=\"{}\">\n# \
#![allow(unused_variables)]\n\
{}#fn main() {{\n\
{}\
#}}</code></pre>",
classes,
attrs,
code)
format!(
"<pre class=\"playpen\"><code class=\"{}\">\n# \
#![allow(unused_variables)]\n{}#fn main() {{\n{}#}}</code></pre>",
classes, attrs, code
)
}
} else {
// not language-rust, so no-op
text.to_owned()
}
} else {
// not language-rust, so no-op
text.to_owned()
}
})
.into_owned()
})
.into_owned()
}
fn partition_source(s: &str) -> (String, String) {
@@ -624,26 +651,6 @@ struct RenderItemContext<'a> {
html_config: HtmlConfig,
}
pub fn normalize_path(path: &str) -> String {
use std::path::is_separator;
path.chars()
.map(|ch| if is_separator(ch) { '/' } else { ch })
.collect::<String>()
}
pub fn normalize_id(content: &str) -> String {
content.chars()
.filter_map(|ch| if ch.is_alphanumeric() || ch == '_' || ch == '-' {
Some(ch.to_ascii_lowercase())
} else if ch.is_whitespace() {
Some('-')
} else {
None
})
.collect::<String>()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -687,12 +694,4 @@ mod tests {
assert_eq!(got, should_be);
}
}
#[test]
fn anchor_generation() {
assert_eq!(id_from_content("## `--passes`: add more rustdoc passes"),
"--passes-add-more-rustdoc-passes");
assert_eq!(id_from_content("## Method-call expressions"),
"method-call-expressions");
}
}

View File

@@ -1,2 +1,2 @@
pub mod navigation;
pub mod toc;
pub mod navigation;

View File

@@ -4,7 +4,6 @@ use std::collections::BTreeMap;
use serde_json;
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
type StringMap = BTreeMap<String, String>;
/// Target for `find_chapter`.
@@ -15,22 +14,23 @@ enum Target {
impl Target {
/// Returns target if found.
fn find(&self,
base_path: &String,
current_path: &String,
current_item: &StringMap,
previous_item: &StringMap,
) -> Result<Option<StringMap>, RenderError> {
fn find(
&self,
base_path: &String,
current_path: &String,
current_item: &StringMap,
previous_item: &StringMap,
) -> Result<Option<StringMap>, RenderError> {
match self {
&Target::Next => {
let previous_path = previous_item.get("path").ok_or_else(|| {
RenderError::new("No path found for chapter in JSON data")
})?;
let previous_path = previous_item
.get("path")
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
if previous_path == base_path {
return Ok(Some(current_item.clone()));
}
},
}
&Target::Previous => {
if current_path == base_path {
@@ -43,21 +43,18 @@ impl Target {
}
}
fn find_chapter(
rc: &mut RenderContext,
target: Target
) -> Result<Option<StringMap>, RenderError> {
fn find_chapter(rc: &mut RenderContext, target: Target) -> Result<Option<StringMap>, RenderError> {
debug!("Get data from context");
let chapters = rc.evaluate_absolute("chapters").and_then(|c| {
let chapters = rc.evaluate_absolute("chapters", true).and_then(|c| {
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
let base_path = rc.evaluate_absolute("path")?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
let base_path = rc.evaluate_absolute("path", true)?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
let mut previous: Option<StringMap> = None;
@@ -78,7 +75,7 @@ fn find_chapter(
}
}
Ok(None)
Ok(None)
}
fn render(
@@ -91,18 +88,21 @@ fn render(
let mut context = BTreeMap::new();
chapter.get("name")
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
.map(|name| context.insert("title".to_owned(), json!(name)))?;
chapter
.get("name")
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
.map(|name| context.insert("title".to_owned(), json!(name)))?;
chapter.get("path")
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
.and_then(|p| {
Path::new(p).with_extension("html")
.to_str()
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
.map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/"))))
})?;
chapter
.get("path")
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
.and_then(|p| {
Path::new(p)
.with_extension("html")
.to_str()
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
.map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/"))))
})?;
trace!("Render template");
@@ -138,14 +138,14 @@ pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), R
#[cfg(test)]
mod tests {
use super::*;
use super::*;
static TEMPLATE: &'static str =
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
static TEMPLATE: &'static str =
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
#[test]
fn test_next_previous() {
let data = json!({
#[test]
fn test_next_previous() {
let data = json!({
"name": "two",
"path": "two.path",
"chapters": [
@@ -164,18 +164,19 @@ mod tests {
]
});
let mut h = Handlebars::new();
h.register_helper("previous", Box::new(previous));
h.register_helper("next", Box::new(next));
let mut h = Handlebars::new();
h.register_helper("previous", Box::new(previous));
h.register_helper("next", Box::new(next));
assert_eq!(
h.template_render(TEMPLATE, &data).unwrap(),
"one: one.html|three: three.html");
}
assert_eq!(
h.render_template(TEMPLATE, &data).unwrap(),
"one: one.html|three: three.html"
);
}
#[test]
fn test_first() {
let data = json!({
#[test]
fn test_first() {
let data = json!({
"name": "one",
"path": "one.path",
"chapters": [
@@ -194,17 +195,18 @@ mod tests {
]
});
let mut h = Handlebars::new();
h.register_helper("previous", Box::new(previous));
h.register_helper("next", Box::new(next));
let mut h = Handlebars::new();
h.register_helper("previous", Box::new(previous));
h.register_helper("next", Box::new(next));
assert_eq!(
h.template_render(TEMPLATE, &data).unwrap(),
"|two: two.html");
}
#[test]
fn test_last() {
let data = json!({
assert_eq!(
h.render_template(TEMPLATE, &data).unwrap(),
"|two: two.html"
);
}
#[test]
fn test_last() {
let data = json!({
"name": "three",
"path": "three.path",
"chapters": [
@@ -223,12 +225,13 @@ mod tests {
]
});
let mut h = Handlebars::new();
h.register_helper("previous", Box::new(previous));
h.register_helper("next", Box::new(next));
let mut h = Handlebars::new();
h.register_helper("previous", Box::new(previous));
h.register_helper("next", Box::new(next));
assert_eq!(
h.template_render(TEMPLATE, &data).unwrap(),
"two: two.html|");
}
assert_eq!(
h.render_template(TEMPLATE, &data).unwrap(),
"two: two.html|"
);
}
}

View File

@@ -8,7 +8,7 @@ use pulldown_cmark::{html, Event, Parser, Tag};
// Handlebars helper to construct TOC
#[derive(Clone, Copy)]
pub struct RenderToc {
pub no_section_label: bool
pub no_section_label: bool,
}
impl HelperDef for RenderToc {
@@ -16,14 +16,14 @@ impl HelperDef for RenderToc {
// get value from context data
// rc.get_path() is current json parent path, you should always use it like this
// param is the key of value you want to display
let chapters = rc.evaluate_absolute("chapters").and_then(|c| {
let chapters = rc.evaluate_absolute("chapters", true).and_then(|c| {
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
let current = rc.evaluate_absolute("path")?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
let current = rc.evaluate_absolute("path", true)?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
rc.writer.write_all(b"<ol class=\"chapter\">")?;
@@ -107,12 +107,12 @@ impl HelperDef for RenderToc {
// filter all events that are not inline code blocks
let parser = Parser::new(name).filter(|event| match *event {
Event::Start(Tag::Code) |
Event::End(Tag::Code) |
Event::InlineHtml(_) |
Event::Text(_) => true,
_ => false,
});
Event::Start(Tag::Code)
| Event::End(Tag::Code)
| Event::InlineHtml(_)
| Event::Text(_) => true,
_ => false,
});
// render markdown to html
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);

View File

@@ -4,3 +4,6 @@ pub use self::hbs_renderer::HtmlHandlebars;
mod hbs_renderer;
mod helpers;
#[cfg(feature = "search")]
mod search;

View File

@@ -0,0 +1,235 @@
extern crate ammonia;
extern crate elasticlunr;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use pulldown_cmark::*;
use serde_json;
use self::elasticlunr::Index;
use book::{Book, BookItem};
use config::Search;
use errors::*;
use utils;
use theme::searcher;
/// Creates all files required for search.
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
let mut index = Index::new(&["title", "body", "breadcrumbs"]);
for item in book.iter() {
render_item(&mut index, &search_config, item)?;
}
let index = write_to_js(index, &search_config)?;
debug!("Writing search index ✓");
if search_config.copy_js {
utils::fs::write_file(destination, "searchindex.js", index.as_bytes())?;
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
debug!("Copying search files ✓");
}
Ok(())
}
/// Uses the given arguments to construct a search document, then inserts it to the given index.
fn add_doc<'a>(
index: &mut Index,
anchor_base: &'a str,
section_id: &Option<String>,
items: &[&str],
) {
let doc_ref: Cow<'a, str> = if let &Some(ref id) = section_id {
format!("{}#{}", anchor_base, id).into()
} else {
anchor_base.into()
};
let doc_ref = utils::collapse_whitespace(doc_ref.trim());
let items = items.iter().map(|&x| utils::collapse_whitespace(x.trim()));
index.add_doc(&doc_ref, items);
}
/// Renders markdown into flat unformatted text and adds it to the search index.
fn render_item(
index: &mut Index,
search_config: &Search,
item: &BookItem,
) -> Result<()> {
let chapter = match item {
&BookItem::Chapter(ref ch) => ch,
_ => return Ok(()),
};
let filepath = Path::new(&chapter.path).with_extension("html");
let filepath = filepath
.to_str()
.chain_err(|| "Could not convert HTML path to str")?;
let anchor_base = utils::fs::normalize_path(filepath);
let mut opts = Options::empty();
opts.insert(OPTION_ENABLE_TABLES);
opts.insert(OPTION_ENABLE_FOOTNOTES);
let p = Parser::new_ext(&chapter.content, opts);
let mut in_header = false;
let max_section_depth = search_config.heading_split_level as i32;
let mut section_id = None;
let mut heading = String::new();
let mut body = String::new();
let mut breadcrumbs = chapter.parent_names.clone();
let mut footnote_numbers = HashMap::new();
for event in p {
match event {
Event::Start(Tag::Header(i)) if i <= max_section_depth => {
if heading.len() > 0 {
// Section finished, the next header is following now
// Write the data to the index, and clear it for the next section
add_doc(
index,
&anchor_base,
&section_id,
&[&heading, &body, &breadcrumbs.join(" » ")],
);
section_id = None;
heading.clear();
body.clear();
breadcrumbs.pop();
}
in_header = true;
}
Event::End(Tag::Header(i)) if i <= max_section_depth => {
in_header = false;
section_id = Some(utils::id_from_content(&heading));
breadcrumbs.push(heading.clone());
}
Event::Start(Tag::FootnoteDefinition(name)) => {
let number = footnote_numbers.len() + 1;
footnote_numbers.entry(name).or_insert(number);
}
Event::Start(_) | Event::End(_) | Event::SoftBreak | Event::HardBreak => {
// Insert spaces where HTML output would usually seperate text
// to ensure words don't get merged together
if in_header {
heading.push(' ');
} else {
body.push(' ');
}
}
Event::Text(text) => {
if in_header {
heading.push_str(&text);
} else {
body.push_str(&text);
}
}
Event::Html(html) | Event::InlineHtml(html) => {
body.push_str(&clean_html(&html));
}
Event::FootnoteReference(name) => {
let len = footnote_numbers.len() + 1;
let number = footnote_numbers.entry(name).or_insert(len);
body.push_str(&format!(" [{}] ", number));
}
}
}
if heading.len() > 0 {
// Make sure the last section is added to the index
add_doc(
index,
&anchor_base,
&section_id,
&[&heading, &body, &breadcrumbs.join(" » ")],
);
}
Ok(())
}
/// Exports the index and search options to a JS script which stores the index in `window.search`.
/// Using a JS script is a workaround for CORS in `file://` URIs. It also removes the need for
/// downloading/parsing JSON in JS.
fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
use std::collections::BTreeMap;
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
#[derive(Serialize)]
struct ResultsOptions {
limit_results: u32,
teaser_word_count: u32,
}
#[derive(Serialize)]
struct SearchindexJson {
/// The options used for displaying search results
resultsoptions: ResultsOptions,
/// The searchoptions for elasticlunr.js
searchoptions: SearchOptions,
/// The index for elasticlunr.js
index: elasticlunr::Index,
}
let mut fields = BTreeMap::new();
let mut opt = SearchOptionsField::default();
opt.boost = Some(search_config.boost_title);
fields.insert("title".into(), opt);
opt.boost = Some(search_config.boost_paragraph);
fields.insert("body".into(), opt);
opt.boost = Some(search_config.boost_hierarchy);
fields.insert("breadcrumbs".into(), opt);
let searchoptions = SearchOptions {
bool: if search_config.use_boolean_and {
SearchBool::And
} else {
SearchBool::Or
},
expand: search_config.expand,
fields,
};
let resultsoptions = ResultsOptions {
limit_results: search_config.limit_results,
teaser_word_count: search_config.teaser_word_count,
};
let json_contents = SearchindexJson {
resultsoptions,
searchoptions,
index,
};
// 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))
}
fn clean_html(html: &str) -> String {
lazy_static! {
static ref AMMONIA: ammonia::Builder<'static> = {
let mut clean_content = HashSet::new();
clean_content.insert("script");
clean_content.insert("style");
let mut builder = ammonia::Builder::new();
builder
.tags(HashSet::new())
.tag_attributes(HashMap::new())
.generic_attributes(HashSet::new())
.link_rel(None)
.allowed_classes(HashMap::new())
.clean_content_tags(clean_content);
builder
};
}
AMMONIA.clean(html).to_string()
}

View File

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 348 KiB

View File

@@ -36,6 +36,15 @@ h5 {
.header + .header h5 {
margin-top: 1em;
}
a.header:target h1:before,
a.header:target h2:before,
a.header:target h3:before,
a.header:target h4:before {
display: inline-block;
content: "»";
margin-left: -30px;
width: 30px;
}
table {
margin: 0 auto;
border-collapse: collapse;
@@ -72,11 +81,13 @@ table thead td {
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
overscroll-behavior-y: contain;
-webkit-transition: -webkit-transform 0.5s;
-moz-transition: -moz-transform 0.5s;
-o-transition: -o-transform 0.5s;
-ms-transition: -ms-transform 0.5s;
transition: transform 0.5s;
}
.js .sidebar {
-webkit-transition: -webkit-transform 0.3s;
-moz-transition: -moz-transform 0.3s;
-o-transition: -o-transform 0.3s;
-ms-transition: -ms-transform 0.3s;
transition: transform 0.3s;
}
.sidebar code {
line-height: 2em;
@@ -131,18 +142,28 @@ table thead td {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-transition: padding-left 0.5s, margin-left 0.5s;
-moz-transition: padding-left 0.5s, margin-left 0.5s;
-o-transition: padding-left 0.5s, margin-left 0.5s;
-ms-transition: padding-left 0.5s, margin-left 0.5s;
transition: padding-left 0.5s, margin-left 0.5s;
}
.js .page-wrapper {
-webkit-transition: margin-left 0.3s ease, -webkit-transform 0.3s ease;
-moz-transition: margin-left 0.3s ease, -moz-transform 0.3s ease;
-o-transition: margin-left 0.3s ease, -o-transform 0.3s ease;
-ms-transition: margin-left 0.3s ease, -ms-transform 0.3s ease;
transition: margin-left 0.3s ease, transform 0.3s ease;
}
.sidebar-visible .page-wrapper {
padding-left: 300px;
-webkit-transform: translateX(300px);
-moz-transform: translateX(300px);
-o-transform: translateX(300px);
-ms-transform: translateX(300px);
transform: translateX(300px);
}
@media only screen and (max-width: 1079px) {
@media only screen and (min-width: 620px) {
.sidebar-visible .page-wrapper {
padding-left: 0;
-webkit-transform: none;
-moz-transform: none;
-o-transform: none;
-ms-transform: none;
transform: none;
margin-left: 300px;
}
}
@@ -151,10 +172,14 @@ table thead td {
padding: 0 15px;
}
.content {
overflow-y: auto;
padding: 0 15px;
padding-bottom: 50px;
}
.content main {
margin-left: auto;
margin-right: auto;
max-width: 750px;
padding-bottom: 50px;
}
.content a {
text-decoration: none;
@@ -184,11 +209,13 @@ table thead td {
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-transition: -webkit-transform 0.5s, border-bottom-color 0.5s;
-moz-transition: -moz-transform 0.5s, border-bottom-color 0.5s;
-o-transition: -o-transform 0.5s, border-bottom-color 0.5s;
-ms-transition: -ms-transform 0.5s, border-bottom-color 0.5s;
transition: transform 0.5s, border-bottom-color 0.5s;
}
.js #menu-bar > #menu-bar-sticky-container {
-webkit-transition: -webkit-transform 0.3s;
-moz-transition: -moz-transform 0.3s;
-o-transition: -o-transform 0.3s;
-ms-transition: -ms-transform 0.3s;
transition: transform 0.3s;
}
#menu-bar i,
#menu-bar .icon-button {
@@ -196,15 +223,15 @@ table thead td {
margin: 0 10px;
z-index: 10;
line-height: 50px;
cursor: pointer;
-webkit-transition: color 0.5s;
-moz-transition: color 0.5s;
-o-transition: color 0.5s;
-ms-transition: color 0.5s;
transition: color 0.5s;
}
#menu-bar i:hover,
#menu-bar .icon-button:hover {
cursor: pointer;
#menu-bar #print-button {
margin: 0 15px;
}
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
-webkit-transform: translateY(-60px);
@@ -213,6 +240,9 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
-ms-transform: translateY(-60px);
transform: translateY(-60px);
}
.no-js .left-buttons {
display: none;
}
.menu-title {
display: inline-block;
font-weight: 200;
@@ -231,6 +261,8 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
}
.js .menu-title {
cursor: pointer;
}
.nav-chapters {
@@ -337,11 +369,11 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
color: #333;
background-color: #fff;
/* Inline code */
/* Search */
}
.light .content .header:link,
.light .content .header:visited {
color: #333;
pointer: cursor;
}
.light .content .header:link:hover,
.light .content .header:visited:hover {
@@ -405,6 +437,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.light .mobile-nav-chapters {
background-color: #fafafa;
}
.light #searchresults a,
.light .content a:link,
.light a:visited,
.light a > .hljs {
@@ -499,15 +532,38 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.light ::-webkit-scrollbar-thumb {
background: #ccc;
}
.light #searchbar {
border: 1px solid #aaa;
border-radius: 3px;
background-color: #fafafa;
color: #000;
}
.light #searchbar:focus,
.light #searchbar.active {
-webkit-box-shadow: 0 0 3px #aaa;
box-shadow: 0 0 3px #aaa;
}
.light .searchresults-header {
color: #666;
}
.light .searchresults-outer {
border-bottom: 1px dashed #888;
}
.light ul#searchresults li.focus {
background-color: #e4f2fe;
}
.light mark {
background-color: #a2cff5;
}
.coal {
color: #98a3ad;
background-color: #141617;
/* Inline code */
/* Search */
}
.coal .content .header:link,
.coal .content .header:visited {
color: #98a3ad;
pointer: cursor;
}
.coal .content .header:link:hover,
.coal .content .header:visited:hover {
@@ -571,6 +627,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.coal .mobile-nav-chapters {
background-color: #292c2f;
}
.coal #searchresults a,
.coal .content a:link,
.coal a:visited,
.coal a > .hljs {
@@ -665,15 +722,38 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.coal ::-webkit-scrollbar-thumb {
background: #a1adb8;
}
.coal #searchbar {
border: 1px solid #aaa;
border-radius: 3px;
background-color: #b7b7b7;
color: #000;
}
.coal #searchbar:focus,
.coal #searchbar.active {
-webkit-box-shadow: 0 0 3px #aaa;
box-shadow: 0 0 3px #aaa;
}
.coal .searchresults-header {
color: #666;
}
.coal .searchresults-outer {
border-bottom: 1px dashed #98a3ad;
}
.coal ul#searchresults li.focus {
background-color: #2b2b2f;
}
.coal mark {
background-color: #355c7d;
}
.navy {
color: #bcbdd0;
background-color: #161923;
/* Inline code */
/* Search */
}
.navy .content .header:link,
.navy .content .header:visited {
color: #bcbdd0;
pointer: cursor;
}
.navy .content .header:link:hover,
.navy .content .header:visited:hover {
@@ -737,6 +817,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.navy .mobile-nav-chapters {
background-color: #282d3f;
}
.navy #searchresults a,
.navy .content a:link,
.navy a:visited,
.navy a > .hljs {
@@ -831,15 +912,38 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.navy ::-webkit-scrollbar-thumb {
background: #c8c9db;
}
.navy #searchbar {
border: 1px solid #aaa;
border-radius: 3px;
background-color: #aeaec6;
color: #000;
}
.navy #searchbar:focus,
.navy #searchbar.active {
-webkit-box-shadow: 0 0 3px #aaa;
box-shadow: 0 0 3px #aaa;
}
.navy .searchresults-header {
color: #5f5f71;
}
.navy .searchresults-outer {
border-bottom: 1px dashed #5c5c68;
}
.navy ul#searchresults li.focus {
background-color: #242430;
}
.navy mark {
background-color: #a2cff5;
}
.rust {
color: #262625;
background-color: #e1e1db;
/* Inline code */
/* Search */
}
.rust .content .header:link,
.rust .content .header:visited {
color: #262625;
pointer: cursor;
}
.rust .content .header:link:hover,
.rust .content .header:visited:hover {
@@ -903,6 +1007,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.rust .mobile-nav-chapters {
background-color: #3b2e2a;
}
.rust #searchresults a,
.rust .content a:link,
.rust a:visited,
.rust a > .hljs {
@@ -997,15 +1102,38 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.rust ::-webkit-scrollbar-thumb {
background: #c8c9db;
}
.rust #searchbar {
border: 1px solid #aaa;
border-radius: 3px;
background-color: #fafafa;
color: #000;
}
.rust #searchbar:focus,
.rust #searchbar.active {
-webkit-box-shadow: 0 0 3px #aaa;
box-shadow: 0 0 3px #aaa;
}
.rust .searchresults-header {
color: #666;
}
.rust .searchresults-outer {
border-bottom: 1px dashed #888;
}
.rust ul#searchresults li.focus {
background-color: #dec2a2;
}
.rust mark {
background-color: #e69f67;
}
.ayu {
color: #c5c5c5;
background-color: #0f1419;
/* Inline code */
/* Search */
}
.ayu .content .header:link,
.ayu .content .header:visited {
color: #c5c5c5;
pointer: cursor;
}
.ayu .content .header:link:hover,
.ayu .content .header:visited:hover {
@@ -1069,6 +1197,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.ayu .mobile-nav-chapters {
background-color: #14191f;
}
.ayu #searchresults a,
.ayu .content a:link,
.ayu a:visited,
.ayu a > .hljs {
@@ -1163,6 +1292,29 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.ayu ::-webkit-scrollbar-thumb {
background: #c8c9db;
}
.ayu #searchbar {
border: 1px solid #848484;
border-radius: 3px;
background-color: #424242;
color: #fff;
}
.ayu #searchbar:focus,
.ayu #searchbar.active {
-webkit-box-shadow: 0 0 3px #d4c89f;
box-shadow: 0 0 3px #d4c89f;
}
.ayu .searchresults-header {
color: #666;
}
.ayu .searchresults-outer {
border-bottom: 1px dashed #888;
}
.ayu ul#searchresults li.focus {
background-color: #252932;
}
.ayu mark {
background-color: #e3b171;
}
@media only print {
#sidebar,
#menu-bar,
@@ -1170,12 +1322,14 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.mobile-nav-chapters {
display: none;
}
#page-wrapper {
left: 0;
overflow-y: initial;
}
#page-wrapper.page-wrapper {
padding-left: 0px;
-webkit-transform: none;
-moz-transform: none;
-o-transform: none;
-ms-transform: none;
transform: none;
margin-left: 0px;
overflow-y: initial;
}
#content {
max-width: none;
@@ -1209,16 +1363,14 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
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+ */;
white-space: pre-wrap;
}
.fa {
display: none !important;
}
}
.tooltiptext {
@@ -1243,3 +1395,65 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.tooltipped .tooltiptext {
visibility: visible;
}
#searchresults a {
text-decoration: none;
}
mark {
border-radius: 2px;
padding: 0 3px 1px 3px;
margin: 0 -3px -1px -3px;
-webkit-transition: background-color 300ms linear;
-moz-transition: background-color 300ms linear;
-o-transition: background-color 300ms linear;
-ms-transition: background-color 300ms linear;
transition: background-color 300ms linear;
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: 750px;
}
#searchbar {
width: 100%;
margin: 5px auto 0px auto;
padding: 10px 16px;
-webkit-transition: box-shadow 300ms ease-in-out;
-moz-transition: box-shadow 300ms ease-in-out;
-o-transition: box-shadow 300ms ease-in-out;
-ms-transition: box-shadow 300ms ease-in-out;
transition: box-shadow 300ms ease-in-out;
}
.searchresults-header {
font-weight: bold;
font-size: 1em;
padding: 18px 0 0 5px;
}
.searchresults-outer {
margin-left: auto;
margin-right: auto;
max-width: 750px;
}
ul#searchresults {
list-style: none;
padding-left: 20px;
}
ul#searchresults li {
margin: 10px 0px;
padding: 2px;
border-radius: 2px;
}
ul#searchresults span.teaser {
display: block;
clear: both;
margin: 5px 0 0 20px;
font-size: 0.8em;
}
ul#searchresults span.teaser em {
font-weight: bold;
font-style: normal;
}

View File

@@ -1,3 +1,5 @@
"use strict";
// Fix back button cache problem
window.onunload = function () { };
@@ -55,6 +57,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]);
}
@@ -175,7 +178,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 +216,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 +225,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 +236,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 +251,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 +265,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);
@@ -372,7 +375,15 @@ function playpen_text(playpen) {
});
themePopup.addEventListener('focusout', function(e) {
if (!themePopup.contains(e.relatedTarget)) {
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
if (!!e.relatedTarget && !themePopup.contains(e.relatedTarget)) {
hideThemes();
}
});
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang-nursery/mdBook/issues/628
document.addEventListener('click', function(e) {
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
hideThemes();
}
});
@@ -491,6 +502,7 @@ function playpen_text(playpen) {
(function chapterNavigation() {
document.addEventListener('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (window.search && window.search.hasFocus()) { return; }
switch (e.key) {
case 'ArrowRight':

View File

@@ -1,6 +1,7 @@
<!DOCTYPE HTML>
<html lang="{{ language }}">
<html lang="{{ language }}" class="sidebar-visible no-js">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>{{ title }}</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
@@ -17,30 +18,22 @@
<link rel="shortcut icon" href="{{ favicon }}">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link rel="stylesheet" href="FontAwesome/css/font-awesome.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}}">
{{/each}}
{{#if mathjax_support}}
<!-- MathJax -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}}
<!-- Fetch Clipboard.js from CDN but have a local fallback -->
<script src="https://cdn.jsdelivr.net/clipboard.js/1.6.1/clipboard.min.js"></script>
<script>
if (typeof Clipboard == 'undefined') {
document.write(unescape("%3Cscript src='clipboard.min.js'%3E%3C/script%3E"));
}
</script>
</head>
<body class="light">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
@@ -65,17 +58,19 @@
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = 'light'; }
document.body.className = theme;
document.querySelector('html').className = theme;
document.querySelector('html').className = theme + ' js';
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
document.querySelector('html').classList.add("sidebar-" + sidebar);
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
@@ -102,9 +97,14 @@
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
{{#if search_enabled}}
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
{{/if}}
</div>
<h1 class="menu-title">{{ book_title }}</h1>
<h1 class="menu-title">{{ book_title }}</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
@@ -114,6 +114,19 @@
</div>
</div>
{{#if search_enabled}}
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
{{/if}}
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
@@ -163,18 +176,6 @@
</div>
<!-- Local fallback for Font Awesome -->
<script>
if (getComputedStyle(document.querySelector(".fa")).fontFamily !== "FontAwesome") {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = '_FontAwesome/css/font-awesome.css';
document.head.insertBefore(link, document.head.firstChild)
}
</script>
{{#if livereload}}
<!-- Livereload script (if served using the cli tool) -->
<script type="text/javascript">
@@ -194,7 +195,7 @@
{{#if google_analytics}}
<!-- Google Analytics Tag -->
<script>
<script type="text/javascript">
var localAddrs = ["localhost", "127.0.0.1", ""];
// make sure we don't activate google analytics if the developer is
@@ -211,29 +212,49 @@
</script>
{{/if}}
{{#if playpens_editable}}
<script src="{{ ace_js }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ editor_js }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ mode_rust_js }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ theme_dawn_js }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ theme_tomorrow_night_js }}" type="text/javascript" charset="utf-8"></script>
{{#if playpen_js}}
<script src="ace.js" type="text/javascript" charset="utf-8"></script>
<script src="editor.js" type="text/javascript" charset="utf-8"></script>
<script src="mode-rust.js" type="text/javascript" charset="utf-8"></script>
<script src="theme-dawn.js" type="text/javascript" charset="utf-8"></script>
<script src="theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
{{#if is_print}}
<script>
document.addEventListener('DOMContentLoaded', function() {
window.print();
})
</script>
{{#if search_enabled}}
<script src="searchindex.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
{{#if search_js}}
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
<script src="highlight.js"></script>
<script src="book.js"></script>
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS script -->
<!-- Custom JS scripts -->
{{#each additional_js}}
<script type="text/javascript" src="{{this}}"></script>
{{/each}}
{{#if is_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}}
{{/if}}
</body>
</html>

View File

@@ -1,6 +1,10 @@
#![allow(missing_docs)] // FIXME: Document this
#![allow(missing_docs)]
pub mod playpen_editor;
#[cfg(feature = "search")]
pub mod searcher;
use std::path::Path;
use std::fs::File;
use std::io::Read;
@@ -17,22 +21,22 @@ 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
@@ -52,6 +56,8 @@ pub struct Theme {
}
impl Theme {
/// Creates a `Theme` from the given `theme_dir`.
/// If a file is found in the theme dir, it will override the default version.
pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
let theme_dir = theme_dir.as_ref();
let mut theme = Theme::default();
@@ -128,7 +134,7 @@ fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result
#[cfg(test)]
mod tests {
use super::*;
use tempdir::TempDir;
use tempfile::Builder as TempFileBuilder;
use std::path::PathBuf;
#[test]
@@ -153,7 +159,7 @@ mod tests {
.map(|f| f.path())
.filter(|p| p.is_file() && !p.ends_with(".rs"));
let temp = TempDir::new("mdbook").unwrap();
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
// "touch" all of the special files so we have empty copies
for special_file in special_files {

View File

@@ -1,3 +1,4 @@
"use strict";
window.editors = [];
(function(editors) {
if (typeof(ace) === 'undefined' || !ace) {

View File

@@ -1,70 +1,7 @@
use std::path::Path;
use theme::load_file_contents;
//! Theme dependencies for the playpen editor.
pub static JS: &'static [u8] = include_bytes!("editor.js");
pub static ACE_JS: &'static [u8] = include_bytes!("ace.js");
pub static MODE_RUST_JS: &'static [u8] = include_bytes!("mode-rust.js");
pub static THEME_DAWN_JS: &'static [u8] = include_bytes!("theme-dawn.js");
pub static THEME_TOMORROW_NIGHT_JS: &'static [u8] = include_bytes!("theme-tomorrow_night.js");
/// Integration of a JavaScript editor for playpens.
/// Uses the Ace editor: https://ace.c9.io/.
/// The Ace editor itself, the mode, and the theme files are the
/// generated minified no conflict versions.
///
/// The `PlaypenEditor` struct should be used instead of the static variables because
/// the `new()` method
/// will look if the user has an editor directory in his source folder and use
/// the users editor instead
/// of the default.
///
/// You should exceptionnaly use the static variables only if you need the
/// default editor even if the
/// user has specified another editor.
pub struct PlaypenEditor {
pub js: Vec<u8>,
pub ace_js: Vec<u8>,
pub mode_rust_js: Vec<u8>,
pub theme_dawn_js: Vec<u8>,
pub theme_tomorrow_night_js: Vec<u8>,
}
impl PlaypenEditor {
pub fn new(src: &Path) -> Self {
let mut editor = PlaypenEditor {
js: JS.to_owned(),
ace_js: ACE_JS.to_owned(),
mode_rust_js: MODE_RUST_JS.to_owned(),
theme_dawn_js: THEME_DAWN_JS.to_owned(),
theme_tomorrow_night_js: THEME_TOMORROW_NIGHT_JS.to_owned(),
};
// Check if the given path exists
if !src.exists() || !src.is_dir() {
return editor;
}
// Check for individual files if they exist
{
let files = vec![(src.join("editor.js"), &mut editor.js),
(src.join("ace.js"), &mut editor.ace_js),
(src.join("mode-rust.js"), &mut editor.mode_rust_js),
(src.join("theme-dawn.js"), &mut editor.theme_dawn_js),
(src.join("theme-tomorrow_night.js"),
&mut editor.theme_tomorrow_night_js)];
for (filename, dest) in files {
if !filename.exists() {
continue;
}
if let Err(e) = load_file_contents(&filename, dest) {
warn!("Couldn't load custom file, {}: {}", filename.display(), e);
}
}
}
editor
}
}

10
src/theme/searcher/elasticlunr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
src/theme/searcher/mark.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
//! Theme dependencies for in-browser search. Not included in mdbook when
//! the "search" cargo feature is disabled.
pub static JS: &'static [u8] = include_bytes!("searcher.js");
pub static MARK_JS: &'static [u8] = include_bytes!("mark.min.js");
pub static ELASTICLUNR_JS: &'static [u8] = include_bytes!("elasticlunr.min.js");

View File

@@ -0,0 +1,459 @@
"use strict";
window.search = window.search || {};
(function search(search) {
// Search functionality
//
// You can use !hasFocus() to prevent keyhandling in your key
// event handlers while the user is typing their search.
if (!Mark || !elasticlunr) {
return;
}
var search_wrap = document.getElementById('search-wrapper'),
searchbar = document.getElementById('searchbar'),
searchbar_outer = document.getElementById('searchbar-outer'),
searchresults = document.getElementById('searchresults'),
searchresults_outer = document.getElementById('searchresults-outer'),
searchresults_header = document.getElementById('searchresults-header'),
searchicon = document.getElementById('search-toggle'),
content = document.getElementById('content'),
searchindex = null,
resultsoptions = {
teaser_word_count: 30,
limit_results: 30,
},
searchoptions = {
bool: "AND",
expand: true,
fields: {
title: {boost: 1},
body: {boost: 1},
breadcrumbs: {boost: 0}
}
},
mark_exclude = [],
marker = new Mark(content),
current_searchterm = "",
URL_SEARCH_PARAM = 'search',
URL_MARK_PARAM = 'highlight',
teaser_count = 0,
SEARCH_HOTKEY_KEYCODE = 83,
ESCAPE_KEYCODE = 27,
DOWN_KEYCODE = 40,
UP_KEYCODE = 38,
SELECT_KEYCODE = 13;
function hasFocus() {
return searchbar === document.activeElement;
}
function removeChildren(elem) {
while (elem.firstChild) {
elem.removeChild(elem.firstChild);
}
}
// Helper to parse a url into its building blocks.
function parseURL(url) {
var a = document.createElement('a');
a.href = url;
return {
source: url,
protocol: a.protocol.replace(':',''),
host: a.hostname,
port: a.port,
params: (function(){
var ret = {};
var seg = a.search.replace(/^\?/,'').split('&');
var len = seg.length, i = 0, s;
for (;i<len;i++) {
if (!seg[i]) { continue; }
s = seg[i].split('=');
ret[s[0]] = s[1];
}
return ret;
})(),
file: (a.pathname.match(/\/([^/?#]+)$/i) || [,''])[1],
hash: a.hash.replace('#',''),
path: a.pathname.replace(/^([^/])/,'/$1')
};
}
// Helper to recreate a url string from its building blocks.
function renderURL(urlobject) {
var url = urlobject.protocol + "://" + urlobject.host;
if (urlobject.port != "") {
url += ":" + urlobject.port;
}
url += urlobject.path;
var joiner = "?";
for(var prop in urlobject.params) {
if(urlobject.params.hasOwnProperty(prop)) {
url += joiner + prop + "=" + urlobject.params[prop];
joiner = "&";
}
}
if (urlobject.hash != "") {
url += "#" + urlobject.hash;
}
return url;
}
// Helper to escape html special chars for displaying the teasers
var escapeHTML = (function() {
var MAP = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
"'": '&#39;'
};
var repl = function(c) { return MAP[c]; };
return function(s) {
return s.replace(/[&<>'"]/g, repl);
};
})();
function formatSearchMetric(count, searchterm) {
if (count == 1) {
return count + " search result for '" + searchterm + "':";
} else if (count == 0) {
return "No search results for '" + searchterm + "'.";
} else {
return count + " search results for '" + searchterm + "':";
}
}
function formatSearchResult(result, searchterms) {
var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
teaser_count++;
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
var url = result.ref.split("#");
if (url.length == 1) { // no anchor found
url.push("");
}
return '<a href="' + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
+ teaser + '</span>';
}
function makeTeaser(body, searchterms) {
// The strategy is as follows:
// First, assign a value to each word in the document:
// Words that correspond to search terms (stemmer aware): 40
// Normal words: 2
// First word in a sentence: 8
// Then use a sliding window with a constant number of words and count the
// sum of the values of the words within the window. Then use the window that got the
// maximum sum. If there are multiple maximas, then get the last one.
// Enclose the terms in <em>.
var stemmed_searchterms = searchterms.map(function(w) {
return elasticlunr.stemmer(w.toLowerCase());
});
var searchterm_weight = 40;
var weighted = []; // contains elements of ["word", weight, index_in_document]
// split in sentences, then words
var sentences = body.toLowerCase().split('. ');
var index = 0;
var value = 0;
var searchterm_found = false;
for (var sentenceindex in sentences) {
var words = sentences[sentenceindex].split(' ');
value = 8;
for (var wordindex in words) {
var word = words[wordindex];
if (word.length > 0) {
for (var searchtermindex in stemmed_searchterms) {
if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) {
value = searchterm_weight;
searchterm_found = true;
}
};
weighted.push([word, value, index]);
value = 2;
}
index += word.length;
index += 1; // ' ' or '.' if last word in sentence
};
index += 1; // because we split at a two-char boundary '. '
};
if (weighted.length == 0) {
return body;
}
var window_weight = [];
var window_size = Math.min(weighted.length, resultsoptions.teaser_word_count);
var cur_sum = 0;
for (var wordindex = 0; wordindex < window_size; wordindex++) {
cur_sum += weighted[wordindex][1];
};
window_weight.push(cur_sum);
for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
cur_sum -= weighted[wordindex][1];
cur_sum += weighted[wordindex + window_size][1];
window_weight.push(cur_sum);
};
if (searchterm_found) {
var max_sum = 0;
var max_sum_window_index = 0;
// backwards
for (var i = window_weight.length - 1; i >= 0; i--) {
if (window_weight[i] > max_sum) {
max_sum = window_weight[i];
max_sum_window_index = i;
}
};
} else {
max_sum_window_index = 0;
}
// add <em/> around searchterms
var teaser_split = [];
var index = weighted[max_sum_window_index][2];
for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) {
var word = weighted[i];
if (index < word[2]) {
// missing text from index to start of `word`
teaser_split.push(body.substring(index, word[2]));
index = word[2];
}
if (word[1] == searchterm_weight) {
teaser_split.push("<em>")
}
index = word[2] + word[0].length;
teaser_split.push(body.substring(word[2], index));
if (word[1] == searchterm_weight) {
teaser_split.push("</em>")
}
};
return teaser_split.join('');
}
function init() {
resultsoptions = window.search.resultsoptions;
searchoptions = window.search.searchoptions;
searchbar_outer = window.search.searchbar_outer;
searchindex = elasticlunr.Index.load(window.search.index);
// Set up events
searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false);
searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false);
document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false);
// If the user uses the browser buttons, do the same as if a reload happened
window.onpopstate = function(e) { doSearchOrMarkFromUrl(); };
// Suppress "submit" events so the page doesn't reload when the user presses Enter
document.addEventListener('submit', function(e) { e.preventDefault(); }, false);
// If reloaded, do the search or mark again, depending on the current url parameters
doSearchOrMarkFromUrl();
}
function unfocusSearchbar() {
// hacky, but just focusing a div only works once
var tmp = document.createElement('input');
tmp.setAttribute('style', 'position: absolute; opacity: 0;');
searchicon.appendChild(tmp);
tmp.focus();
tmp.remove();
}
// On reload or browser history backwards/forwards events, parse the url and do search or mark
function doSearchOrMarkFromUrl() {
// Check current URL for search request
var url = parseURL(window.location.href);
if (url.params.hasOwnProperty(URL_SEARCH_PARAM)
&& url.params[URL_SEARCH_PARAM] != "") {
showSearch(true);
searchbar.value = decodeURIComponent(
(url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20'));
searchbarKeyUpHandler(); // -> doSearch()
} else {
showSearch(false);
}
if (url.params.hasOwnProperty(URL_MARK_PARAM)) {
var words = url.params[URL_MARK_PARAM].split(' ');
marker.mark(words, {
exclude: mark_exclude
});
var markers = document.querySelectorAll("mark");
function hide() {
for (var i = 0; i < markers.length; i++) {
markers[i].classList.add("fade-out");
window.setTimeout(function(e) { marker.unmark(); }, 300);
}
}
for (var i = 0; i < markers.length; i++) {
markers[i].addEventListener('click', hide);
}
}
}
// Eventhandler for keyevents on `document`
function globalKeyHandler(e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea') { return; }
if (e.keyCode === ESCAPE_KEYCODE) {
e.preventDefault();
searchbar.classList.remove("active");
setSearchUrlParameters("",
(searchbar.value.trim() !== "") ? "push" : "replace");
if (hasFocus()) {
unfocusSearchbar();
}
showSearch(false);
marker.unmark();
} else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) {
e.preventDefault();
showSearch(true);
window.scrollTo(0, 0);
searchbar.select();
} else if (hasFocus() && e.keyCode === DOWN_KEYCODE) {
e.preventDefault();
unfocusSearchbar();
searchresults.firstElementChild.classList.add("focus");
} else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE
|| e.keyCode === UP_KEYCODE
|| e.keyCode === SELECT_KEYCODE)) {
// not `:focus` because browser does annoying scrolling
var focused = searchresults.querySelector("li.focus");
if (!focused) return;
e.preventDefault();
if (e.keyCode === DOWN_KEYCODE) {
var next = focused.nextElementSibling;
if (next) {
focused.classList.remove("focus");
next.classList.add("focus");
}
} else if (e.keyCode === UP_KEYCODE) {
focused.classList.remove("focus");
var prev = focused.previousElementSibling;
if (prev) {
prev.classList.add("focus");
} else {
searchbar.select();
}
} else { // SELECT_KEYCODE
window.location.assign(focused.querySelector('a'));
}
}
}
function showSearch(yes) {
if (yes) {
search_wrap.classList.remove('hidden');
searchicon.setAttribute('aria-expanded', 'true');
} else {
search_wrap.classList.add('hidden');
searchicon.setAttribute('aria-expanded', 'false');
var results = searchresults.children;
for (var i = 0; i < results.length; i++) {
results[i].classList.remove("focus");
}
}
}
function showResults(yes) {
if (yes) {
searchresults_outer.classList.remove('hidden');
} else {
searchresults_outer.classList.add('hidden');
}
}
// Eventhandler for search icon
function searchIconClickHandler() {
if (search_wrap.classList.contains('hidden')) {
showSearch(true);
window.scrollTo(0, 0);
searchbar.select();
} else {
showSearch(false);
}
}
// Eventhandler for keyevents while the searchbar is focused
function searchbarKeyUpHandler() {
var searchterm = searchbar.value.trim();
if (searchterm != "") {
searchbar.classList.add("active");
doSearch(searchterm);
} else {
searchbar.classList.remove("active");
showResults(false);
removeChildren(searchresults);
}
setSearchUrlParameters(searchterm, "push_if_new_search_else_replace");
// Remove marks
marker.unmark();
}
// Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor .
// `action` can be one of "push", "replace", "push_if_new_search_else_replace"
// and replaces or pushes a new browser history item.
// "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet.
function setSearchUrlParameters(searchterm, action) {
var url = parseURL(window.location.href);
var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM);
if (searchterm != "" || action == "push_if_new_search_else_replace") {
url.params[URL_SEARCH_PARAM] = searchterm;
delete url.params[URL_MARK_PARAM];
url.hash = "";
} else {
delete url.params[URL_SEARCH_PARAM];
}
// A new search will also add a new history item, so the user can go back
// to the page prior to searching. A updated search term will only replace
// the url.
if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) {
history.pushState({}, document.title, renderURL(url));
} else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) {
history.replaceState({}, document.title, renderURL(url));
}
}
function doSearch(searchterm) {
// Don't search the same twice
if (current_searchterm == searchterm) { return; }
else { current_searchterm = searchterm; }
if (searchindex == null) { return; }
// Do the actual search
var results = searchindex.search(searchterm, searchoptions);
var resultcount = Math.min(results.length, resultsoptions.limit_results);
// Display search metrics
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
// Clear and insert results
var searchterms = searchterm.split(' ');
removeChildren(searchresults);
for(var i = 0; i < resultcount ; i++){
var resultElem = document.createElement('li');
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
searchresults.appendChild(resultElem);
}
// Display results
showResults(true);
}
init();
// Exported functions
search.hasFocus = hasFocus;
})(window.search);

View File

@@ -9,3 +9,4 @@
@import 'themes'
@import 'print'
@import 'tooltip'
@import 'searchbar'

View File

@@ -35,6 +35,16 @@ h4, h5 { margin-top: 2em }
.header + .header h3, .header + .header h4, .header + .header h5 { margin-top: 1em }
a.header:target h1:before,
a.header:target h2:before,
a.header:target h3:before,
a.header:target h4:before {
display: inline-block;
content: "»";
margin-left: -30px;
width: 30px;
}
table {
margin: 0 auto;
border-collapse: collapse;

View File

@@ -7,7 +7,9 @@
& > #menu-bar-sticky-container {
display: flex
flex-wrap: wrap
transition: transform 0.5s, border-bottom-color 0.5s
.js & {
transition: transform 0.3s
}
}
i, .icon-button {
@@ -15,18 +17,23 @@
margin: 0 10px
z-index: 10
line-height: 50px
cursor: pointer
transition: color 0.5s
}
&:hover { cursor: pointer }
#print-button {
margin: 0 15px
}
}
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
transform: translateY(-60px);
}
.no-js .left-buttons {
display: none
}
.menu-title {
display: inline-block
font-weight: 200
@@ -38,5 +45,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
cursor: pointer;
.js & {
cursor: pointer
}
}

View File

@@ -4,16 +4,16 @@
box-sizing: border-box
// Animation: slide away
transition: padding-left 0.5s, margin-left 0.5s
.js & {
transition: margin-left 0.3s ease, transform 0.3s ease
}
}
.sidebar-visible .page-wrapper {
padding-left: $sidebar-width
}
transform: translateX($sidebar-width)
@media only screen and (max-width: $page-plus-sidebar-width - 1) {
.sidebar-visible .page-wrapper {
padding-left: 0
@media only screen and (min-width: $sidebar-reflow-width) {
transform: none
margin-left: $sidebar-width
}
}
@@ -24,11 +24,16 @@
}
.content {
margin-left: auto
margin-right: auto
max-width: $content-max-width
overflow-y: auto
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; }

View File

@@ -7,13 +7,10 @@
display: none
}
#page-wrapper {
left: 0;
overflow-y: initial;
}
#page-wrapper.page-wrapper {
padding-left: 0px;
transform: none;
margin-left: 0px;
overflow-y: initial;
}
#content {
@@ -46,15 +43,14 @@
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+ */
white-space: pre-wrap
}
.fa {
display: none !important
}
}

View File

@@ -0,0 +1,66 @@
@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;
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: $content-max-width;
}
#searchbar {
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 {
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;
}
}

View File

@@ -14,7 +14,9 @@
overscroll-behavior-y: contain;
// Animation: slide away
transition: transform 0.5s
.js & {
transition: transform 0.3s
}
code {
line-height: 2em;

View File

@@ -29,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
$searchbar-border-color = #848484
$searchbar-bg = #424242
$searchbar-fg = #fff
$searchbar-shadow-color = #d4c89f
$searchresults-header-fg = #666
$searchresults-border-color = #888
$searchresults-li-bg = #252932
$search-mark-bg = #e3b171
@import 'base'

View File

@@ -5,7 +5,6 @@
.content .header:link, .content .header:visited {
color: $fg;
pointer: cursor;
&:hover {
text-decoration: none;
@@ -84,7 +83,10 @@
background-color: $sidebar-bg
}
.content a:link, a:visited, a > .hljs {
#searchresults a,
.content a:link,
a:visited,
a > .hljs {
color: $links
}
@@ -188,4 +190,32 @@
::-webkit-scrollbar-thumb {
background: $scrollbar;
}
/* Search */
#searchbar {
border: 1px solid $searchbar-border-color;
border-radius: 3px;
background-color: $searchbar-bg;
color: $searchbar-fg
&:focus, &.active {
box-shadow: 0 0 3px $searchbar-shadow-color;
}
}
.searchresults-header {
color: $searchresults-header-fg;
}
.searchresults-outer {
border-bottom: 1px dashed $searchresults-border-color;
}
ul#searchresults li.focus {
background-color: $searchresults-li-bg;
}
mark {
background-color: $search-mark-bg;
}
}

View File

@@ -29,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
$searchbar-border-color = #aaa
$searchbar-bg = #b7b7b7
$searchbar-fg = #000
$searchbar-shadow-color = #aaa
$searchresults-header-fg = #666
$searchresults-border-color = #98a3ad
$searchresults-li-bg = #2b2b2f
$search-mark-bg = #355c7d
@import 'base'

View File

@@ -16,7 +16,7 @@ $icons-hover = #333333
$links = #4183c4
$inline-code-color = #6e6b5e;
$inline-code-color = #6e6b5e
$theme-popup-bg = #fafafa
$theme-popup-border = #cccccc
@@ -29,4 +29,13 @@ $table-border-color = darken($bg, 5%)
$table-header-bg = darken($bg, 20%)
$table-alternate-bg = darken($bg, 3%)
$searchbar-border-color = #aaa
$searchbar-bg = #fafafa
$searchbar-fg = #000
$searchbar-shadow-color = #aaa
$searchresults-header-fg = #666
$searchresults-border-color = #888
$searchresults-li-bg = #e4f2fe
$search-mark-bg = #a2cff5
@import 'base'

View File

@@ -29,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
$searchbar-border-color = #aaa
$searchbar-bg = #aeaec6
$searchbar-fg = #000
$searchbar-shadow-color = #aaa
$searchresults-header-fg = #5f5f71
$searchresults-border-color = #5c5c68
$searchresults-li-bg = #242430
$search-mark-bg = #a2cff5
@import 'base'

View File

@@ -29,4 +29,13 @@ $table-border-color = darken($bg, 5%)
$table-header-bg = #b3a497
$table-alternate-bg = darken($bg, 3%)
$searchbar-border-color = #aaa
$searchbar-bg = #fafafa
$searchbar-fg = #000
$searchbar-shadow-color = #aaa
$searchresults-header-fg = #666
$searchresults-border-color = #888
$searchresults-li-bg = #dec2a2
$search-mark-bg = #e69f67
@import 'base'

View File

@@ -1,4 +1,6 @@
$sidebar-width = 300px
$page-padding = 15px
$content-max-width = 750px
$content-min-width = 320px
$page-plus-sidebar-width = $content-max-width + $sidebar-width + $page-padding * 2
$sidebar-reflow-width = $sidebar-width + $content-min-width

View File

@@ -1,7 +1,7 @@
use std::path::{Component, Path, PathBuf};
use errors::*;
use std::io::Read;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Component, Path, PathBuf};
/// Takes a path to a file and try to read the file into a String
pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
@@ -16,6 +16,27 @@ pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
Ok(content)
}
/// Naively replaces any path seperator with a forward-slash '/'
pub fn normalize_path(path: &str) -> String {
use std::path::is_separator;
path.chars()
.map(|ch| if is_separator(ch) { '/' } else { ch })
.collect::<String>()
}
/// Write the given data to a file, creating it first if necessary
pub fn write_file<P: AsRef<Path>>(
build_dir: &Path,
filename: P,
content: &[u8],
) -> Result<()> {
let path = build_dir.join(filename);
create_file(&path)?
.write_all(content)
.map_err(|e| e.into())
}
/// Takes a path and returns a path containing just enough `../` to point to
/// the root of the given path.
///
@@ -38,7 +59,6 @@ pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
/// it doesn't return the correct path.
/// Consider [submitting a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
/// or a [pull-request](https://github.com/rust-lang-nursery/mdBook/pulls) to improve it.
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
debug!("path_to_root");
// Remove filename and add "../" for every directory
@@ -61,7 +81,6 @@ pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
/// This function creates a file and returns it. But before creating the file
/// it checks every directory in the path to see if it exists,
/// and if it does not it will be created.
pub fn create_file(path: &Path) -> Result<File> {
debug!("Creating {}", path.display());
@@ -76,7 +95,6 @@ pub fn create_file(path: &Path) -> Result<File> {
}
/// Removes all the content of a directory but not the directory itself
pub fn remove_dir_content(dir: &Path) -> Result<()> {
for item in fs::read_dir(dir)? {
if let Ok(item) = item {
@@ -93,7 +111,6 @@ pub fn remove_dir_content(dir: &Path) -> Result<()> {
/// Copies all files of a directory to another one except the files
/// with the extensions given in the `ext_blacklist` array
pub fn copy_files_except_ext(
from: &Path,
to: &Path,
@@ -176,14 +193,14 @@ pub fn copy_files_except_ext(
#[cfg(test)]
mod tests {
extern crate tempdir;
extern crate tempfile;
use super::copy_files_except_ext;
use std::fs;
#[test]
fn copy_files_except_ext_test() {
let tmp = match tempdir::TempDir::new("") {
let tmp = match tempfile::TempDir::new() {
Ok(t) => t,
Err(_) => panic!("Could not create a temp dir"),
};

View File

@@ -3,13 +3,71 @@
pub mod fs;
mod string;
use errors::Error;
use regex::Regex;
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
OPTION_ENABLE_TABLES};
use std::borrow::Cow;
pub use self::string::{RangeArgument, take_lines};
/// Replaces multiple consecutive whitespace characters with a single space character.
pub fn collapse_whitespace<'a>(text: &'a str) -> Cow<'a, str> {
lazy_static! {
static ref RE: Regex = Regex::new(r"\s\s+").unwrap();
}
RE.replace_all(text, " ")
}
/// Convert the given string to a valid HTML element ID
pub fn normalize_id(content: &str) -> String {
let mut ret = content
.chars()
.filter_map(|ch| {
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
Some(ch.to_ascii_lowercase())
} else if ch.is_whitespace() {
Some('-')
} else {
None
}
})
.collect::<String>();
// Ensure that the first character is [A-Za-z]
if ret.chars().next().map_or(false, |c| !c.is_ascii_alphabetic()) {
ret.insert(0, 'a');
}
ret
}
/// Generate an ID for use with anchors which is derived from a "normalised"
/// string.
pub fn id_from_content(content: &str) -> String {
let mut content = content.to_string();
// Skip any tags or html-encoded stuff
const REPL_SUB: &[&str] = &["<em>",
"</em>",
"<code>",
"</code>",
"<strong>",
"</strong>",
"&lt;",
"&gt;",
"&amp;",
"&#39;",
"&quot;"];
for sub in REPL_SUB {
content = content.replace(sub, "");
}
// Remove spaces and hashes indicating a header
let trimmed = content.trim().trim_left_matches('#').trim();
normalize_id(trimmed)
}
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2);
@@ -212,6 +270,29 @@ more text with spaces
}
}
mod html_munging {
use super::super::{id_from_content, normalize_id};
#[test]
fn it_generates_anchors() {
assert_eq!(id_from_content("## `--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes");
assert_eq!(id_from_content("## Method-call expressions"),
"method-call-expressions");
}
#[test]
fn it_normalizes_ids() {
assert_eq!(normalize_id("`--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes");
assert_eq!(normalize_id("Method-call 🐙 expressions \u{1f47c}"),
"method-call--expressions-");
assert_eq!(normalize_id("_-_12345"), "a_-_12345");
assert_eq!(normalize_id("12345"), "a12345");
assert_eq!(normalize_id(""), "");
}
}
mod convert_quotes_to_curly {
use super::super::convert_quotes_to_curly;

View File

@@ -50,7 +50,7 @@ pub fn take_lines<R: RangeArgument<usize>>(s: &str, range: R) -> String {
let start = *range.start().unwrap_or(&0);
let mut lines = s.lines().skip(start);
match range.end() {
Some(&end) => lines.take(end.checked_sub(start).unwrap_or(0)).join("\n"),
Some(&end) => lines.take(end.saturating_sub(start)).join("\n"),
None => lines.join("\n"),
}
}

View File

@@ -1,14 +1,13 @@
//! Integration tests to make sure alternate backends work.
extern crate mdbook;
extern crate tempdir;
extern crate tempfile;
use std::fs::File;
#[cfg(not(windows))]
use std::path::Path;
use tempdir::TempDir;
use tempfile::{TempDir, Builder as TempFileBuilder};
use mdbook::config::Config;
use mdbook::MDBook;
use mdbook::renderer::RenderContext;
#[test]
fn passing_alternate_backend() {
@@ -39,6 +38,7 @@ fn alternate_backend_with_arguments() {
}
/// Get a command which will pipe `stdin` to the provided file.
#[cfg(not(windows))]
fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
let out_file = out_file.as_ref();
@@ -52,7 +52,10 @@ fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
#[test]
#[cfg(not(windows))]
fn backends_receive_render_context_via_stdin() {
let temp = TempDir::new("output").unwrap();
use std::fs::File;
use mdbook::renderer::RenderContext;
let temp = TempFileBuilder::new().prefix("output").tempdir().unwrap();
let out_file = temp.path().join("out.txt");
let cmd = tee_command(&out_file);
@@ -67,7 +70,7 @@ fn backends_receive_render_context_via_stdin() {
}
fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) {
let temp = TempDir::new("mdbook").unwrap();
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let mut config = Config::default();
config

View File

@@ -4,7 +4,7 @@
// Not all features are used in all test crates, so...
#![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
extern crate mdbook;
extern crate tempdir;
extern crate tempfile;
extern crate walkdir;
use std::path::Path;
@@ -15,7 +15,7 @@ use mdbook::utils::fs::file_to_string;
// The funny `self::` here is because we've got an `extern crate ...` and are
// in a submodule
use self::tempdir::TempDir;
use self::tempfile::{TempDir, Builder as TempFileBuilder};
use self::mdbook::MDBook;
use self::walkdir::WalkDir;
@@ -47,7 +47,7 @@ impl DummyBook {
/// Write a book to a temporary directory using the provided settings.
pub fn build(&self) -> Result<TempDir> {
let temp = TempDir::new("dummy_book").chain_err(|| "Unable to create temp directory")?;
let temp = TempFileBuilder::new().prefix("dummy_book").tempdir().chain_err(|| "Unable to create temp directory")?;
let dummy_book_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/dummy_book");
recursive_copy(&dummy_book_root, temp.path()).chain_err(|| {
@@ -128,11 +128,11 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
}
pub fn new_copy_of_example_book() -> Result<TempDir> {
let temp = TempDir::new("book-example")?;
let temp = TempFileBuilder::new().prefix("book-example").tempdir()?;
let book_example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
recursive_copy(book_example, temp.path())?;
Ok(temp)
}
}

View File

@@ -5,6 +5,7 @@
- [First Chapter](first/index.md)
- [Nested Chapter](first/nested.md)
- [Includes](first/includes.md)
- [Recursive](first/recursive.md)
- [Second Chapter](second.md)
---

View File

@@ -1 +1,20 @@
# Conclusion
# Conclusion
<p>
<!--secret secret-->
I put &lt;HTML&gt; in here!<br/>
</p>
<script type="text/javascript" >
// I probably shouldn't do this
if (3 < 5 > 10)
{
alert("The sky is falling!");
}
</script >
<style >
/*
css looks, like this {
foo: < 3 <bar >
}
*/
</style>

View File

@@ -0,0 +1,2 @@
Around the world, around the world
{{#include recursive.md}}

View File

@@ -0,0 +1 @@
# Root README

View File

@@ -0,0 +1,7 @@
# This dummy book is for testing the conversion of README.md to index.html by IndexPreprocessor
[Root README](README.md)
- [1st README](first/README.md)
- [2nd README](second/README.md)
- [2nd index](second/index.md)

View File

@@ -0,0 +1 @@
# First README

View File

@@ -0,0 +1 @@
# Second README

View File

@@ -0,0 +1 @@
# Second index

View File

@@ -1,11 +1,11 @@
extern crate mdbook;
extern crate tempdir;
extern crate tempfile;
use std::path::PathBuf;
use std::fs;
use mdbook::MDBook;
use mdbook::config::Config;
use tempdir::TempDir;
use tempfile::Builder as TempFileBuilder;
/// Run `mdbook init` in an empty directory and make sure the default files
@@ -14,7 +14,7 @@ use tempdir::TempDir;
fn base_mdbook_init_should_create_default_content() {
let created_files = vec!["book", "src", "src/SUMMARY.md", "src/chapter_1.md"];
let temp = TempDir::new("mdbook").unwrap();
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
for file in &created_files {
assert!(!temp.path().join(file).exists());
}
@@ -34,7 +34,7 @@ fn base_mdbook_init_should_create_default_content() {
fn run_mdbook_init_with_custom_book_and_src_locations() {
let created_files = vec!["out", "in", "in/SUMMARY.md", "in/chapter_1.md"];
let temp = TempDir::new("mdbook").unwrap();
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
for file in &created_files {
assert!(
!temp.path().join(file).exists(),
@@ -61,7 +61,7 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
#[test]
fn book_toml_isnt_required() {
let temp = TempDir::new("mdbook").unwrap();
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let md = MDBook::init(temp.path()).build().unwrap();
let _ = fs::remove_file(temp.path().join("book.toml"));

View File

@@ -2,7 +2,7 @@ extern crate mdbook;
#[macro_use]
extern crate pretty_assertions;
extern crate select;
extern crate tempdir;
extern crate tempfile;
extern crate walkdir;
mod dummy_book;
@@ -16,9 +16,9 @@ use std::ffi::OsStr;
use walkdir::{DirEntry, WalkDir};
use select::document::Document;
use select::predicate::{Class, Name, Predicate};
use tempdir::TempDir;
use tempfile::Builder as TempFileBuilder;
use mdbook::errors::*;
use mdbook::utils::fs::file_to_string;
use mdbook::utils::fs::{file_to_string, write_file};
use mdbook::config::Config;
use mdbook::MDBook;
@@ -29,7 +29,7 @@ const TOC_TOP_LEVEL: &[&'static str] = &[
"Conclusion",
"Introduction",
];
const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter", "1.2. Includes"];
const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter", "1.2. Includes", "1.3. Recursive"];
/// Make sure you can load the dummy book and build it without panicking.
#[test]
@@ -313,6 +313,20 @@ fn able_to_include_files_in_chapters() {
assert_doesnt_contain_strings(&includes, &["{{#include ../SUMMARY.md::}}"]);
}
/// Ensure cyclic includes are capped so that no exceptions occur
#[test]
fn recursive_includes_are_capped() {
let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap();
md.build().unwrap();
let recursive = temp.path().join("book/first/recursive.html");
let content = &["Around the world, around the world
Around the world, around the world
Around the world, around the world"];
assert_contains_strings(&recursive, content);
}
#[test]
fn example_book_can_build() {
let example_book_dir = dummy_book::new_copy_of_example_book().unwrap();
@@ -324,7 +338,7 @@ fn example_book_can_build() {
#[test]
fn book_with_a_reserved_filename_does_not_build() {
let tmp_dir = TempDir::new("mdBook").unwrap();
let tmp_dir = TempFileBuilder::new().prefix("mdBook").tempdir().unwrap();
let src_path = tmp_dir.path().join("src");
fs::create_dir(&src_path).unwrap();
@@ -339,3 +353,151 @@ fn book_with_a_reserved_filename_does_not_build() {
let got = md.build();
assert!(got.is_err());
}
#[test]
fn by_default_mdbook_use_index_preprocessor_to_convert_readme_to_index() {
let temp = DummyBook::new().build().unwrap();
let mut cfg = Config::default();
cfg.set("book.src", "src2").expect("Couldn't set config.book.src to \"src2\".");
let md = MDBook::load_with_config(temp.path(), cfg).unwrap();
md.build().unwrap();
let first_index = temp.path()
.join("book")
.join("first")
.join("index.html");
let expected_strings = vec![
r#"href="first/index.html""#,
r#"href="second/index.html""#,
"First README",
];
assert_contains_strings(&first_index, &expected_strings);
assert_doesnt_contain_strings(&first_index, &vec!["README.html"]);
let second_index = temp.path()
.join("book")
.join("second")
.join("index.html");
let unexpected_strings = vec![
"Second README",
];
assert_doesnt_contain_strings(&second_index, &unexpected_strings);
}
#[test]
fn theme_dir_overrides_work_correctly() {
let book_dir = dummy_book::new_copy_of_example_book().unwrap();
let book_dir = book_dir.path();
let theme_dir = book_dir.join("theme");
let mut index = ::mdbook::theme::INDEX.to_vec();
index.extend_from_slice(b"\n<!-- This is a modified index.hbs! -->");
write_file(&theme_dir, "index.hbs", &index).unwrap();
let md = MDBook::load(book_dir).unwrap();
md.build().unwrap();
let built_index = book_dir.join("book").join("index.html");
dummy_book::assert_contains_strings(built_index, &["This is a modified index.hbs!"]);
}
#[cfg(feature = "search")]
mod search {
extern crate serde_json;
use std::fs::File;
use std::path::Path;
use mdbook::utils::fs::file_to_string;
use mdbook::MDBook;
use dummy_book::DummyBook;
fn read_book_index(root: &Path) -> serde_json::Value {
let index = root.join("book/searchindex.js");
let index = file_to_string(index).unwrap();
let index = index.trim_left_matches("window.search = ");
let index = index.trim_right_matches(";");
serde_json::from_str(&index).unwrap()
}
#[test]
fn book_creates_reasonable_search_index() {
let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap();
md.build().unwrap();
let index = read_book_index(temp.path());
let bodyidx = &index["index"]["index"]["body"]["root"];
let textidx = &bodyidx["t"]["e"]["x"]["t"];
assert_eq!(textidx["df"], 2);
assert_eq!(textidx["docs"]["first/index.html#first-chapter"]["tf"], 1.0);
assert_eq!(textidx["docs"]["intro.html#introduction"]["tf"], 1.0);
let docs = &index["index"]["documentStore"]["docs"];
assert_eq!(docs["first/index.html#first-chapter"]["body"], "more text.");
assert_eq!(docs["first/index.html#some-section"]["body"], "");
assert_eq!(
docs["first/includes.html#summary"]["body"],
"Introduction First Chapter Nested Chapter Includes Recursive Second Chapter Conclusion"
);
assert_eq!(
docs["first/includes.html#summary"]["breadcrumbs"],
"First Chapter » Summary"
);
assert_eq!(
docs["conclusion.html#conclusion"]["body"],
"I put &lt;HTML&gt; in here!"
);
}
// Setting this to `true` may cause issues with `cargo watch`,
// since it may not finish writing the fixture before the tests
// are run again.
const GENERATE_FIXTURE: bool = true;
fn get_fixture() -> serde_json::Value {
if GENERATE_FIXTURE {
let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap();
md.build().unwrap();
let src = read_book_index(temp.path());
let dest = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/searchindex_fixture.json");
let dest = File::create(&dest).unwrap();
serde_json::to_writer_pretty(dest, &src).unwrap();
src
} else {
let json = include_str!("searchindex_fixture.json");
serde_json::from_str(json).expect("Unable to deserialize the fixture")
}
}
// So you've broken the test. If you changed dummy_book, it's probably
// safe to regenerate the fixture. If you haven't then make sure that the
// search index still works. Run `cargo run -- serve tests/dummy_book`
// and try some searches. Are you getting results? Do the teasers look OK?
// Are there new errors in the JS console?
//
// If you're pretty sure you haven't broken anything, change `GENERATE_FIXTURE`
// above to `true`, and run `cargo test` to generate a new fixture. Then
// change it back to `false`. Include the changed `searchindex_fixture.json` in your commit.
#[test]
fn search_index_hasnt_changed_accidentally() {
let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap();
md.build().unwrap();
let book_index = read_book_index(temp.path());
let fixture_index = get_fixture();
// Uncomment this if you're okay with pretty-printing 32KB of JSON
//assert_eq!(fixture_index, book_index);
if book_index != fixture_index {
panic!("The search index has changed from the fixture");
}
}
}

File diff suppressed because it is too large Load Diff