Compare commits

...

87 Commits

Author SHA1 Message Date
Matt Ickstadt
2bbabdcd62 (cargo-release) version 0.1.9 2018-07-26 12:44:29 -05:00
Matt Ickstadt
a287a0dcc8 Merge pull request #742 from mattico/narrow-device-style-fixes
Reduce the margins on header icons on narrow devices
2018-07-25 13:41:33 -05:00
Matt Ickstadt
e7afb3340c Reduce the margins on header icons on narrow devices 2018-07-25 13:28:05 -05:00
Matt Ickstadt
b4e15e5357 Merge pull request #741 from mattico/fix-mdbook-test
Fix `mdbook test`
2018-07-25 12:56:28 -05:00
Matt Ickstadt
1d1d4d7c30 Update tests to handle added dummy_book chapter 2018-07-25 12:45:20 -05:00
Matt Ickstadt
35c2d1ff91 Merge pull request #740 from mattico/fix-ios-text-size
Prevent certain mobile browsers from enlarging fonts in landscape ori…
2018-07-25 12:27:16 -05:00
Matt Ickstadt
fd9d27e082 rustfmt 2018-07-25 12:20:48 -05:00
Matt Ickstadt
0e1787c617 Don't run index preprocessor on mdbook test 2018-07-25 12:19:01 -05:00
Matt Ickstadt
e5563182fc Add readme to cause test to fail
The test for `mdbook test` fails due to the index preprocessor which only runs on README files.
2018-07-25 12:14:27 -05:00
Matt Ickstadt
a08255316a Prevent certain mobile browsers from enlarging fonts in landscape orientation 2018-07-25 11:25:09 -05:00
Matt Ickstadt
ebea8c2337 Fix travis cache configuration 2018-07-24 17:29:44 -05:00
Matt Ickstadt
a7342230d5 Merge pull request #739 from mattico/travis-cache-timeout
Increase the travis-ci cache timeout
2018-07-24 17:25:36 -05:00
Matt Ickstadt
15b18a682b Increase the travis-ci cache timeout 2018-07-24 17:23:51 -05:00
Matt Ickstadt
a8590928cf Update env_logger version 2018-07-24 16:55:02 -05:00
Matt Ickstadt
c9a9987aec Merge pull request #738 from mattico/fix-cargo-bin
Move subcommand modules to match cargo conventions
2018-07-24 16:46:57 -05:00
Matt Ickstadt
775377acce Merge pull request #737 from mattico/update-deps-easy
Update dependencies
2018-07-24 16:43:14 -05:00
Matt Ickstadt
5dd0496a4f Update dependencies
`crossbeam` and `time` are removed since they're no longer used.
2018-07-24 16:40:34 -05:00
Matt Ickstadt
f300a21a47 Move subcommand modules to match cargo conventions 2018-07-24 16:34:49 -05:00
Matt Ickstadt
0f89abb1c7 Merge pull request #735 from mattico/fix-travis-deploy
Fix travis deploy script
2018-07-24 14:13:39 -05:00
Matt Ickstadt
b5e32f57dc Fix travis deploy script 2018-07-24 14:12:27 -05:00
Matt Ickstadt
b88abb171c Merge pull request #710 from Eyenseo/master
Fix different font sizes in editor and code
2018-07-24 13:34:34 -05:00
Matt Ickstadt
7c8dd5085b Merge pull request #732 from mattico/sidebar-overflow
Allow sidebar section titles to wrap
2018-07-24 12:11:46 -05:00
Matt Ickstadt
4f793af53b Allow sidebar section titles to overflow 2018-07-24 12:00:48 -05:00
Matt Ickstadt
29b3ff14c7 Merge pull request #719 from lucasem/patch-1
rustdoc codeblock hash escape
2018-07-24 11:09:28 -05:00
Matt Ickstadt
0ac36f2183 Merge pull request #690 from mattico/rustfmt
Remove rustfmt.toml
2018-07-23 12:47:33 -05:00
Matt Ickstadt
5835da2432 Run rustfmt 2018-07-23 12:47:04 -05:00
Matt Ickstadt
646e3f8fd2 Remove rustfmt.toml 2018-07-23 12:39:18 -05:00
Matt Ickstadt
da9be67516 Merge pull request #707 from mattico/search-index-opt
Optimize search index
2018-07-23 12:32:05 -05:00
Matt Ickstadt
d9dbba49ea Fix for relative paths 2018-07-23 12:19:59 -05:00
Matt Ickstadt
384582aeba Cleanup add_doc 2018-07-23 12:08:04 -05:00
Matt Ickstadt
e94078cc9c Fix tests 2018-07-23 12:08:04 -05:00
Matt Ickstadt
e1a46d213e Use JSON search index with JS fallback
This allows the search index to be loaded asynchronously, and should
use fewer resources as it doesn't have to execute the JS.
JS loading is kept as a fallback for CORS issues with file:// URIs in Chrome.
2018-07-23 12:08:04 -05:00
Matt Ickstadt
62c8311301 Don't copy search js when disabled 2018-07-23 12:08:04 -05:00
Matt Ickstadt
b8011de3e8 Warn when search index is >10MB 2018-07-23 12:08:04 -05:00
Matt Ickstadt
019e74041d Use integer doc_refs to shrink the search index
This change reduced the searchindex.js of book_example from 508KB to 317KB.
2018-07-23 12:08:04 -05:00
Matt Ickstadt
8cd7061ff2 Add search.enable config field 2018-07-23 12:08:04 -05:00
Matt Ickstadt
96b99472fd Merge pull request #709 from mattico/no-playpen-no-fetch
Only fetch crates list on pages with playpens
2018-07-23 12:06:02 -05:00
Matt Ickstadt
4d357b6779 Merge pull request #728 from kggp1995/master
- Fixed token type.
2018-07-23 12:01:35 -05:00
Corey Farwell
1e6328c112 Merge pull request #727 from waywardmonkeys/update-toml-query
Update to toml-query 0.7
2018-07-22 17:26:16 -04:00
Ganesh Prasad Kumble
21c24c2815 - Fix token type
OAuth token type doesn't work. Have to change it to PAT.
2018-07-21 19:23:47 +05:30
Corey Farwell
bb8b43d396 Merge pull request #726 from waywardmonkeys/update-elasticlunr
Update to elasticlunr 2.3.
2018-07-19 06:38:58 -04:00
Bruce Mitchener
f07e734efc Update to toml-query 0.7
This was just released with an update to its dependencies, so that
mdBook could update its dependencies.
2018-07-19 17:35:34 +07:00
Bruce Mitchener
db2c16102e Update to elasticlunr 2.3.
This removes one of the remaining dependencies upon pre-1.0 regex.
2018-07-19 11:11:54 +07:00
Matt Ickstadt
cae8a8ffe2 Only fetch crates list on pages with playpens 2018-07-16 19:17:00 -05:00
cetra3
bdb37ec117 Use relative links and translate internal references (#603)
* Relative links for 0.1.8

* Compat for IE11 search
2018-07-11 21:33:44 +08:00
Michael Bryan
01656b610f (cargo-release) start next development iteration 0.1.9-alpha.0 2018-07-09 18:40:43 +08:00
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
Lucas Morales
374e1d3f94 rustdoc codeblock hash escape
pending merge of rust-lang/rust#51803
2018-07-04 16:54:55 +04: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
eyenseo
b452d5e0c7 Fix different font sizes in editor and code
Fixes #705
2018-06-19 22:28:23 +02: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
82 changed files with 2430 additions and 1576 deletions

View File

@@ -1,87 +1,47 @@
# Based on the "trust" template v0.1.1
# https://github.com/japaric/trust/tree/v0.1.1
dist: trusty
language: rust
services: docker
sudo: required
cache: cargo
cache:
timeout: 360
cargo: true
before_cache:
- chmod -R a+r $HOME/.cargo
- chmod -R a+r $HOME/.cargo
env:
global:
- CRATE_NAME=mdbook
- secure: DPzSRXyfRIVTibv1wOKFeGekXlL8sumGEZxpeq911MpLlrndOKmOo5Ibi3JD8fbUOsE9A/5spj4B2KQNjhbplH+Cp26oEikjuNAA6cA/b2+/TMoC3i0klAYpVopBBV3FFna0gLP+q6t6fzG2v9TJrvmmVav6KVX6ylPNvD/LoReCjrkpgLIQuAQ6dSQNor9uV+EVt4plKhhkiS28DlYdgmTvNb5g4dzOhs8hoWty72J765VYWEDDC8qXn6N9GyrhsC3dhjASGn+1QDSCADYdbG9nrRlb4CZhrfcgOnHhAFva363kshg9HtCphigMgQy2oZXk4nLWK90/HuaPPkVj+N/lpIYjtiHOunToZJfIb0MWzyVI+7+I7WR6n6XbhLCPMe/sPXHHQ3HhQhZZ9xv7CDx9IkYJQBcF3LC+9kzJRi4QT0UTqrxcO3ncgXwvholP8Vg2KKPqFcbuyLPzbvr/o8zIilvLUFAEoDPfTEwSAC4BCzaGkFQVWzhWkgw8Pe1ckOEYFkZ0VLBuCpEiz+x45sbBL1SnnO5xhpjmdc572ZyW7ZmAABw1VfiWhhBWg4WGSf8lLnDHhNA36Qon34pnME/xpJQtWoo7ZZkkzvzYP/oW88/0UIMWDSOYKz7MijXlbNUggwAwUhrLzXDuB71HUKfPreFubfUxbOpu+OtTcOQ=
matrix:
include:
# Android
- env: TARGET=arm-linux-androideabi DISABLE_TESTS=1
# Linux
- env: TARGET=aarch64-unknown-linux-gnu
- env: TARGET=arm-unknown-linux-gnueabi
- env: TARGET=i686-unknown-linux-gnu
- env: TARGET=x86_64-unknown-linux-gnu
- env: TARGET=x86_64-unknown-linux-musl
# Mac
- env: TARGET=i686-apple-darwin
os: osx
- env: TARGET=x86_64-apple-darwin
os: osx
# BSD
- env: TARGET=i686-unknown-freebsd DISABLE_TESTS=1
- env: TARGET=x86_64-unknown-freebsd DISABLE_TESTS=1
- env: TARGET=x86_64-unknown-netbsd DISABLE_TESTS=1
# Other channels
- env: TARGET=x86_64-unknown-linux-gnu
rust: beta
- env: TARGET=x86_64-apple-darwin
os: osx
rust: beta
- env: TARGET=x86_64-unknown-linux-gnu
rust: nightly
- env: TARGET=x86_64-apple-darwin
os: osx
rust: nightly
before_install:
- set -e
- rustup self update
- CRATE_NAME=mdbook
- TARGET=x86_64-unknown-linux-gnu
install:
- sh ci/install.sh
- source ~/.cargo/env || true
- sh ci/install.sh
- export PATH=$PATH:$HOME/.cargo/bin
script:
- bash ci/script.sh
- 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:

760
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "mdbook"
version = "0.1.6"
version = "0.1.9"
authors = ["Mathieu David <mathieudavid@mathieudavid.org>", "Michael-F-Bryan <michaelfbryan@gmail.com>"]
description = "Create books from markdown files"
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
@@ -14,36 +14,29 @@ exclude = [
"src/theme/stylus/**",
]
[package.metadata.release]
sign-commit = true
push-remote = "origin"
tag-prefix = "v"
[dependencies]
clap = "2.24"
chrono = "0.4"
handlebars = "0.32"
serde = "1.0"
serde_derive = "1.0"
error-chain = "0.11"
error-chain = "0.12"
serde_json = "1.0"
pulldown-cmark = "0.1.2"
lazy_static = "1.0"
log = "0.4"
env_logger = "0.5.0-rc.1"
env_logger = "0.5"
toml = "0.4"
memchr = "2.0"
open = "1.1"
regex = "0.2.1"
regex = "1.0.0"
tempfile = "3.0"
itertools = "0.7"
shlex = "0.1"
toml-query = "0.6"
toml-query = "0.7"
# Watch feature
notify = { version = "4.0", optional = true }
time = { version = "0.1.34", optional = true }
crossbeam = { version = "0.3", optional = true }
# Serve feature
iron = { version = "0.6", optional = true }
@@ -51,11 +44,11 @@ staticfile = { version = "0.5", optional = true }
ws = { version = "0.7", optional = true}
# Search feature
elasticlunr-rs = { version = "2.0", optional = true }
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
ammonia = { version = "1.1", optional = true }
[build-dependencies]
error-chain = "0.11"
error-chain = "0.12"
[dev-dependencies]
select = "0.4"
@@ -68,11 +61,10 @@ default = ["output", "watch", "serve", "search"]
debug = []
output = []
regenerate-css = []
watch = ["notify", "time", "crossbeam"]
watch = ["notify"]
serve = ["iron", "staticfile", "ws"]
search = ["elasticlunr-rs", "ammonia"]
[[bin]]
doc = false
name = "mdbook"
path = "src/bin/mdbook.rs"

View File

@@ -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:
```

View File

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

View File

@@ -22,7 +22,7 @@ configuration files, etc.
- The `book` directory is where your book is rendered. All the output is ready to be uploaded
to a server to be seen by your audience.
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is discussed in more detail in another [chapter](format/summary.html).
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is discussed in more detail in another [chapter](../format/summary.md)
#### Tip & Trick: Hidden Feature
When a `SUMMARY.md` file already exists, the `init` command will first parse it and generate the missing files according to the paths used in the `SUMMARY.md`. This allows you to think and create the whole structure of your book and then let mdBook generate it for you.

View File

@@ -0,0 +1,52 @@
# Running `mdbook` in Continuous Integration
While the following examples use Travis CI, their principles should
straightforwardly transfer to other continuous integration providers as well.
## Ensuring Your Book Builds and Tests Pass
Here is a sample Travis CI `.travis.yml` configuration that ensures `mdbook
build` and `mdbook test` run successfully. The key to fast CI turnaround times
is caching `mdbook` installs, so that you aren't compiling `mdbook` on every CI
run.
```yaml
language: rust
sudo: false
cache:
- cargo
rust:
- stable
before_script:
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.1" mdbook)
- cargo install-update -a
script:
- cd path/to/mybook && mdbook build && mdbook test
```
## Deploying Your Book to GitHub Pages
Following these instructions will result in your book being published to GitHub
pages after a successful CI run on your repository's `master` branch.
First, create a new GitHub "Personal Access Token" with the "public_repo" permissions (or "repo" for private repositories). Go to your repository's Travis CI settings page and add an environment variable named `GITHUB_TOKEN` that is marked secure and *not* shown in the logs.
Then, append this snippet to your `.travis.yml` and update the path to the `book` directory:
```yaml
deploy:
provider: pages
skip-cleanup: true
github-token: $GITHUB_TOKEN
local-dir: path/to/mybook/book
keep-history: false
on:
branch: master
```
That's it!

View File

@@ -11,8 +11,8 @@ The *For Developers* chapters are here to show you the more advanced usage of
The two main ways a developer can hook into the book's build process is via,
- [Preprocessors](for_developers/preprocessors.html)
- [Alternate Backends](for_developers/backends.html)
- [Preprocessors](preprocessors.md)
- [Alternate Backends](backends.md)
## The Build Process

View File

@@ -58,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
@@ -104,6 +113,7 @@ Available configuration options for the `[output.html.playpen]` table:
Available configuration options for the `[output.html.search]` table:
- **enable:** Enables the search feature. Defaults to `true`.
- **limit-results:** The maximum number of search results. Defaults to `30`.
- **teaser-word-count:** The number of words used for a search result teaser.
Defaults to `30`.
@@ -132,6 +142,11 @@ title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
[build]
build-dir = "book"
create-missing = true
preprocess = ["links", "index"]
[output.html]
theme = "my-theme"
curly-quotes = true
@@ -154,6 +169,7 @@ boost-hierarchy = 1
boost-paragraph = 1
expand = true
heading-split-level = 3
copy-js = true
```

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

@@ -16,3 +16,4 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add
- [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

@@ -24,7 +24,6 @@ mod execs {
}
}
error_chain!{
foreign_links {
Io(std::io::Error);
@@ -32,23 +31,25 @@ error_chain!{
}
fn program_exists(program: &str) -> Result<()> {
execs::cmd(program).arg("-v")
.output()
.chain_err(|| format!("Please install '{}'!", program))?;
execs::cmd(program)
.arg("-v")
.output()
.chain_err(|| format!("Please install '{}'!", program))?;
Ok(())
}
fn npm_package_exists(package: &str) -> Result<()> {
let status = execs::cmd("npm").args(&["list", "-g"])
.arg(package)
.output();
let status = execs::cmd("npm")
.args(&["list", "-g"])
.arg(package)
.output();
match status {
Ok(ref out) if out.status.success() => Ok(()),
_ => {
bail!("Missing npm package '{0}' install with: 'npm -g install {0}'",
package)
}
_ => bail!(
"Missing npm package '{0}' install with: 'npm -g install {0}'",
package
),
}
}
@@ -81,13 +82,14 @@ fn run() -> Result<()> {
let theme_dir = Path::new(&manifest_dir).join("src/theme/");
let stylus_dir = theme_dir.join("stylus/book.styl");
if !execs::cmd("stylus").arg(stylus_dir)
.arg("--out")
.arg(theme_dir)
.arg("--use")
.arg("nib")
.status()?
.success()
if !execs::cmd("stylus")
.arg(stylus_dir)
.arg("--out")
.arg(theme_dir)
.arg("--use")
.arg("nib")
.status()?
.success()
{
bail!("Stylus encountered an error");
}

View File

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

View File

@@ -1,22 +0,0 @@
# This script takes care of testing your crate
set -ex
main() {
cross build --target $TARGET --all --no-default-features
cross build --target $TARGET --all
cross build --target $TARGET --all --release
if [ ! -z $DISABLE_TESTS ]; then
return
fi
cross test --target $TARGET --no-default-features
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

View File

@@ -3,15 +3,15 @@ extern crate mdbook;
extern crate pulldown_cmark;
extern crate pulldown_cmark_to_cmark;
use mdbook::errors::{Error, Result};
use mdbook::MDBook;
use mdbook::book::{Book, BookItem, Chapter};
use mdbook::errors::{Error, Result};
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::MDBook;
use pulldown_cmark::{Event, Parser, Tag};
use pulldown_cmark_to_cmark::fmt::cmark;
use std::ffi::OsString;
use std::env::{args, args_os};
use std::ffi::OsString;
use std::process;
struct Deemphasize;

3
release.toml Normal file
View File

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

View File

@@ -1,7 +0,0 @@
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,8 @@
use std::fmt::{self, Display, Formatter};
use std::path::{Path, PathBuf};
use std::collections::VecDeque;
use std::fmt::{self, Display, Formatter};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
use config::BuildConfig;
@@ -297,8 +297,8 @@ impl Display for Chapter {
#[cfg(test)]
mod tests {
use super::*;
use tempfile::{TempDir, Builder as TempFileBuilder};
use std::io::Write;
use tempfile::{Builder as TempFileBuilder, TempDir};
const DUMMY_SRC: &'static str = "
# Dummy Chapter
@@ -404,14 +404,12 @@ And here is some \
..Default::default()
};
let should_be = Book {
sections: vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
path: PathBuf::from("chapter_1.md"),
..Default::default()
}),
],
sections: vec![BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
path: PathBuf::from("chapter_1.md"),
..Default::default()
})],
..Default::default()
};
@@ -535,13 +533,11 @@ And here is some \
fn cant_load_chapters_with_an_empty_path() {
let (_, temp) = dummy_link();
let summary = Summary {
numbered_chapters: vec![
SummaryItem::Link(Link {
name: String::from("Empty"),
location: PathBuf::from(""),
..Default::default()
}),
],
numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("Empty"),
location: PathBuf::from(""),
..Default::default()
})],
..Default::default()
};
@@ -556,13 +552,11 @@ And here is some \
fs::create_dir(&dir).unwrap();
let summary = Summary {
numbered_chapters: vec![
SummaryItem::Link(Link {
name: String::from("nested"),
location: dir,
..Default::default()
}),
],
numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("nested"),
location: dir,
..Default::default()
})],
..Default::default()
};

View File

@@ -1,12 +1,12 @@
use std::fs::{self, File};
use std::path::PathBuf;
use std::io::Write;
use std::path::PathBuf;
use toml;
use config::Config;
use super::MDBook;
use theme;
use config::Config;
use errors::*;
use theme;
/// A helper for setting up a new book and its directory structure.
#[derive(Debug, Clone, PartialEq)]

View File

@@ -5,24 +5,24 @@
//!
//! [1]: ../index.html
mod summary;
mod book;
mod init;
mod summary;
pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
pub use self::init::BookBuilder;
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
use std::path::PathBuf;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use tempfile::Builder as TempFileBuilder;
use toml::Value;
use utils;
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
use preprocess::{LinkPreprocessor, Preprocessor, PreprocessorContext};
use errors::*;
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext};
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
use utils;
use config::Config;
@@ -213,11 +213,13 @@ impl MDBook {
.flat_map(|x| vec![x.0, x.1])
.collect();
let temp_dir = TempFileBuilder::new().prefix("mdbook").tempdir()?;
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
let preprocess_context = PreprocessorContext::new(self.root.clone(), self.config.clone());
LinkPreprocessor::new().run(&preprocess_context, &mut self.book)?;
// Index Preprocessor is disabled so that chapter paths continue to point to the
// actual markdown files.
for item in self.iter() {
if let BookItem::Chapter(ref ch) = *item {
@@ -322,15 +324,19 @@ fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
}
fn default_preprocessors() -> Vec<Box<Preprocessor>> {
vec![Box::new(LinkPreprocessor::new())]
vec![
Box::new(LinkPreprocessor::new()),
Box::new(IndexPreprocessor::new()),
]
}
/// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
let preprocess_list = match config.build.preprocess {
Some(ref p) => p,
// If no preprocessor field is set, default to the LinkPreprocessor. This allows you
// to disable the LinkPreprocessor by setting "preprocess" to an empty list.
// If no preprocessor field is set, default to the LinkPreprocessor and
// IndexPreprocessor. This allows you to disable default preprocessors
// by setting "preprocess" to an empty list.
None => return Ok(default_preprocessors()),
};
@@ -339,6 +345,7 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
for key in preprocess_list {
match key.as_ref() {
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
_ => bail!("{:?} is not a recognised preprocessor", key),
}
}
@@ -403,7 +410,7 @@ mod tests {
}
#[test]
fn config_defaults_to_link_preprocessor_if_not_set() {
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
let cfg = Config::default();
// make sure we haven't got anything in the `output` table
@@ -412,8 +419,9 @@ mod tests {
let got = determine_preprocessors(&cfg);
assert!(got.is_ok());
assert_eq!(got.as_ref().unwrap().len(), 1);
assert_eq!(got.as_ref().unwrap().len(), 2);
assert_eq!(got.as_ref().unwrap()[0].name(), "links");
assert_eq!(got.as_ref().unwrap()[1].name(), "index");
}
#[test]

View File

@@ -1,10 +1,10 @@
use errors::*;
use memchr::{self, Memchr};
use pulldown_cmark::{self, Event, Tag};
use std::fmt::{self, Display, Formatter};
use std::iter::FromIterator;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use memchr::{self, Memchr};
use pulldown_cmark::{self, Event, Tag};
use errors::*;
/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be
/// used when loading a book from disk.
@@ -164,33 +164,34 @@ struct SummaryParser<'a> {
/// use pattern matching and you won't get errors because `take_while()`
/// moves `$stream` out of self.
macro_rules! collect_events {
($stream:expr, start $delimiter:pat) => {
($stream:expr,start $delimiter:pat) => {
collect_events!($stream, Event::Start($delimiter))
};
($stream:expr, end $delimiter:pat) => {
($stream:expr,end $delimiter:pat) => {
collect_events!($stream, Event::End($delimiter))
};
($stream:expr, $delimiter:pat) => {
{
let mut events = Vec::new();
($stream:expr, $delimiter:pat) => {{
let mut events = Vec::new();
loop {
let event = $stream.next();
trace!("Next event: {:?}", event);
loop {
let event = $stream.next();
trace!("Next event: {:?}", event);
match event {
Some($delimiter) => break,
Some(other) => events.push(other),
None => {
debug!("Reached end of stream without finding the closing pattern, {}", stringify!($delimiter));
break;
}
match event {
Some($delimiter) => break,
Some(other) => events.push(other),
None => {
debug!(
"Reached end of stream without finding the closing pattern, {}",
stringify!($delimiter)
);
break;
}
}
events
}
}
events
}};
}
impl<'a> SummaryParser<'a> {
@@ -659,14 +660,12 @@ mod tests {
name: String::from("First"),
location: PathBuf::from("./first.md"),
number: Some(SectionNumber(vec![1])),
nested_items: vec![
SummaryItem::Link(Link {
name: String::from("Nested"),
location: PathBuf::from("./nested.md"),
number: Some(SectionNumber(vec![1, 1])),
nested_items: Vec::new(),
}),
],
nested_items: vec![SummaryItem::Link(Link {
name: String::from("Nested"),
location: PathBuf::from("./nested.md"),
number: Some(SectionNumber(vec![1, 1])),
nested_items: Vec::new(),
})],
}),
SummaryItem::Link(Link {
name: String::from("Second"),

View File

@@ -1,7 +1,7 @@
use std::path::PathBuf;
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::Result;
use mdbook::MDBook;
use std::path::PathBuf;
use {get_book_dir, open};
// Create clap subcommand arguments

View File

@@ -1,9 +1,9 @@
use clap::{App, ArgMatches, SubCommand};
use get_book_dir;
use mdbook::errors::*;
use mdbook::MDBook;
use std::fs;
use std::path::PathBuf;
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::*;
use get_book_dir;
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {

View File

@@ -1,12 +1,11 @@
use clap::{App, ArgMatches, SubCommand};
use get_book_dir;
use mdbook::config;
use mdbook::errors::Result;
use mdbook::MDBook;
use std::io;
use std::io::Write;
use std::process::Command;
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::Result;
use mdbook::utils;
use mdbook::config;
use get_book_dir;
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {

10
src/cmd/mod.rs Normal file
View File

@@ -0,0 +1,10 @@
//! Subcommand modules for the `mdbook` binary.
pub mod build;
pub mod clean;
pub mod init;
#[cfg(feature = "serve")]
pub mod serve;
pub mod test;
#[cfg(feature = "watch")]
pub mod watch;

View File

@@ -2,16 +2,17 @@ extern crate iron;
extern crate staticfile;
extern crate ws;
use std;
use self::iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response,
Set};
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::utils;
use mdbook::errors::*;
use {get_book_dir, open};
use self::iron::{
status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set,
};
#[cfg(feature = "watch")]
use watch;
use super::watch;
use clap::{App, ArgMatches, SubCommand};
use mdbook::errors::*;
use mdbook::utils;
use mdbook::MDBook;
use std;
use {get_book_dir, open};
struct ErrorRecover;

View File

@@ -1,22 +1,20 @@
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::Result;
use get_book_dir;
use mdbook::errors::Result;
use mdbook::MDBook;
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("test")
.about("Test that code samples compile")
.arg_from_usage(
"-L, --library-path [DIR]... 'directory to add to crate search path'",
)
.arg_from_usage("-L, --library-path [DIR]... 'directory to add to crate search path'")
}
// test command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
let library_paths: Vec<&str> = args.values_of("library-path")
.map(|v| v.collect())
.unwrap_or_default();
.map(|v| v.collect())
.unwrap_or_default();
let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?;

View File

@@ -1,13 +1,13 @@
extern crate notify;
use std::path::Path;
use self::notify::Watcher;
use std::time::Duration;
use std::sync::mpsc::channel;
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::utils;
use mdbook::errors::Result;
use mdbook::utils;
use mdbook::MDBook;
use std::path::Path;
use std::sync::mpsc::channel;
use std::time::Duration;
use {get_book_dir, open};
// Create clap subcommand arguments
@@ -48,8 +48,8 @@ pub fn trigger_on_change<F>(book: &MDBook, closure: F)
where
F: Fn(&Path, &Path),
{
use self::notify::RecursiveMode::*;
use self::notify::DebouncedEvent::*;
use self::notify::RecursiveMode::*;
// Create a channel to receive the events.
let (tx, rx) = channel();

View File

@@ -50,17 +50,17 @@
#![deny(missing_docs)]
use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::Read;
use std::env;
use toml::{self, Value};
use toml::value::Table;
use toml_query::read::TomlValueReadExt;
use toml_query::insert::TomlValueInsertExt;
use toml_query::delete::TomlValueDeleteExt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json;
use std::env;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use toml::value::Table;
use toml::{self, Value};
use toml_query::delete::TomlValueDeleteExt;
use toml_query::insert::TomlValueInsertExt;
use toml_query::read::TomlValueReadExt;
use errors::*;
@@ -217,9 +217,10 @@ impl Config {
// figure out what try_into() deserializes to.
macro_rules! get_and_insert {
($table:expr, $key:expr => $out:expr) => {
let got = $table.as_table_mut()
.and_then(|t| t.remove($key))
.and_then(|v| v.try_into().ok());
let got = $table
.as_table_mut()
.and_then(|t| t.remove($key))
.and_then(|v| v.try_into().ok());
if let Some(value) = got {
$out = value;
}
@@ -463,9 +464,11 @@ impl Default for Playpen {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Search {
/// Enable the search feature. Default: `true`.
pub enable: bool,
/// Maximum number of visible results. Default: `30`.
pub limit_results: u32,
/// The number of words used for a search result teaser. Default: `30`,
/// The number of words used for a search result teaser. Default: `30`.
pub teaser_word_count: u32,
/// Define the logical link between multiple search words.
/// If true, all search words must appear in each result. Default: `true`.
@@ -494,6 +497,7 @@ impl Default for Search {
fn default() -> Search {
// Please update the documentation of `Search` when changing values!
Search {
enable: true,
limit_results: 30,
teaser_word_count: 30,
use_boolean_and: false,

View File

@@ -107,17 +107,17 @@ extern crate toml_query;
#[macro_use]
extern crate pretty_assertions;
pub mod preprocess;
pub mod book;
pub mod config;
pub mod preprocess;
pub mod renderer;
pub mod theme;
pub mod utils;
pub use book::MDBook;
pub use book::BookItem;
pub use renderer::Renderer;
pub use book::MDBook;
pub use config::Config;
pub use renderer::Renderer;
/// The error types used through out this crate.
pub mod errors {

View File

@@ -8,24 +8,17 @@ extern crate log;
extern crate mdbook;
extern crate open;
use chrono::Local;
use clap::{App, AppSettings, ArgMatches};
use env_logger::Builder;
use log::LevelFilter;
use mdbook::utils;
use std::env;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::io::Write;
use clap::{App, AppSettings, ArgMatches};
use chrono::Local;
use log::LevelFilter;
use env_logger::Builder;
use mdbook::utils;
use std::path::{Path, PathBuf};
pub mod build;
pub mod clean;
pub mod init;
pub mod test;
#[cfg(feature = "serve")]
pub mod serve;
#[cfg(feature = "watch")]
pub mod watch;
mod cmd;
const NAME: &'static str = "mdbook";
@@ -43,26 +36,26 @@ fn main() {
try `mdbook <command> --help`\n\
Source code for mdbook available \
at: https://github.com/rust-lang-nursery/mdBook")
.subcommand(init::make_subcommand())
.subcommand(build::make_subcommand())
.subcommand(test::make_subcommand())
.subcommand(clean::make_subcommand());
.subcommand(cmd::init::make_subcommand())
.subcommand(cmd::build::make_subcommand())
.subcommand(cmd::test::make_subcommand())
.subcommand(cmd::clean::make_subcommand());
#[cfg(feature = "watch")]
let app = app.subcommand(watch::make_subcommand());
let app = app.subcommand(cmd::watch::make_subcommand());
#[cfg(feature = "serve")]
let app = app.subcommand(serve::make_subcommand());
let app = app.subcommand(cmd::serve::make_subcommand());
// Check which subcomamnd the user ran...
let res = match app.get_matches().subcommand() {
("init", Some(sub_matches)) => init::execute(sub_matches),
("build", Some(sub_matches)) => build::execute(sub_matches),
("clean", Some(sub_matches)) => clean::execute(sub_matches),
("init", Some(sub_matches)) => cmd::init::execute(sub_matches),
("build", Some(sub_matches)) => cmd::build::execute(sub_matches),
("clean", Some(sub_matches)) => cmd::clean::execute(sub_matches),
#[cfg(feature = "watch")]
("watch", Some(sub_matches)) => watch::execute(sub_matches),
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches),
#[cfg(feature = "serve")]
("serve", Some(sub_matches)) => serve::execute(sub_matches),
("test", Some(sub_matches)) => test::execute(sub_matches),
("serve", Some(sub_matches)) => cmd::serve::execute(sub_matches),
("test", Some(sub_matches)) => cmd::test::execute(sub_matches),
(_, _) => unreachable!(),
};

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

@@ -0,0 +1,97 @@
use regex::Regex;
use std::path::Path;
use errors::*;
use super::{Preprocessor, PreprocessorContext};
use book::{Book, BookItem};
/// A preprocessor for converting file name `README.md` to `index.md` since
/// `README.md` is the de facto index file in a markdown-based documentation.
pub struct IndexPreprocessor;
impl IndexPreprocessor {
/// Create a new `IndexPreprocessor`.
pub fn new() -> Self {
IndexPreprocessor
}
}
impl Preprocessor for IndexPreprocessor {
fn name(&self) -> &str {
"index"
}
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
let source_dir = ctx.root.join(&ctx.config.book.src);
book.for_each_mut(|section: &mut BookItem| {
if let BookItem::Chapter(ref mut ch) = *section {
if is_readme_file(&ch.path) {
let index_md = source_dir.join(ch.path.with_file_name("index.md"));
if index_md.exists() {
warn_readme_name_conflict(&ch.path, &index_md);
}
ch.path.set_file_name("index.md");
}
}
});
Ok(())
}
}
fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
let file_name = readme_path.as_ref().file_name().unwrap_or_default();
let parent_dir = index_path.as_ref().parent().unwrap_or(index_path.as_ref());
warn!(
"It seems that there are both {:?} and index.md under \"{}\".",
file_name,
parent_dir.display()
);
warn!(
"mdbook converts {:?} into index.html by default. It may cause",
file_name
);
warn!("unexpected behavior if putting both files under the same directory.");
warn!("To solve the warning, try to rearrange the book structure or disable");
warn!("\"index\" preprocessor to stop the conversion.");
}
fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
lazy_static! {
static ref RE: Regex = Regex::new(r"(?i)^readme$").unwrap();
}
RE.is_match(
path.as_ref()
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn file_stem_exactly_matches_readme_case_insensitively() {
let path = "path/to/Readme.md";
assert!(is_readme_file(path));
let path = "path/to/README.md";
assert!(is_readme_file(path));
let path = "path/to/rEaDmE.md";
assert!(is_readme_file(path));
let path = "path/to/README.markdown";
assert!(is_readme_file(path));
let path = "path/to/README";
assert!(is_readme_file(path));
let path = "path/to/README-README.md";
assert!(!is_readme_file(path));
}
}

View File

@@ -1,14 +1,15 @@
use errors::*;
use regex::{CaptureMatches, Captures, Regex};
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
use std::path::{Path, PathBuf};
use regex::{CaptureMatches, Captures, Regex};
use utils::fs::file_to_string;
use utils::take_lines;
use errors::*;
use super::{Preprocessor, PreprocessorContext};
use book::{Book, BookItem};
const ESCAPE_CHAR: char = '\\';
const MAX_LINK_NESTED_DEPTH: usize = 10;
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
/// helpers in a chapter.
@@ -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,21 @@ 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 +100,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 +128,7 @@ fn parse_include_path(path: &str) -> LinkType<'static> {
let start = parts
.next()
.and_then(|s| s.parse::<usize>().ok())
.map(|val| val.checked_sub(1).unwrap_or(0));
.map(|val| val.saturating_sub(1));
let end = parts.next();
let has_end = end.is_some();
let end = end.and_then(|s| s.parse::<usize>().ok());
@@ -210,15 +247,16 @@ fn find_links(contents: &str) -> LinkIter {
// lazily compute following regex
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
lazy_static! {
static ref RE: Regex = Regex::new(r"(?x) # insignificant whitespace mode
\\\{\{\#.*\}\} # match escaped link
| # or
\{\{\s* # link opening parens and whitespace
\#([a-zA-Z0-9]+) # link type
\s+ # separating whitespace
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
\s*\}\} # whitespace and link closing parens
").unwrap();
static ref RE: Regex = Regex::new(
r"(?x) # insignificant whitespace mode
\\\{\{\#.*\}\} # match escaped link
| # or
\{\{\s* # link opening parens and whitespace
\#([a-zA-Z0-9]+) # link type
\s+ # separating whitespace
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
\s*\}\} # whitespace and link closing parens"
).unwrap();
}
LinkIter(RE.captures_iter(contents))
}
@@ -288,14 +326,12 @@ mod tests {
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 48,
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
link_text: "{{#include file.rs:10:20}}",
},
]
vec![Link {
start_index: 22,
end_index: 48,
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
link_text: "{{#include file.rs:10:20}}",
}]
);
}
@@ -306,14 +342,12 @@ mod tests {
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 45,
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
link_text: "{{#include file.rs:10}}",
},
]
vec![Link {
start_index: 22,
end_index: 45,
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
link_text: "{{#include file.rs:10}}",
}]
);
}
@@ -324,14 +358,12 @@ mod tests {
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 46,
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
link_text: "{{#include file.rs:10:}}",
},
]
vec![Link {
start_index: 22,
end_index: 46,
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
link_text: "{{#include file.rs:10:}}",
}]
);
}
@@ -342,14 +374,12 @@ mod tests {
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 46,
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
link_text: "{{#include file.rs::20}}",
},
]
vec![Link {
start_index: 22,
end_index: 46,
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
link_text: "{{#include file.rs::20}}",
}]
);
}
@@ -360,14 +390,12 @@ mod tests {
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 44,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_text: "{{#include file.rs::}}",
},
]
vec![Link {
start_index: 22,
end_index: 44,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_text: "{{#include file.rs::}}",
}]
);
}
@@ -378,14 +406,12 @@ mod tests {
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 42,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_text: "{{#include file.rs}}",
},
]
vec![Link {
start_index: 22,
end_index: 42,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_text: "{{#include file.rs}}",
}]
);
}
@@ -398,14 +424,12 @@ mod tests {
assert_eq!(
res,
vec![
Link {
start_index: 38,
end_index: 68,
link: LinkType::Escaped,
link_text: "\\{{#playpen file.rs editable}}",
},
]
vec![Link {
start_index: 38,
end_index: 68,
link: LinkType::Escaped,
link_text: "\\{{#playpen file.rs editable}}",
}]
);
}

View File

@@ -1,7 +1,9 @@
//! Book preprocessing.
pub use self::index::IndexPreprocessor;
pub use self::links::LinkPreprocessor;
mod index;
mod links;
use book::Book;
@@ -10,7 +12,7 @@ use errors::*;
use std::path::PathBuf;
/// Extra information for a `Preprocessor` to give them more context when
/// Extra information for a `Preprocessor` to give them more context when
/// processing a book.
pub struct PreprocessorContext {
/// The location of the book directory on disk.
@@ -26,7 +28,7 @@ impl PreprocessorContext {
}
}
/// An operation which is run immediately after loading a book into memory and
/// An operation which is run immediately after loading a book into memory and
/// before it gets rendered.
pub trait Preprocessor {
/// Get the `Preprocessor`'s name.
@@ -35,4 +37,4 @@ pub trait Preprocessor {
/// Run this `Preprocessor`, allowing it to update the book before it is
/// given to a renderer.
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
}
}

View File

@@ -1,12 +1,11 @@
use book::{Book, BookItem, Chapter};
use config::{Config, HtmlConfig, Playpen};
use errors::*;
use renderer::{RenderContext, Renderer};
use renderer::html_handlebars::helpers;
use theme::{self, Theme, playpen_editor};
use renderer::{RenderContext, Renderer};
use theme::{self, playpen_editor, Theme};
use utils;
#[allow(unused_imports)] use std::ascii::AsciiExt;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fs::{self, File};
@@ -42,11 +41,7 @@ impl HtmlHandlebars {
let path = ch.path
.to_str()
.chain_err(|| "Could not convert path to str")?;
let filepath = Path::new(&ch.path)
.with_extension("html");
let filepathstr = filepath.to_str()
.chain_err(|| "Could not convert HTML path to str")?;
let filepathstr = utils::fs::normalize_path(filepathstr);
let filepath = Path::new(&ch.path).with_extension("html");
// "print.html" is used for the print page.
if ch.path == Path::new("print.md") {
@@ -57,9 +52,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;
}
@@ -67,21 +62,19 @@ impl HtmlHandlebars {
ctx.data.insert("content".to_owned(), json!(content));
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
ctx.data.insert("title".to_owned(), json!(title));
ctx.data.insert("path_to_root".to_owned(),
json!(utils::fs::path_to_root(&ch.path)));
ctx.data.insert(
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(&ch.path)),
);
// Render the handlebars template with the data
debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?;
let rendered = self.post_process(
rendered,
&filepathstr,
&ctx.html_config.playpen,
);
let rendered = self.post_process(rendered, &ctx.html_config.playpen);
// Write to file
debug!("Creating {} ✓", filepathstr);
debug!("Creating {} ✓", filepath.display());
utils::fs::write_file(&ctx.destination, &filepath, &rendered.into_bytes())?;
if ctx.is_index {
@@ -106,10 +99,11 @@ 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");
utils::fs::write_file(destination, "index.html", content.as_bytes())?;
@@ -122,13 +116,8 @@ impl HtmlHandlebars {
}
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
fn post_process(&self,
rendered: String,
filepath: &str,
playpen_config: &Playpen)
-> String {
let rendered = build_header_links(&rendered, filepath);
let rendered = fix_anchor_links(&rendered, filepath);
fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String {
let rendered = build_header_links(&rendered);
let rendered = fix_code_blocks(&rendered);
let rendered = add_playpen_pre(&rendered, playpen_config);
@@ -143,6 +132,12 @@ impl HtmlHandlebars {
) -> Result<()> {
use utils::fs::write_file;
write_file(
destination,
".nojekyll",
b"This file makes sure that Github Pages doesn't process mdBook's output.",
)?;
write_file(destination, "book.js", &theme.js)?;
write_file(destination, "book.css", &theme.css)?;
write_file(destination, "favicon.png", &theme.favicon)?;
@@ -153,37 +148,37 @@ impl HtmlHandlebars {
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
write_file(
destination,
"_FontAwesome/css/font-awesome.css",
"FontAwesome/css/font-awesome.css",
theme::FONT_AWESOME,
)?;
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.eot",
"FontAwesome/fonts/fontawesome-webfont.eot",
theme::FONT_AWESOME_EOT,
)?;
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.svg",
"FontAwesome/fonts/fontawesome-webfont.svg",
theme::FONT_AWESOME_SVG,
)?;
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.ttf",
"FontAwesome/fonts/fontawesome-webfont.ttf",
theme::FONT_AWESOME_TTF,
)?;
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.woff",
"FontAwesome/fonts/fontawesome-webfont.woff",
theme::FONT_AWESOME_WOFF,
)?;
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.woff2",
"FontAwesome/fonts/fontawesome-webfont.woff2",
theme::FONT_AWESOME_WOFF2,
)?;
write_file(
destination,
"_FontAwesome/fonts/FontAwesome.ttf",
"FontAwesome/fonts/FontAwesome.ttf",
theme::FONT_AWESOME_TTF,
)?;
@@ -196,7 +191,8 @@ impl HtmlHandlebars {
write_file(destination, "ace.js", playpen_editor::ACE_JS)?;
write_file(destination, "mode-rust.js", playpen_editor::MODE_RUST_JS)?;
write_file(destination, "theme-dawn.js", playpen_editor::THEME_DAWN_JS)?;
write_file(destination,
write_file(
destination,
"theme-tomorrow_night.js",
playpen_editor::THEME_TOMORROW_NIGHT_JS,
)?;
@@ -206,28 +202,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");
@@ -258,6 +268,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"
@@ -274,9 +303,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");
@@ -305,9 +344,7 @@ impl Renderer for HtmlHandlebars {
is_index: is_index,
html_config: html_config.clone(),
};
self.render_item(item,
ctx,
&mut print_content)?;
self.render_item(item, ctx, &mut print_content)?;
is_index = false;
}
@@ -321,9 +358,7 @@ impl Renderer for HtmlHandlebars {
debug!("Render template");
let rendered = handlebars.render("index", &data)?;
let rendered = self.post_process(rendered,
"print.html",
&html_config.playpen);
let rendered = self.post_process(rendered, &html_config.playpen);
utils::fs::write_file(&destination, "print.html", &rendered.into_bytes())?;
debug!("Creating print.html ✓");
@@ -336,7 +371,12 @@ impl Renderer for HtmlHandlebars {
// Render search index
#[cfg(feature = "search")]
super::search::create_files(&html_config.search.unwrap_or_default(), &destination, &book)?;
{
let search = html_config.search.unwrap_or_default();
if search.enable {
super::search::create_files(&search, &destination, &book)?;
}
}
// Copy all remaining files
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
@@ -345,14 +385,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));
@@ -372,13 +423,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));
@@ -390,12 +436,13 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
for script in &html.additional_js {
match script.strip_prefix(root) {
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
Err(_) => {
js.push(script.file_name()
.expect("File has a file name")
.to_str()
.expect("Could not convert to str"))
}
Err(_) => js.push(
script
.file_name()
.expect("File has a file name")
.to_str()
.expect("Could not convert to str"),
),
}
}
data.insert("additional_js".to_owned(), json!(js));
@@ -407,16 +454,20 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
let search = html_config.search.clone();
if cfg!(feature = "search") {
data.insert("search_enabled".to_owned(), json!(true));
if search.unwrap_or_default().copy_js {
data.insert("search_js".to_owned(), json!(true));
}
let search = search.unwrap_or_default();
data.insert("search_enabled".to_owned(), json!(search.enable));
data.insert(
"search_js".to_owned(),
json!(search.enable && search.copy_js),
);
} else if search.is_some() {
warn!("mdBook compiled without search support, ignoring `output.html.search` table");
warn!("please reinstall with `cargo install mdbook --force --features search`\
to use the search feature")
warn!(
"please reinstall with `cargo install mdbook --force --features search`to use the \
search feature"
)
}
let mut chapters = vec![];
for item in book.iter() {
@@ -451,26 +502,28 @@ fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig
/// Goes through the rendered HTML, making sure all header tags are wrapped in
/// an anchor so people can link to sections directly.
fn build_header_links(html: &str, filepath: &str) -> String {
fn build_header_links(html: &str) -> String {
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
let mut id_counter = HashMap::new();
regex.replace_all(html, |caps: &Captures| {
let level = caps[1].parse()
.expect("Regex should ensure we only ever get numbers here");
regex
.replace_all(html, |caps: &Captures| {
let level = caps[1]
.parse()
.expect("Regex should ensure we only ever get numbers here");
wrap_header_with_link(level, &caps[2], &mut id_counter, filepath)
})
.into_owned()
wrap_header_with_link(level, &caps[2], &mut id_counter)
})
.into_owned()
}
/// Wraps a single header tag with a link, making sure each tag gets its own
/// unique ID by appending an auto-incremented number (if necessary).
fn wrap_header_with_link(level: usize,
content: &str,
id_counter: &mut HashMap<String, usize>,
filepath: &str)
-> String {
fn wrap_header_with_link(
level: usize,
content: &str,
id_counter: &mut HashMap<String, usize>,
) -> String {
let raw_id = utils::id_from_content(content);
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
@@ -483,34 +536,13 @@ fn wrap_header_with_link(level: usize,
*id_count += 1;
format!(
r##"<a class="header" href="{filepath}#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
r##"<a class="header" href="#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
level = level,
id = id,
text = content,
filepath = filepath
text = content
)
}
// anchors to the same page (href="#anchor") do not work because of
// <base href="../"> pointing to the root folder. This function *fixes*
// that in a very inelegant way
fn fix_anchor_links(html: &str, filepath: &str) -> String {
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
regex.replace_all(html, |caps: &Captures| {
let before = &caps[1];
let anchor = &caps[2];
let after = &caps[3];
format!("<a{before}href=\"{filepath}#{anchor}\"{after}>",
before = before,
filepath = filepath,
anchor = anchor,
after = after)
})
.into_owned()
}
// The rust book uses annotations for rustdoc to test code snippets,
// like the following:
// ```rust,should_panic
@@ -521,51 +553,55 @@ fn fix_anchor_links(html: &str, filepath: &str) -> String {
// This function replaces all commas by spaces in the code block classes
fn fix_code_blocks(html: &str) -> String {
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
regex.replace_all(html, |caps: &Captures| {
let before = &caps[1];
let classes = &caps[2].replace(",", " ");
let after = &caps[3];
regex
.replace_all(html, |caps: &Captures| {
let before = &caps[1];
let classes = &caps[2].replace(",", " ");
let after = &caps[3];
format!(r#"<code{before}class="{classes}"{after}>"#,
format!(
r#"<code{before}class="{classes}"{after}>"#,
before = before,
classes = classes,
after = after)
}).into_owned()
after = after
)
})
.into_owned()
}
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
regex.replace_all(html, |caps: &Captures| {
let text = &caps[1];
let classes = &caps[2];
let code = &caps[3];
regex
.replace_all(html, |caps: &Captures| {
let text = &caps[1];
let classes = &caps[2];
let code = &caps[3];
if (classes.contains("language-rust") && !classes.contains("ignore")) ||
classes.contains("mdbook-runnable")
{
// wrap the contents in an external pre block
if playpen_config.editable && classes.contains("editable") ||
text.contains("fn main") || text.contains("quick_main!")
if (classes.contains("language-rust") && !classes.contains("ignore"))
|| classes.contains("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) {
@@ -606,37 +642,32 @@ mod tests {
let inputs = vec![
(
"blah blah <h1>Foo</h1>",
r##"blah blah <a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
r##"blah blah <a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
),
(
"<h1>Foo</h1>",
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
),
(
"<h3>Foo^bar</h3>",
r##"<a class="header" href="./some_chapter/some_section.html#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
r##"<a class="header" href="#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
),
(
"<h4></h4>",
r##"<a class="header" href="./some_chapter/some_section.html#" id=""><h4></h4></a>"##,
r##"<a class="header" href="#" id=""><h4></h4></a>"##,
),
(
"<h4><em>Hï</em></h4>",
r##"<a class="header" href="./some_chapter/some_section.html#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
r##"<a class="header" href="#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
),
(
"<h1>Foo</h1><h3>Foo</h3>",
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a><a class="header" href="./some_chapter/some_section.html#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a><a class="header" href="#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
),
];
for (src, should_be) in inputs {
let filepath = "./some_chapter/some_section.html";
let got = build_header_links(&src, filepath);
assert_eq!(got, should_be);
// This is redundant for most cases
let got = fix_anchor_links(&got, filepath);
let got = build_header_links(&src);
assert_eq!(got, should_be);
}
}

View File

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

View File

@@ -1,8 +1,10 @@
use std::path::Path;
use std::collections::BTreeMap;
use std::path::Path;
use serde_json;
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
use serde_json;
use utils;
type StringMap = BTreeMap<String, String>;
@@ -87,6 +89,15 @@ fn render(
trace!("Creating BTreeMap to inject in context");
let mut context = BTreeMap::new();
let base_path = rc.evaluate_absolute("path", false)?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
context.insert(
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(&base_path)),
);
chapter
.get("name")

View File

@@ -1,9 +1,11 @@
use std::path::Path;
use std::collections::BTreeMap;
use std::path::Path;
use utils;
use serde_json;
use handlebars::{Handlebars, Helper, HelperDef, RenderContext, RenderError};
use pulldown_cmark::{html, Event, Parser, Tag};
use serde_json;
// Handlebars helper to construct TOC
#[derive(Clone, Copy)]
@@ -77,6 +79,8 @@ impl HelperDef for RenderToc {
.replace("\\", "/");
// Add link
rc.writer
.write_all(&utils::fs::path_to_root(&current).as_bytes())?;
rc.writer.write_all(tmp.as_bytes())?;
rc.writer.write_all(b"\"")?;

View File

@@ -5,29 +5,38 @@ use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use self::elasticlunr::Index;
use pulldown_cmark::*;
use serde_json;
use self::elasticlunr::Index;
use book::{Book, BookItem};
use config::Search;
use errors::*;
use utils;
use theme::searcher;
use utils;
/// Creates all files required for search.
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
let mut index = Index::new(&["title", "body", "breadcrumbs"]);
let mut doc_urls = Vec::with_capacity(book.sections.len());
for item in book.iter() {
render_item(&mut index, &search_config, item)?;
render_item(&mut index, &search_config, &mut doc_urls, item)?;
}
let index = write_to_js(index, &search_config)?;
let index = write_to_json(index, &search_config, doc_urls)?;
debug!("Writing search index ✓");
if index.len() > 10_000_000 {
warn!("searchindex.json is very large ({} bytes)", index.len());
}
if search_config.copy_js {
utils::fs::write_file(destination, "searchindex.js", index.as_bytes())?;
utils::fs::write_file(destination, "searchindex.json", index.as_bytes())?;
utils::fs::write_file(
destination,
"searchindex.js",
format!("window.search = {};", index).as_bytes(),
)?;
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
@@ -38,18 +47,22 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
}
/// Uses the given arguments to construct a search document, then inserts it to the given index.
fn add_doc<'a>(
fn add_doc(
index: &mut Index,
anchor_base: &'a str,
doc_urls: &mut Vec<String>,
anchor_base: &str,
section_id: &Option<String>,
items: &[&str],
) {
let doc_ref: Cow<'a, str> = if let &Some(ref id) = section_id {
format!("{}#{}", anchor_base, id).into()
let url = if let &Some(ref id) = section_id {
Cow::Owned(format!("{}#{}", anchor_base, id))
} else {
anchor_base.into()
Cow::Borrowed(anchor_base)
};
let doc_ref = utils::collapse_whitespace(doc_ref.trim());
let url = utils::collapse_whitespace(url.trim());
let doc_ref = doc_urls.len().to_string();
doc_urls.push(url.into());
let items = items.iter().map(|&x| utils::collapse_whitespace(x.trim()));
index.add_doc(&doc_ref, items);
}
@@ -58,6 +71,7 @@ fn add_doc<'a>(
fn render_item(
index: &mut Index,
search_config: &Search,
doc_urls: &mut Vec<String>,
item: &BookItem,
) -> Result<()> {
let chapter = match item {
@@ -92,6 +106,7 @@ fn render_item(
// Write the data to the index, and clear it for the next section
add_doc(
index,
doc_urls,
&anchor_base,
&section_id,
&[&heading, &body, &breadcrumbs.join(" » ")],
@@ -144,6 +159,7 @@ fn render_item(
// Make sure the last section is added to the index
add_doc(
index,
doc_urls,
&anchor_base,
&section_id,
&[&heading, &body, &breadcrumbs.join(" » ")],
@@ -153,12 +169,9 @@ fn render_item(
Ok(())
}
/// Exports the index and search options to a JS script which stores the index in `window.search`.
/// Using a JS script is a workaround for CORS in `file://` URIs. It also removes the need for
/// downloading/parsing JSON in JS.
fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
use std::collections::BTreeMap;
fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
use std::collections::BTreeMap;
#[derive(Serialize)]
struct ResultsOptions {
@@ -169,9 +182,11 @@ fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
#[derive(Serialize)]
struct SearchindexJson {
/// The options used for displaying search results
resultsoptions: ResultsOptions,
results_options: ResultsOptions,
/// The searchoptions for elasticlunr.js
searchoptions: SearchOptions,
search_options: SearchOptions,
/// Used to lookup a document's URL from an integer document ref.
doc_urls: Vec<String>,
/// The index for elasticlunr.js
index: elasticlunr::Index,
}
@@ -185,7 +200,7 @@ fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
opt.boost = Some(search_config.boost_hierarchy);
fields.insert("breadcrumbs".into(), opt);
let searchoptions = SearchOptions {
let search_options = SearchOptions {
bool: if search_config.use_boolean_and {
SearchBool::And
} else {
@@ -195,19 +210,24 @@ fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
fields,
};
let resultsoptions = ResultsOptions {
let results_options = ResultsOptions {
limit_results: search_config.limit_results,
teaser_word_count: search_config.teaser_word_count,
};
let json_contents = SearchindexJson {
resultsoptions,
searchoptions,
results_options,
search_options,
doc_urls,
index,
};
// By converting to serde_json::Value as an intermediary, we use a
// BTreeMap internally and can force a stable ordering of map keys.
let json_contents = serde_json::to_value(&json_contents)?;
let json_contents = serde_json::to_string(&json_contents)?;
Ok(format!("window.search = {};", json_contents))
Ok(json_contents)
}
fn clean_html(html: &str) -> String {

View File

@@ -15,16 +15,16 @@ pub use self::html_handlebars::HtmlHandlebars;
mod html_handlebars;
use serde_json;
use shlex::Shlex;
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use serde_json;
use shlex::Shlex;
use errors::*;
use config::Config;
use book::Book;
use config::Config;
use errors::*;
const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -162,17 +162,21 @@ impl Renderer for CmdRenderer {
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.current_dir(&ctx.destination)
.spawn() {
Ok(c) => c,
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
warn!("The command wasn't found, is the \"{}\" backend installed?", self.name);
warn!("\tCommand: {}", self.cmd);
return Ok(());
}
Err(e) => {
return Err(e).chain_err(|| "Unable to start the backend")?;
}
};
.spawn()
{
Ok(c) => c,
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
warn!(
"The command wasn't found, is the \"{}\" backend installed?",
self.name
);
warn!("\tCommand: {}", self.cmd);
return Ok(());
}
Err(e) => {
return Err(e).chain_err(|| "Unable to start the backend")?;
}
};
{
let mut stdin = child.stdin.take().expect("Child has stdin");

View File

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 348 KiB

View File

@@ -1,6 +1,9 @@
html {
font-family: "Open Sans", sans-serif;
color: #333;
-webkit-text-size-adjust: none;
-ms-text-size-adjust: none;
text-size-adjust: none;
}
body {
margin: 0;
@@ -81,11 +84,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;
@@ -130,49 +135,45 @@ table thead td {
padding-left: 20px;
line-height: 1.9em;
}
.section li {
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.page-wrapper {
left: 0;
position: absolute;
right: 0;
top: 0;
bottom: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-transition: padding-left 0.5s, margin-left 0.5s, left 0.5s;
-moz-transition: padding-left 0.5s, margin-left 0.5s, left 0.5s;
-o-transition: padding-left 0.5s, margin-left 0.5s, left 0.5s;
-ms-transition: padding-left 0.5s, margin-left 0.5s, left 0.5s;
transition: padding-left 0.5s, margin-left 0.5s, left 0.5s;
}
.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 {
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 (min-width: 620px) {
.sidebar-visible .page-wrapper {
-webkit-transform: none;
-moz-transform: none;
-o-transform: none;
-ms-transform: none;
transform: none;
margin-left: 300px;
}
}
.page {
outline: 0;
padding: 0 15px;
}
.content {
position: relative;
top: 0;
bottom: 0;
overflow-y: auto;
right: 0;
left: 0;
padding: 0 15px;
padding-bottom: 50px;
}
.sidebar-visible .content {
position: absolute;
top: 52px;
}
.content > main {
.content main {
margin-left: auto;
margin-right: auto;
max-width: 750px;
@@ -205,27 +206,35 @@ 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 {
position: relative;
margin: 0 10px;
margin: 0 8px;
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;
@media only screen and (max-width: 420px) {
#menu-bar i,
#menu-bar .icon-button {
margin: 0 5px;
}
}
#menu-bar #print-button {
margin: 0 15px;
}
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
-webkit-transform: translateY(-60px);
@@ -234,6 +243,12 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
-ms-transform: translateY(-60px);
transform: translateY(-60px);
}
.left-buttons {
margin: 0 5px;
}
.no-js .left-buttons {
display: none;
}
.menu-title {
display: inline-block;
font-weight: 200;
@@ -252,6 +267,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 {
@@ -363,7 +380,6 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.light .content .header:link,
.light .content .header:visited {
color: #333;
pointer: cursor;
}
.light .content .header:link:hover,
.light .content .header:visited:hover {
@@ -554,7 +570,6 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.coal .content .header:link,
.coal .content .header:visited {
color: #98a3ad;
pointer: cursor;
}
.coal .content .header:link:hover,
.coal .content .header:visited:hover {
@@ -745,7 +760,6 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.navy .content .header:link,
.navy .content .header:visited {
color: #bcbdd0;
pointer: cursor;
}
.navy .content .header:link:hover,
.navy .content .header:visited:hover {
@@ -936,7 +950,6 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.rust .content .header:link,
.rust .content .header:visited {
color: #262625;
pointer: cursor;
}
.rust .content .header:link:hover,
.rust .content .header:visited:hover {
@@ -1127,7 +1140,6 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.ayu .content .header:link,
.ayu .content .header:visited {
color: #c5c5c5;
pointer: cursor;
}
.ayu .content .header:link:hover,
.ayu .content .header:visited:hover {
@@ -1316,12 +1328,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;
@@ -1355,16 +1369,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 {
@@ -1401,18 +1413,18 @@ mark {
-o-transition: background-color 300ms linear;
-ms-transition: background-color 300ms linear;
transition: background-color 300ms linear;
cursor: pointer;
}
.fade-out {
mark.fade-out {
background-color: rgba(0,0,0,0) !important;
cursor: auto;
}
.searchbar-outer {
display: none;
margin-left: auto;
margin-right: auto;
max-width: 750px;
}
#searchbar {
display: block;
width: 100%;
margin: 5px auto 0px auto;
padding: 10px 16px;
@@ -1428,7 +1440,6 @@ mark {
padding: 18px 0 0 5px;
}
.searchresults-outer {
display: none;
margin-left: auto;
margin-right: auto;
max-width: 750px;

View File

@@ -1,3 +1,5 @@
"use strict";
// Fix back button cache problem
window.onunload = function () { };
@@ -16,13 +18,23 @@ function playpen_text(playpen) {
(function codeSnippets() {
// Hide Rust code lines prepended with a specific character
var hiding_character = "#";
var request = fetch("https://play.rust-lang.org/meta/crates", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
});
var playpens = Array.from(document.querySelectorAll(".playpen"));
if (playpens.length > 0) {
fetch("https://play.rust-lang.org/meta/crates", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
})
.then(response => response.json())
.then(response => {
// get list of crates available in the rust playground
let playground_crates = response.crates.map(item => item["id"]);
playpens.forEach(block => handle_crate_list_update(block, playground_crates));
});
}
function handle_crate_list_update(playpen_block, playground_crates) {
// update the play buttons after receiving the response
@@ -55,6 +67,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]);
}
@@ -147,9 +160,11 @@ function playpen_text(playpen) {
var lines = code_block.innerHTML.split("\n");
var first_non_hidden_line = false;
var lines_hidden = false;
var trimmed_line = "";
for (var n = 0; n < lines.length; n++) {
if (lines[n].trim()[0] == hiding_character) {
trimmed_line = lines[n].trim();
if (trimmed_line[0] == hiding_character && trimmed_line[1] != hiding_character) {
if (first_non_hidden_line) {
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)# ?/, "$1") + "</span>";
}
@@ -164,6 +179,9 @@ function playpen_text(playpen) {
else {
first_non_hidden_line = true;
}
if (trimmed_line[0] == hiding_character && trimmed_line[1] == hiding_character) {
lines[n] = lines[n].replace("##", "#")
}
}
code_block.innerHTML = lines.join("");
@@ -175,7 +193,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 +231,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 +240,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 +251,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 +266,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 +280,7 @@ function playpen_text(playpen) {
undoChangesButton.title = 'Undo changes';
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
buttons.prepend(undoChangesButton);
buttons.insertBefore(undoChangesButton, buttons.firstChild);
undoChangesButton.addEventListener('click', function () {
let editor = window.ace.edit(code_block);
@@ -271,17 +289,6 @@ function playpen_text(playpen) {
});
}
});
request
.then(function (response) { return response.json(); })
.then(function (response) {
// get list of crates available in the rust playground
let playground_crates = response.crates.map(function (item) { return item["id"]; });
Array.from(document.querySelectorAll(".playpen")).forEach(function (block) {
handle_crate_list_update(block, playground_crates);
});
});
})();
(function themes() {
@@ -290,9 +297,9 @@ function playpen_text(playpen) {
var themePopup = document.getElementById('theme-list');
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
var stylesheets = {
ayuHighlight: document.querySelector("[href='ayu-highlight.css']"),
tomorrowNight: document.querySelector("[href='tomorrow-night.css']"),
highlight: document.querySelector("[href='highlight.css']"),
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
highlight: document.querySelector("[href$='highlight.css']"),
};
function showThemes() {

View File

@@ -1,5 +1,5 @@
<!DOCTYPE HTML>
<html lang="{{ language }}" class="sidebar-visible">
<html lang="{{ language }}" class="sidebar-visible no-js">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
@@ -9,47 +9,29 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<base href="{{ path_to_root }}">
<link rel="stylesheet" href="book.css">
<link rel="stylesheet" href="{{ path_to_root }}book.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
<link rel="shortcut icon" href="{{ favicon }}">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link rel="stylesheet" href="{{ path_to_root }}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">
<link rel="stylesheet" href="{{ path_to_root }}highlight.css">
<link rel="stylesheet" href="{{ path_to_root }}tomorrow-night.css">
<link rel="stylesheet" href="{{ path_to_root }}ayu-highlight.css">
<!-- 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>
<noscript>
<style type="text/css">
.javascript-only {
display: none;
}
</style>
</noscript>
</head>
<body class="light">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
@@ -74,7 +56,7 @@
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = 'light'; }
document.body.className = theme;
document.querySelector('html').className = theme;
document.querySelector('html').className = theme + ' js';
</script>
<!-- Hide / unhide sidebar before it is displayed -->
@@ -99,7 +81,7 @@
{{> header}}
<div id="menu-bar" class="menu-bar">
<div id="menu-bar-sticky-container">
<div class="left-buttons javascript-only">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
@@ -123,7 +105,7 @@
<h1 class="menu-title">{{ book_title }}</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
@@ -131,13 +113,15 @@
</div>
{{#if search_enabled}}
<div id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</div>
<div id="searchresults-outer" class="searchresults-outer">
<div class="searchresults-header" id="searchresults-header"></div>
<ul id="searchresults">
</ul>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
{{/if}}
@@ -158,13 +142,13 @@
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
{{#previous}}
<a rel="prev" href="{{link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<a rel="prev" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a rel="next" href="{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
@@ -176,13 +160,13 @@
<nav class="nav-wide-wrapper" aria-label="Page navigation">
{{#previous}}
<a href="{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<a href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a href="{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<a href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
@@ -190,18 +174,6 @@
</div>
<!-- Local fallback for Font Awesome -->
<script>
if (getComputedStyle(document.querySelector(".fa")).fontFamily !== "FontAwesome") {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = '_FontAwesome/css/font-awesome.css';
document.head.insertBefore(link, document.head.firstChild)
}
</script>
{{#if livereload}}
<!-- Livereload script (if served using the cli tool) -->
<script type="text/javascript">
@@ -221,7 +193,7 @@
{{#if google_analytics}}
<!-- Google Analytics Tag -->
<script>
<script type="text/javascript">
var localAddrs = ["localhost", "127.0.0.1", ""];
// make sure we don't activate google analytics if the developer is
@@ -239,37 +211,51 @@
{{/if}}
{{#if playpen_js}}
<script src="ace.js" type="text/javascript" charset="utf-8"></script>
<script src="editor.js" type="text/javascript" charset="utf-8"></script>
<script src="mode-rust.js" type="text/javascript" charset="utf-8"></script>
<script src="theme-dawn.js" type="text/javascript" charset="utf-8"></script>
<script src="theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
{{#if search_enabled}}
<script src="searchindex.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
{{#if search_js}}
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
{{#if is_print}}
<script src="{{ path_to_root }}searchindex.js" type="text/javascript" charset="utf-8"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
window.print();
})
var path_to_root = "{{path_to_root}}";
</script>
{{/if}}
{{#if search_js}}
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
<script src="highlight.js"></script>
<script src="book.js"></script>
<script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS script -->
<!-- Custom JS scripts -->
{{#each additional_js}}
<script type="text/javascript" src="{{this}}"></script>
<script type="text/javascript" src="{{ path_to_root }}{{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

@@ -5,9 +5,9 @@ pub mod playpen_editor;
#[cfg(feature = "search")]
pub mod searcher;
use std::path::Path;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use errors::*;
@@ -21,22 +21,21 @@ pub static TOMORROW_NIGHT_CSS: &'static [u8] = include_bytes!("tomorrow-night.cs
pub static HIGHLIGHT_CSS: &'static [u8] = include_bytes!("highlight.css");
pub static AYU_HIGHLIGHT_CSS: &'static [u8] = include_bytes!("ayu-highlight.css");
pub static CLIPBOARD_JS: &'static [u8] = include_bytes!("clipboard.min.js");
pub static FONT_AWESOME: &'static [u8] = include_bytes!("_FontAwesome/css/font-awesome.min.css");
pub static FONT_AWESOME: &'static [u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
pub static FONT_AWESOME_EOT: &'static [u8] =
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.eot");
include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
pub static FONT_AWESOME_SVG: &'static [u8] =
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.svg");
include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
pub static FONT_AWESOME_TTF: &'static [u8] =
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.ttf");
include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
pub static FONT_AWESOME_WOFF: &'static [u8] =
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff");
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
pub static FONT_AWESOME_WOFF2: &'static [u8] =
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff2");
pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/FontAwesome.otf");
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff2");
pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
/// The `Theme` struct should be used instead of the static variables because
/// the `new()` method will look if the user has a theme directory in his
/// the `new()` method will look if the user has a theme directory in their
/// source folder and use the users theme instead of the default.
///
/// You should only ever use the static variables directly if you want to
@@ -78,8 +77,14 @@ impl Theme {
(theme_dir.join("highlight.js"), &mut theme.highlight_js),
(theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
(theme_dir.join("highlight.css"), &mut theme.highlight_css),
(theme_dir.join("tomorrow-night.css"), &mut theme.tomorrow_night_css),
(theme_dir.join("ayu-highlight.css"), &mut theme.ayu_highlight_css),
(
theme_dir.join("tomorrow-night.css"),
&mut theme.tomorrow_night_css,
),
(
theme_dir.join("ayu-highlight.css"),
&mut theme.ayu_highlight_css,
),
];
for (filename, dest) in files {
@@ -130,12 +135,11 @@ fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::Builder as TempFileBuilder;
use std::path::PathBuf;
use tempfile::Builder as TempFileBuilder;
#[test]
fn theme_uses_defaults_with_nonexistent_src_dir() {

View File

@@ -1,3 +1,4 @@
"use strict";
window.editors = [];
(function(editors) {
if (typeof(ace) === 'undefined' || !ace) {
@@ -11,7 +12,8 @@ window.editors = [];
showPrintMargin: false,
showLineNumbers: false,
showGutter: false,
maxLines: Infinity
maxLines: Infinity,
fontSize: "0.875em" // please adjust the font size of the code in general.styl
});
editor.$blockScrolling = Infinity;

View File

@@ -1,15 +1,24 @@
"use strict";
window.search = window.search || {};
(function search(search) {
// Search functionality
//
// You can use !hasFocus() to prevent keyhandling in your key
// event handlers while the user is typing his search.
// event handlers while the user is typing their search.
if (!Mark || !elasticlunr) {
return;
}
var searchbar = document.getElementById('searchbar'),
//IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
if (!String.prototype.startsWith) {
String.prototype.startsWith = function(search, pos) {
return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
};
}
var search_wrap = document.getElementById('search-wrapper'),
searchbar = document.getElementById('searchbar'),
searchbar_outer = document.getElementById('searchbar-outer'),
searchresults = document.getElementById('searchresults'),
searchresults_outer = document.getElementById('searchresults-outer'),
@@ -18,11 +27,12 @@ window.search = window.search || {};
content = document.getElementById('content'),
searchindex = null,
resultsoptions = {
doc_urls = [],
results_options = {
teaser_word_count: 30,
limit_results: 30,
},
searchoptions = {
search_options = {
bool: "AND",
expand: true,
fields: {
@@ -130,12 +140,12 @@ window.search = window.search || {};
teaser_count++;
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
var url = result.ref.split("#");
var url = doc_urls[result.ref].split("#");
if (url.length == 1) { // no anchor found
url.push("");
}
return '<a href="' + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
+ teaser + '</span>';
@@ -187,7 +197,7 @@ window.search = window.search || {};
}
var window_weight = [];
var window_size = Math.min(weighted.length, resultsoptions.teaser_word_count);
var window_size = Math.min(weighted.length, results_options.teaser_word_count);
var cur_sum = 0;
for (var wordindex = 0; wordindex < window_size; wordindex++) {
@@ -237,17 +247,21 @@ window.search = window.search || {};
return teaser_split.join('');
}
function init() {
resultsoptions = window.search.resultsoptions;
searchoptions = window.search.searchoptions;
searchindex = elasticlunr.Index.load(window.search.index);
function init(config) {
results_options = config.results_options;
search_options = config.search_options;
searchbar_outer = config.searchbar_outer;
doc_urls = config.doc_urls;
searchindex = elasticlunr.Index.load(config.index);
// Set up events
searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false);
searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false);
document.addEventListener('keydown', function (e) { globalKeyHandler(e); }, false);
document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false);
// If the user uses the browser buttons, do the same as if a reload happened
window.onpopstate = function(e) { doSearchOrMarkFromUrl(); };
// Suppress "submit" events so the page doesn't reload when the user presses Enter
document.addEventListener('submit', function(e) { e.preventDefault(); }, false);
// If reloaded, do the search or mark again, depending on the current url parameters
doSearchOrMarkFromUrl();
@@ -297,94 +311,84 @@ window.search = window.search || {};
// Eventhandler for keyevents on `document`
function globalKeyHandler(e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea') { return; }
if (e.keyCode == ESCAPE_KEYCODE) {
if (e.keyCode === ESCAPE_KEYCODE) {
e.preventDefault();
searchbar.classList.remove("active");
setSearchUrlParameters("",
(searchbar.value.trim() != "") ? "push" : "replace");
(searchbar.value.trim() !== "") ? "push" : "replace");
if (hasFocus()) {
unfocusSearchbar();
}
showSearch(false);
marker.unmark();
return;
}
if (!hasFocus() && e.keyCode == SEARCH_HOTKEY_KEYCODE) {
} else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) {
e.preventDefault();
showSearch(true);
window.scrollTo(0, 0);
searchbar.focus();
return;
}
if (hasFocus() && e.keyCode == DOWN_KEYCODE) {
searchbar.select();
} else if (hasFocus() && e.keyCode === DOWN_KEYCODE) {
e.preventDefault();
unfocusSearchbar();
searchresults.children('li').first().classList.add("focus");
return;
}
if (!hasFocus() && (e.keyCode == DOWN_KEYCODE
|| e.keyCode == UP_KEYCODE
|| e.keyCode == SELECT_KEYCODE)) {
searchresults.firstElementChild.classList.add("focus");
} else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE
|| e.keyCode === UP_KEYCODE
|| e.keyCode === SELECT_KEYCODE)) {
// not `:focus` because browser does annoying scrolling
var current_focus = search.searchresults.find("li.focus");
if (current_focus.length == 0) return;
var focused = searchresults.querySelector("li.focus");
if (!focused) return;
e.preventDefault();
if (e.keyCode == DOWN_KEYCODE) {
var next = current_focus.next()
if (next.length > 0) {
current_focus.classList.remove("focus");
if (e.keyCode === DOWN_KEYCODE) {
var next = focused.nextElementSibling;
if (next) {
focused.classList.remove("focus");
next.classList.add("focus");
}
} else if (e.keyCode == UP_KEYCODE) {
current_focus.classList.remove("focus");
var prev = current_focus.prev();
if (prev.length == 0) {
searchbar.focus();
} else {
} else if (e.keyCode === UP_KEYCODE) {
focused.classList.remove("focus");
var prev = focused.previousElementSibling;
if (prev) {
prev.classList.add("focus");
} else {
searchbar.select();
}
} else {
window.location = current_focus.children('a').attr('href');
} else { // SELECT_KEYCODE
window.location.assign(focused.querySelector('a'));
}
}
}
function showSearch(yes) {
if (yes) {
searchbar_outer.style.display = 'block';
content.style.display = 'none';
search_wrap.classList.remove('hidden');
searchicon.setAttribute('aria-expanded', 'true');
} else {
content.style.display = 'block';
searchbar_outer.style.display = 'none';
searchresults_outer.style.display = 'none';
searchbar.value = '';
removeChildren(searchresults);
search_wrap.classList.add('hidden');
searchicon.setAttribute('aria-expanded', 'false');
var results = searchresults.children;
for (var i = 0; i < results.length; i++) {
results[i].classList.remove("focus");
}
}
}
function showResults(yes) {
if (yes) {
searchbar_outer.style.display = 'block';
content.style.display = 'none';
searchresults_outer.style.display = 'block';
searchresults_outer.classList.remove('hidden');
} else {
content.style.display = 'block';
searchresults_outer.style.display = 'none';
searchresults_outer.classList.add('hidden');
}
}
// Eventhandler for search icon
function searchIconClickHandler() {
if (searchbar_outer.style.display === 'block') {
showSearch(false);
} else {
if (search_wrap.classList.contains('hidden')) {
showSearch(true);
window.scrollTo(0, 0);
searchbar.focus();
searchbar.select();
} else {
showSearch(false);
}
}
@@ -439,8 +443,8 @@ window.search = window.search || {};
if (searchindex == null) { return; }
// Do the actual search
var results = searchindex.search(searchterm, searchoptions);
var resultcount = Math.min(results.length, resultsoptions.limit_results);
var results = searchindex.search(searchterm, search_options);
var resultcount = Math.min(results.length, results_options.limit_results);
// Display search metrics
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
@@ -458,7 +462,16 @@ window.search = window.search || {};
showResults(true);
}
init();
fetch(path_to_root + 'searchindex.json')
.then(response => response.json())
.then(json => init(json))
.catch(error => { // Try to load searchindex.js if fetch failed
var script = document.createElement('script');
script.src = path_to_root + 'searchindex.js';
script.onload = () => init(window.search);
document.head.appendChild(script);
});
// Exported functions
search.hasFocus = hasFocus;
})(window.search);

View File

@@ -1,6 +1,7 @@
html {
font-family: "Open Sans", sans-serif
color: #333
text-size-adjust: none
}
body {
@@ -11,7 +12,7 @@ body {
code {
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
font-size: 0.875em;
font-size: 0.875em; // please adjust the ace font size accordingly in editor.js
}
.left {

View File

@@ -7,26 +7,37 @@
& > #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 {
position: relative
margin: 0 10px
margin: 0 8px
@media only screen and (max-width: $narrow-device-max-width) {
margin: 0 5px
}
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);
}
.left-buttons {
.no-js & { display: none }
margin: 0 5px
}
.menu-title {
display: inline-block
font-weight: 200
@@ -38,5 +49,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

@@ -2,18 +2,20 @@
.page-wrapper {
box-sizing: border-box
left: 0
position: absolute
right: 0
top: 0
bottom: 0
// Animation: slide away
transition: padding-left 0.5s, margin-left 0.5s, left 0.5s
.js & {
transition: margin-left 0.3s ease, transform 0.3s ease
}
}
.sidebar-visible .page-wrapper {
left: $sidebar-width
transform: translateX($sidebar-width)
@media only screen and (min-width: $sidebar-reflow-width) {
transform: none
margin-left: $sidebar-width
}
}
.page {
@@ -22,12 +24,7 @@
}
.content {
position: relative
top: 0
bottom: 0
overflow-y: auto
right: 0
left: 0
padding: 0 15px
padding-bottom: 50px
@@ -44,8 +41,3 @@
img { max-width: 100%; }
}
.sidebar-visible .content {
position: absolute
top: 52px
}

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

@@ -9,21 +9,21 @@ mark {
padding: 0 3px 1px 3px;
margin: 0 -3px -1px -3px;
transition: background-color 300ms linear;
cursor: pointer;
}
.fade-out {
background-color: rgba(0,0,0,0) !important
mark.fade-out {
background-color: rgba(0,0,0,0) !important;
cursor: auto;
}
.searchbar-outer {
display: none;
margin-left: auto;
margin-right: auto;
max-width: $content-max-width;
}
#searchbar {
display: block;
width: 100%;
margin: 5px auto 0px auto;
padding: 10px 16px;
@@ -37,7 +37,6 @@ mark {
}
.searchresults-outer {
display: none;
margin-left: auto;
margin-right: auto;
max-width: $content-max-width;

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;
@@ -51,10 +53,4 @@
list-style: none outside none
padding-left: 20px
line-height: 1.9em
li {
text-overflow: ellipsis
overflow: hidden
white-space: nowrap
}
}

View File

@@ -5,7 +5,6 @@
.content .header:link, .content .header:visited {
color: $fg;
pointer: cursor;
&:hover {
text-decoration: none;

View File

@@ -1,4 +1,7 @@
$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
$narrow-device-max-width = 420px

View File

@@ -25,16 +25,10 @@ pub fn normalize_path(path: &str) -> String {
}
/// Write the given data to a file, creating it first if necessary
pub fn write_file<P: AsRef<Path>>(
build_dir: &Path,
filename: P,
content: &[u8],
) -> Result<()> {
pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8]) -> Result<()> {
let path = build_dir.join(filename);
create_file(&path)?
.write_all(content)
.map_err(|e| e.into())
create_file(&path)?.write_all(content).map_err(|e| e.into())
}
/// Takes a path and returns a path containing just enough `../` to point to

View File

@@ -5,12 +5,13 @@ mod string;
use errors::Error;
use regex::Regex;
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
OPTION_ENABLE_TABLES};
use pulldown_cmark::{
html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES, OPTION_ENABLE_TABLES,
};
use std::borrow::Cow;
pub use self::string::{RangeArgument, take_lines};
pub use self::string::{take_lines, RangeArgument};
/// Replaces multiple consecutive whitespace characters with a single space character.
pub fn collapse_whitespace<'a>(text: &'a str) -> Cow<'a, str> {
@@ -35,7 +36,10 @@ pub fn normalize_id(content: &str) -> String {
})
.collect::<String>();
// Ensure that the first character is [A-Za-z]
if ret.chars().next().map_or(false, |c| !c.is_ascii_alphabetic()) {
if ret.chars()
.next()
.map_or(false, |c| !c.is_ascii_alphabetic())
{
ret.insert(0, 'a');
}
ret
@@ -47,17 +51,19 @@ pub fn id_from_content(content: &str) -> String {
let mut content = content.to_string();
// Skip any tags or html-encoded stuff
const REPL_SUB: &[&str] = &["<em>",
"</em>",
"<code>",
"</code>",
"<strong>",
"</strong>",
"&lt;",
"&gt;",
"&amp;",
"&#39;",
"&quot;"];
const REPL_SUB: &[&str] = &[
"<em>",
"</em>",
"<code>",
"</code>",
"<strong>",
"</strong>",
"&lt;",
"&gt;",
"&amp;",
"&#39;",
"&quot;",
];
for sub in REPL_SUB {
content = content.replace(sub, "");
}
@@ -68,6 +74,32 @@ pub fn id_from_content(content: &str) -> String {
normalize_id(trimmed)
}
fn adjust_links(event: Event) -> Event {
lazy_static! {
static ref HTTP_LINK: Regex = Regex::new("^https?://").unwrap();
static ref MD_LINK: Regex = Regex::new("(?P<link>.*).md(?P<anchor>#.*)?").unwrap();
}
match event {
Event::Start(Tag::Link(dest, title)) => {
if !HTTP_LINK.is_match(&dest) {
if let Some(caps) = MD_LINK.captures(&dest) {
let mut html_link = [&caps["link"], ".html"].concat();
if let Some(anchor) = caps.name("anchor") {
html_link.push_str(anchor.as_str());
}
return Event::Start(Tag::Link(Cow::from(html_link), title));
}
}
Event::Start(Tag::Link(dest, title))
}
_ => event,
}
}
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2);
@@ -79,7 +111,8 @@ pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
let p = Parser::new_ext(text, opts);
let mut converter = EventQuoteConverter::new(curly_quotes);
let events = p.map(clean_codeblock_headers)
.map(|event| converter.convert(event));
.map(adjust_links)
.map(|event| converter.convert(event));
html::push_html(&mut s, events);
s
@@ -131,36 +164,36 @@ fn clean_codeblock_headers(event: Event) -> Event {
}
}
fn convert_quotes_to_curly(original_text: &str) -> String {
// We'll consider the start to be "whitespace".
let mut preceded_by_whitespace = true;
original_text.chars()
.map(|original_char| {
let converted_char = match original_char {
'\'' => {
if preceded_by_whitespace {
''
} else {
''
original_text
.chars()
.map(|original_char| {
let converted_char = match original_char {
'\'' => {
if preceded_by_whitespace {
''
} else {
''
}
}
}
'"' => {
if preceded_by_whitespace {
'“'
} else {
'”'
'"' => {
if preceded_by_whitespace {
'“'
} else {
'”'
}
}
}
_ => original_char,
};
_ => original_char,
};
preceded_by_whitespace = original_char.is_whitespace();
preceded_by_whitespace = original_char.is_whitespace();
converted_char
})
.collect()
converted_char
})
.collect()
}
/// Prints a "backtrace" of some `Error`.
@@ -177,6 +210,26 @@ mod tests {
mod render_markdown {
use super::super::render_markdown;
#[test]
fn preserves_external_links() {
assert_eq!(
render_markdown("[example](https://www.rust-lang.org/)", false),
"<p><a href=\"https://www.rust-lang.org/\">example</a></p>\n"
);
}
#[test]
fn it_can_adjust_markdown_links() {
assert_eq!(
render_markdown("[example](example.md)", false),
"<p><a href=\"example.html\">example</a></p>\n"
);
assert_eq!(
render_markdown("[example_anchor](example.md#anchor)", false),
"<p><a href=\"example.html#anchor\">example_anchor</a></p>\n"
);
}
#[test]
fn it_can_keep_quotes_straight() {
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
@@ -275,18 +328,26 @@ more text with spaces
#[test]
fn it_generates_anchors() {
assert_eq!(id_from_content("## `--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes");
assert_eq!(id_from_content("## Method-call expressions"),
"method-call-expressions");
assert_eq!(
id_from_content("## `--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes"
);
assert_eq!(
id_from_content("## Method-call expressions"),
"method-call-expressions"
);
}
#[test]
fn it_normalizes_ids() {
assert_eq!(normalize_id("`--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes");
assert_eq!(normalize_id("Method-call 🐙 expressions \u{1f47c}"),
"method-call--expressions-");
assert_eq!(
normalize_id("`--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes"
);
assert_eq!(
normalize_id("Method-call 🐙 expressions \u{1f47c}"),
"method-call--expressions-"
);
assert_eq!(normalize_id("_-_12345"), "a_-_12345");
assert_eq!(normalize_id("12345"), "a12345");
assert_eq!(normalize_id(""), "");
@@ -298,14 +359,18 @@ more text with spaces
#[test]
fn it_converts_single_quotes() {
assert_eq!(convert_quotes_to_curly("'one', 'two'"),
"one, two");
assert_eq!(
convert_quotes_to_curly("'one', 'two'"),
"one, two"
);
}
#[test]
fn it_converts_double_quotes() {
assert_eq!(convert_quotes_to_curly(r#""one", "two""#),
"one, two");
assert_eq!(
convert_quotes_to_curly(r#""one", "two""#),
"“one”, “two”"
);
}
#[test]

View File

@@ -1,5 +1,5 @@
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
use itertools::Itertools;
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
// This trait is already contained in the standard lib, however it is unstable.
// TODO: Remove when the `collections_range` feature stabilises
@@ -50,7 +50,7 @@ pub fn take_lines<R: RangeArgument<usize>>(s: &str, range: R) -> String {
let start = *range.start().unwrap_or(&0);
let mut lines = s.lines().skip(start);
match range.end() {
Some(&end) => lines.take(end.checked_sub(start).unwrap_or(0)).join("\n"),
Some(&end) => lines.take(end.saturating_sub(start)).join("\n"),
None => lines.join("\n"),
}
}

View File

@@ -3,11 +3,11 @@
extern crate mdbook;
extern crate tempfile;
#[cfg(not(windows))]
use std::path::Path;
use tempfile::{TempDir, Builder as TempFileBuilder};
use mdbook::config::Config;
use mdbook::MDBook;
#[cfg(not(windows))]
use std::path::Path;
use tempfile::{Builder as TempFileBuilder, TempDir};
#[test]
fn passing_alternate_backend() {
@@ -52,8 +52,8 @@ fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
#[test]
#[cfg(not(windows))]
fn backends_receive_render_context_via_stdin() {
use std::fs::File;
use mdbook::renderer::RenderContext;
use std::fs::File;
let temp = TempFileBuilder::new().prefix("output").tempdir().unwrap();
let out_file = temp.path().join("out.txt");

View File

@@ -7,19 +7,18 @@ extern crate mdbook;
extern crate tempfile;
extern crate walkdir;
use std::path::Path;
use std::fs::{self, File};
use std::io::{Read, Write};
use mdbook::errors::*;
use mdbook::utils::fs::file_to_string;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
// The funny `self::` here is because we've got an `extern crate ...` and are
// in a submodule
use self::tempfile::{TempDir, Builder as TempFileBuilder};
use self::mdbook::MDBook;
use self::tempfile::{Builder as TempFileBuilder, TempDir};
use self::walkdir::WalkDir;
/// Create a dummy book in a temporary directory, using the contents of
/// `SUMMARY_MD` as a guide.
///
@@ -47,13 +46,16 @@ impl DummyBook {
/// Write a book to a temporary directory using the provided settings.
pub fn build(&self) -> Result<TempDir> {
let temp = TempFileBuilder::new().prefix("dummy_book").tempdir().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(|| {
"Couldn't copy files into a \
temporary directory"
})?;
"Couldn't copy files into a \
temporary directory"
})?;
let sub_pattern = if self.passing_test { "true" } else { "false" };
let file_containing_test = temp.path().join("src/first/nested.md");
@@ -77,11 +79,13 @@ pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
let content = file_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(content.contains(s),
"Searching for {:?} in {}\n\n{}",
s,
filename.display(),
content);
assert!(
content.contains(s),
"Searching for {:?} in {}\n\n{}",
s,
filename.display(),
content
);
}
}
@@ -90,15 +94,16 @@ pub fn assert_doesnt_contain_strings<P: AsRef<Path>>(filename: P, strings: &[&st
let content = file_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(!content.contains(s),
"Found {:?} in {}\n\n{}",
s,
filename.display(),
content);
assert!(
!content.contains(s),
"Found {:?} in {}\n\n{}",
s,
filename.display(),
content
);
}
}
/// Recursively copy an entire directory tree to somewhere else (a la `cp -r`).
fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()> {
let from = from.as_ref();
@@ -108,9 +113,9 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
let entry = entry.chain_err(|| "Unable to inspect directory entry")?;
let original_location = entry.path();
let relative = original_location.strip_prefix(&from)
.expect("`original_location` is inside the `from` \
directory");
let relative = original_location
.strip_prefix(&from)
.expect("`original_location` is inside the `from` directory");
let new_location = to.join(relative);
if original_location.is_file() {
@@ -118,9 +123,8 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
fs::create_dir_all(parent).chain_err(|| "Couldn't create directory")?;
}
fs::copy(&original_location, &new_location).chain_err(|| {
"Unable to copy file contents"
})?;
fs::copy(&original_location, &new_location)
.chain_err(|| "Unable to copy file contents")?;
}
}
@@ -129,7 +133,7 @@ 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 = TempFileBuilder::new().prefix("book-example").tempdir()?;
let book_example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
recursive_copy(book_example, temp.path())?;

View File

@@ -0,0 +1,5 @@
# Dummy Book
This file is just here to cause the index preprocessor to run.
Does a pretty good job, too.

View File

@@ -1,10 +1,12 @@
# Summary
[Dummy Book](README.md)
[Introduction](intro.md)
- [First Chapter](first/index.md)
- [Nested Chapter](first/nested.md)
- [Includes](first/includes.md)
- [Recursive](first/recursive.md)
- [Second Chapter](second.md)
---

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,13 +1,12 @@
extern crate mdbook;
extern crate tempfile;
use std::path::PathBuf;
use std::fs;
use mdbook::MDBook;
use mdbook::config::Config;
use mdbook::MDBook;
use std::fs;
use std::path::PathBuf;
use tempfile::Builder as TempFileBuilder;
/// Run `mdbook init` in an empty directory and make sure the default files
/// are created.
#[test]

View File

@@ -5,10 +5,10 @@ extern crate env_logger;
extern crate error_chain;
extern crate mdbook;
use mdbook::book;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use mdbook::book;
macro_rules! summary_md_test {
($name:ident, $filename:expr) => {

View File

@@ -9,27 +9,29 @@ mod dummy_book;
use dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, DummyBook};
use mdbook::config::Config;
use mdbook::errors::*;
use mdbook::utils::fs::{file_to_string, write_file};
use mdbook::MDBook;
use select::document::Document;
use select::predicate::{Class, Name, Predicate};
use std::ffi::OsStr;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::ffi::OsStr;
use walkdir::{DirEntry, WalkDir};
use select::document::Document;
use select::predicate::{Class, Name, Predicate};
use tempfile::Builder as TempFileBuilder;
use mdbook::errors::*;
use mdbook::utils::fs::file_to_string;
use mdbook::config::Config;
use mdbook::MDBook;
use walkdir::{DirEntry, WalkDir};
const BOOK_ROOT: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/dummy_book");
const TOC_TOP_LEVEL: &[&'static str] = &[
"1. First Chapter",
"2. Second Chapter",
"Conclusion",
"Dummy Book",
"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]
@@ -83,12 +85,11 @@ fn check_correct_cross_links_in_nested_dir() {
let first = temp.path().join("book").join("first");
let links = vec![
r#"<base href="../">"#,
r#"href="intro.html""#,
r#"href="first/index.html""#,
r#"href="first/nested.html""#,
r#"href="second.html""#,
r#"href="conclusion.html""#,
r#"href="../intro.html""#,
r#"href="../first/index.html""#,
r#"href="../first/nested.html""#,
r#"href="../second.html""#,
r#"href="../conclusion.html""#,
];
let files_in_nested_dir = vec!["index.html", "nested.html"];
@@ -99,16 +100,12 @@ fn check_correct_cross_links_in_nested_dir() {
assert_contains_strings(
first.join("index.html"),
&[
r##"href="first/index.html#some-section" id="some-section""##,
],
&[r##"href="#some-section" id="some-section""##],
);
assert_contains_strings(
first.join("nested.html"),
&[
r##"href="first/nested.html#some-section" id="some-section""##,
],
&[r##"href="#some-section" id="some-section""##],
);
}
@@ -130,7 +127,8 @@ fn rendered_code_has_playpen_stuff() {
#[test]
fn chapter_content_appears_in_rendered_document() {
let content = vec![
("index.html", "Here's some interesting text"),
("index.html", "This file is just here to cause the"),
("intro.html", "Here's some interesting text"),
("second.html", "Second Chapter"),
("first/nested.html", "testable code"),
("first/index.html", "more text"),
@@ -313,6 +311,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();
@@ -340,14 +352,55 @@ fn book_with_a_reserved_filename_does_not_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 dummy_book::DummyBook;
use mdbook::utils::fs::file_to_string;
use mdbook::MDBook;
use dummy_book::DummyBook;
use std::fs::File;
use std::path::Path;
fn read_book_index(root: &Path) -> serde_json::Value {
let index = root.join("book/searchindex.js");
@@ -365,27 +418,31 @@ mod search {
let index = read_book_index(temp.path());
let doc_urls = index["doc_urls"].as_array().unwrap();
let get_doc_ref =
|url: &str| -> String { doc_urls.iter().position(|s| s == url).unwrap().to_string() };
let first_chapter = get_doc_ref("first/index.html#first-chapter");
let introduction = get_doc_ref("intro.html#introduction");
let some_section = get_doc_ref("first/index.html#some-section");
let summary = get_doc_ref("first/includes.html#summary");
let conclusion = get_doc_ref("conclusion.html#conclusion");
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);
assert_eq!(textidx["docs"][&first_chapter]["tf"], 1.0);
assert_eq!(textidx["docs"][&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_chapter]["body"], "more text.");
assert_eq!(docs[&some_section]["body"], "");
assert_eq!(
docs["first/includes.html#summary"]["body"],
"Introduction First Chapter Nested Chapter Includes Second Chapter Conclusion"
);
assert_eq!(
docs["first/includes.html#summary"]["breadcrumbs"],
"First Chapter » Summary"
);
assert_eq!(
docs["conclusion.html#conclusion"]["body"],
"I put &lt;HTML&gt; in here!"
docs[&summary]["body"],
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Second Chapter Conclusion"
);
assert_eq!(docs[&summary]["breadcrumbs"], "First Chapter » Summary");
assert_eq!(docs[&conclusion]["body"], "I put &lt;HTML&gt; in here!");
}
// Setting this to `true` may cause issues with `cargo watch`,
@@ -420,7 +477,7 @@ mod search {
//
// 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.
// **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();

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,11 @@ mod dummy_book;
use dummy_book::DummyBook;
use mdbook::MDBook;
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::book::Book;
use mdbook::config::Config;
use mdbook::errors::*;
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::MDBook;
use std::sync::{Arc, Mutex};
@@ -23,14 +23,13 @@ fn mdbook_can_correctly_test_a_passing_book() {
#[test]
fn mdbook_detects_book_with_failing_tests() {
let temp = DummyBook::new().with_passing_test(false).build().unwrap();
let mut md: MDBook = MDBook::load(temp.path()).unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
assert!(md.test(vec![]).is_err());
}
#[test]
fn mdbook_runs_preprocessors() {
let has_run: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
struct DummyPreprocessor(Arc<Mutex<bool>>);