mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 11:24:57 -05:00
Compare commits
303 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a5409db20 | ||
|
|
dc89a82329 | ||
|
|
42ff5a895c | ||
|
|
8ee795045a | ||
|
|
f22835f7bc | ||
|
|
1f84f66041 | ||
|
|
e735bc6d3e | ||
|
|
803df90efa | ||
|
|
b614b0fd65 | ||
|
|
32df76d077 | ||
|
|
dacc274e0d | ||
|
|
b0b09bad3f | ||
|
|
93874edebf | ||
|
|
1aa9c92ac1 | ||
|
|
5ce05a79be | ||
|
|
c51e080783 | ||
|
|
dd5d94393d | ||
|
|
3d5eb48e32 | ||
|
|
d56ff94ce6 | ||
|
|
fb99276f52 | ||
|
|
5eff572dbb | ||
|
|
238dfb7d1d | ||
|
|
c777913136 | ||
|
|
c25c5d72c8 | ||
|
|
3aa6436679 | ||
|
|
d37821c194 | ||
|
|
1b5137c84e | ||
|
|
18c725ee12 | ||
|
|
1743f2a39f | ||
|
|
cee3296a32 | ||
|
|
ddb0834da8 | ||
|
|
b74c2c18ef | ||
|
|
c056b5cbd0 | ||
|
|
8d7970b32d | ||
|
|
1d22a9a040 | ||
|
|
6059883229 | ||
|
|
79dd03e8e9 | ||
|
|
aecc403fb8 | ||
|
|
cd711bfb1c | ||
|
|
afd9ccb7b1 | ||
|
|
cb5ae21b89 | ||
|
|
dd3bef8000 | ||
|
|
7e5892bd35 | ||
|
|
56cee872e8 | ||
|
|
a554390aa2 | ||
|
|
c64384abc3 | ||
|
|
ba7d40284b | ||
|
|
8f6523a94c | ||
|
|
8fbc59720d | ||
|
|
ac9c150902 | ||
|
|
f2e56c887b | ||
|
|
b4a12fa723 | ||
|
|
382fc4139b | ||
|
|
b45e5e4420 | ||
|
|
a6d4881e00 | ||
|
|
a0515bd104 | ||
|
|
9b64db908f | ||
|
|
f562878131 | ||
|
|
3823fc0e74 | ||
|
|
793fb8f654 | ||
|
|
911683d2cf | ||
|
|
2ae6e6a6e3 | ||
|
|
91fd8a2865 | ||
|
|
a3b6e549e2 | ||
|
|
d450518292 | ||
|
|
c056df597a | ||
|
|
0d6adc5fc9 | ||
|
|
0226da91e4 | ||
|
|
ef5895fa78 | ||
|
|
8e0abfb22f | ||
|
|
c9bc13d786 | ||
|
|
7ce78cbfea | ||
|
|
26544fa531 | ||
|
|
0c93770f4a | ||
|
|
743713ad3a | ||
|
|
e3f4bb5101 | ||
|
|
441bcb5963 | ||
|
|
84ef4d2617 | ||
|
|
bd30cae17e | ||
|
|
6f0b67f44f | ||
|
|
abf86eefd9 | ||
|
|
016ec8836c | ||
|
|
881a1b39ff | ||
|
|
a1e58229b2 | ||
|
|
276eab095c | ||
|
|
f4513d3b5c | ||
|
|
570ce6681f | ||
|
|
ddee839d9c | ||
|
|
99945542ca | ||
|
|
956a5cc7fd | ||
|
|
cef62ec42e | ||
|
|
b1362bfa06 | ||
|
|
a529ca5e65 | ||
|
|
6bc3039b4f | ||
|
|
cd90fdd407 | ||
|
|
a6a7c95c78 | ||
|
|
0a4a2b66da | ||
|
|
2f3c14d609 | ||
|
|
ebcf1e495d | ||
|
|
40a4840867 | ||
|
|
313f9b9403 | ||
|
|
d94c097495 | ||
|
|
b7372d3bf2 | ||
|
|
d0a6aea3aa | ||
|
|
a1926bbe8e | ||
|
|
094c1e7a52 | ||
|
|
4528e24080 | ||
|
|
31983cae6c | ||
|
|
ddf31dcc08 | ||
|
|
c36eca15c2 | ||
|
|
798225bcdc | ||
|
|
eed1a0a591 | ||
|
|
35a447d08a | ||
|
|
16aa545c5b | ||
|
|
6601dbdd61 | ||
|
|
373e36ebfb | ||
|
|
ef435825b0 | ||
|
|
2f8d5ce263 | ||
|
|
ce2d7153f7 | ||
|
|
6628757d8e | ||
|
|
e2eb40bded | ||
|
|
4f754a73ba | ||
|
|
ba719c00be | ||
|
|
64f6f78663 | ||
|
|
3b0d2d1238 | ||
|
|
6ab3d3da2a | ||
|
|
ee29b9d5f6 | ||
|
|
d7ecb1a80c | ||
|
|
f3f6b40ea9 | ||
|
|
c482650e56 | ||
|
|
5da75bc798 | ||
|
|
07b80723b6 | ||
|
|
0c3a2b80f8 | ||
|
|
29e00c0d97 | ||
|
|
7e8819f4d2 | ||
|
|
c90c0f7848 | ||
|
|
cd11035a69 | ||
|
|
8b7c95e02f | ||
|
|
e3f047a35d | ||
|
|
9fab267da1 | ||
|
|
55e7e82e5c | ||
|
|
325458c957 | ||
|
|
9c21fe32c1 | ||
|
|
eaec9eff37 | ||
|
|
287f539b7d | ||
|
|
a220528c15 | ||
|
|
7c023e2d1d | ||
|
|
f2544e0707 | ||
|
|
4974d2cfa1 | ||
|
|
b1ca9cf5b5 | ||
|
|
5a27207844 | ||
|
|
5e088d92c9 | ||
|
|
ea0c8ddea6 | ||
|
|
b3c9ba4555 | ||
|
|
7f51039f9a | ||
|
|
7799ce285e | ||
|
|
fe62d0c407 | ||
|
|
c9a117cc4e | ||
|
|
13ab20ea49 | ||
|
|
f3c8535870 | ||
|
|
35ed9fc286 | ||
|
|
efdd0330c1 | ||
|
|
4c78fdf431 | ||
|
|
b09fdf07e4 | ||
|
|
5c524da3c2 | ||
|
|
99224f40d5 | ||
|
|
83354ab24b | ||
|
|
af05306046 | ||
|
|
b796ee7c36 | ||
|
|
db94b3d839 | ||
|
|
f214c7108f | ||
|
|
2abebfb244 | ||
|
|
fd821a5ead | ||
|
|
487f5ce339 | ||
|
|
1356e0f068 | ||
|
|
0f93cd002b | ||
|
|
6761442241 | ||
|
|
b441066105 | ||
|
|
d50486e337 | ||
|
|
c3dfabd5a2 | ||
|
|
4c187bcb9f | ||
|
|
d42ef1cdbc | ||
|
|
03193e0bd7 | ||
|
|
672d91e6c2 | ||
|
|
6d8ac6a23c | ||
|
|
69b3e2b5cb | ||
|
|
5e93decf6e | ||
|
|
79cdcb46de | ||
|
|
f889eb3d12 | ||
|
|
3306c030e1 | ||
|
|
239886a5cf | ||
|
|
f3cb4265ca | ||
|
|
28afebdca2 | ||
|
|
ab31f4b027 | ||
|
|
4128a78171 | ||
|
|
7f60db069f | ||
|
|
26fc980ffb | ||
|
|
d252dc82d6 | ||
|
|
c186d72b40 | ||
|
|
73160877b3 | ||
|
|
33f3bec301 | ||
|
|
8c30de16d6 | ||
|
|
fa95546988 | ||
|
|
ac16d7aef1 | ||
|
|
e2a7adaa79 | ||
|
|
75f0196c55 | ||
|
|
49336e0698 | ||
|
|
f0c697afd5 | ||
|
|
cff1ed5e08 | ||
|
|
73c845fbbe | ||
|
|
193f014a5b | ||
|
|
bd9b0d29ea | ||
|
|
4af10ce60c | ||
|
|
29708db467 | ||
|
|
deab3ba751 | ||
|
|
c1c06d6dc1 | ||
|
|
b7aa78c3c0 | ||
|
|
2568986fd5 | ||
|
|
f946ef6327 | ||
|
|
0d0deb7c40 | ||
|
|
e8908e32c9 | ||
|
|
9e9a08806d | ||
|
|
ee9fa8c86f | ||
|
|
e890579141 | ||
|
|
9aa39a6a12 | ||
|
|
6ee6da074e | ||
|
|
2bb274d424 | ||
|
|
19692c76df | ||
|
|
a6275ebcdb | ||
|
|
6a279e2775 | ||
|
|
9ce6eebe43 | ||
|
|
0b6378eb13 | ||
|
|
350c86155b | ||
|
|
ad9bda2d69 | ||
|
|
08fd255a56 | ||
|
|
f607978780 | ||
|
|
f96e7e5cba | ||
|
|
6c279453d9 | ||
|
|
56163f69f8 | ||
|
|
a9862a56b3 | ||
|
|
eba90f5440 | ||
|
|
44efc65c63 | ||
|
|
8a05f0d499 | ||
|
|
f1121cf8c2 | ||
|
|
1a8e54bb52 | ||
|
|
23efa9e146 | ||
|
|
bb4ceb481f | ||
|
|
c6bfe0b1d7 | ||
|
|
2e812db13c | ||
|
|
70383d0a25 | ||
|
|
d3ae2eda56 | ||
|
|
170bf8b1eb | ||
|
|
272022621d | ||
|
|
be3418a269 | ||
|
|
3e80268a44 | ||
|
|
3a809e4a1c | ||
|
|
dfc24bec01 | ||
|
|
e567d22f1c | ||
|
|
bfc3fbb405 | ||
|
|
8bfcd9939c | ||
|
|
316bcf7b5d | ||
|
|
453b97bec0 | ||
|
|
4364ec3a7b | ||
|
|
7de24f86a9 | ||
|
|
027c21aef7 | ||
|
|
64f0bdbfba | ||
|
|
cc1cb9edb0 | ||
|
|
35ef31757b | ||
|
|
87f26a82b6 | ||
|
|
1d141aa27b | ||
|
|
a84c1ecf33 | ||
|
|
2518d5c827 | ||
|
|
3f98d69690 | ||
|
|
9c8c819ec3 | ||
|
|
f038dcb404 | ||
|
|
75bbd55128 | ||
|
|
faa716d99c | ||
|
|
b1601b16ea | ||
|
|
379ed9dc16 | ||
|
|
7b6836b75e | ||
|
|
e4dd03c8f0 | ||
|
|
c99ce06370 | ||
|
|
0f67cae1ef | ||
|
|
0443f8a709 | ||
|
|
1442923a0a | ||
|
|
9447274fa5 | ||
|
|
7a59591109 | ||
|
|
d636ca45e8 | ||
|
|
832ef446dd | ||
|
|
74e75d2cfb | ||
|
|
95750be815 | ||
|
|
4ad6fab5e3 | ||
|
|
94dce4f796 | ||
|
|
ada1f29b34 | ||
|
|
4a634f08da | ||
|
|
e8b88cfa5e | ||
|
|
3066597acc | ||
|
|
d1f9174e7f | ||
|
|
7eade3b101 | ||
|
|
435682e95c | ||
|
|
d24ad83a5c | ||
|
|
980ea5796e | ||
|
|
8500d1c8a7 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,10 @@
|
||||
Cargo.lock
|
||||
target
|
||||
|
||||
# MacOS temp file
|
||||
.DS_Store
|
||||
|
||||
book-test
|
||||
book-example/book
|
||||
|
||||
.vscode
|
||||
90
.travis.yml
90
.travis.yml
@@ -1,99 +1,41 @@
|
||||
language: rust
|
||||
sudo: false
|
||||
|
||||
language: generic
|
||||
cache: cargo
|
||||
|
||||
env:
|
||||
global:
|
||||
- PROJECT_NAME=mdBook
|
||||
- secure: l3/qEC4krRerllLQzni8j5AjngFi6pluWvBWj//1mJLoIEYwxlQ9mYxEdd9BqccWWFn3K0bVYCVC/64+tP6sRfLkZCe2gPUtwe7ITwCDbapUxmkiRObVJCs5yMQZt6idyhHUDKAXKgNCrusfI2BM3tKGBfRK7Cnn/R/7p/U9+q7D1sgJtUKp6ypVzK6A3jLNp3dFLFI19a5KmbZMVsaa7tOhtdDJjjr7ebsc9z7HMW5/OItiWU3FSauVQQlUMaCiEgFuIG7H7OnBAYWB/gNEtLuwfLqU9UjtWk/njNNRnmJ7m3y5HbQhv5H5F5mJUOq9XFlPLwPwyTeVztSGdQm6k8Pp2pgKBUjY27afBl9BWU+msmN6k0oXfhvIebiBPe/x2udiKeFik1xqOOEU1q9dF0sZiuPxCSM1n7tgWklJ8epgaRQaMPPQw9pO/2H5/ynHCJqBlw6WcdiqWtwAyyr/GEx62u/cg5IVkqb7KLmYsWzjS8wYG4CYs1eIxCw2xPZxP0FGuUXvxTBUPipFze6Z7FqxVauXtVe2D7c1P4738HZP660rmR0GYtHtKLny1QxCCK9sxd9JmcezFCSz4YeQ1od9xc0OzGJ2ullKNGizmGfYmgL6X8faNylLIEdaiHAcY16xV3L0g3fXL1Qg360UHQyj7GIv+0nqQnf+H9xRTTU=
|
||||
rust:
|
||||
- stable
|
||||
- nightly
|
||||
|
||||
matrix:
|
||||
include:
|
||||
# Stable channel
|
||||
- os: osx
|
||||
env: TARGET=i686-apple-darwin CHANNEL=stable
|
||||
- os: linux
|
||||
env: TARGET=i686-unknown-linux-gnu CHANNEL=stable
|
||||
addons:
|
||||
apt:
|
||||
packages: &i686_unknown_linux_gnu
|
||||
- gcc-multilib
|
||||
- os: osx
|
||||
env: TARGET=x86_64-apple-darwin CHANNEL=stable
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=stable
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- nodejs
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=stable
|
||||
# Beta channel
|
||||
- os: osx
|
||||
env: TARGET=i686-apple-darwin CHANNEL=beta
|
||||
- os: linux
|
||||
env: TARGET=i686-unknown-linux-gnu CHANNEL=beta
|
||||
addons:
|
||||
apt:
|
||||
packages: *i686_unknown_linux_gnu
|
||||
- os: osx
|
||||
env: TARGET=x86_64-apple-darwin CHANNEL=beta
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=beta
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=beta
|
||||
# Nightly channel
|
||||
- os: osx
|
||||
env: TARGET=i686-apple-darwin CHANNEL=nightly
|
||||
- os: linux
|
||||
env: TARGET=i686-unknown-linux-gnu CHANNEL=nightly
|
||||
addons:
|
||||
apt:
|
||||
packages: *i686_unknown_linux_gnu
|
||||
- os: osx
|
||||
env: TARGET=x86_64-apple-darwin CHANNEL=nightly
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=nightly
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=nightly
|
||||
|
||||
# Musl builds fail due to a bug in Rust (https://github.com/azerupi/mdBook/issues/158)
|
||||
allow_failures:
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=stable
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=beta
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=nightly
|
||||
|
||||
install:
|
||||
- export PATH="$PATH:$HOME/.cargo/bin"
|
||||
- bash ci/install.sh
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
script:
|
||||
- bash ci/script.sh
|
||||
- cargo build --verbose
|
||||
- cargo test --verbose
|
||||
|
||||
after_success:
|
||||
# Deploy the docs if the commit is on master
|
||||
- test "$TRAVIS_PULL_REQUEST" == "false" &&
|
||||
test "$TRAVIS_BRANCH" == "master" &&
|
||||
test "$TARGET" == "x86_64-unknown-linux-gnu" &&
|
||||
test "$CHANNEL" = "stable" &&
|
||||
test "$TRAVIS_RUST_VERSION" == "stable" &&
|
||||
npm install stylus nib &&
|
||||
bash deploy.sh
|
||||
bash ci/deploy.sh
|
||||
|
||||
before_deploy:
|
||||
# Script to create packages from the build artefacts to upload to GitHub
|
||||
- bash ci/before_deploy.sh
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: Z1k7WqX7z+tT4+SzTh4tBBzf11VaADB4AWuEczHtylaEb/0hRs8gaiHCNSVHm/QTp0QPWQR2Vw7uKMhVuxG7I8X7h31j3A7ulYBh/iVk0DVIrtrn2Q4WOED9CpoXLuLtk2nxo9MBViFW7mw4nJe9H2Tn9o/9oEYBuwzekvW5mh4muqUuCVTr8eQVYbs3jbC9pQy5oYjOLeUnlL9Cey5VN/nAhzAtyFP+6lIMri0PKit4JtkFou/O1MEpFYlP3VGC2lFiWuByocPKBT/L45FecS9qoHq+i6+ZCPDH2eu46nuYsDbLKAkPdGvf1MdPBPwoj0vSnZbgaTisQ4hIoBngQQQPZlPaGtcdd6g6asxSfnbA9cQhClI5oZJmg+ksxQE+peE8pnbmZ10Ix0PpIkkfWdQeMdUUCQarOTkTK54Munw+X+kp1lH19j6+krQPLBYr95fPRd4b5tWsJD2+pb/UOYFEEJxMNoUHyLCrtdCO7imOwrSUcv51+Z8UudqfPpKQeszrJcntL4owip35r3sF5TsE9YfW5qssLC164IylvP32y1AcfL1jqg8b+zrqLZKanjvDOJ1dtHHuwKqxcwf7PhAf0YjAtVSH9OIYcDzmDa0EMLrq7EK0fs6NAeb5qt6CML7pZrRS3fmOxN53Fbmj81qm6TmjQjDe4dmZlELgNow=
|
||||
file: ${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}.tar.gz
|
||||
file: ${PROJECT_NAME}-${TRAVIS_TAG}-${TRAVIS_OS_NAME}.tar.gz
|
||||
# don't delete the artifacts from previous phases
|
||||
skip_cleanup: true
|
||||
# deploy when a new tag is pushed
|
||||
on:
|
||||
condition: $CHANNEL = stable
|
||||
condition: $TRAVIS_RUST_VERSION = stable
|
||||
tags: true
|
||||
|
||||
notifications:
|
||||
|
||||
84
CONTRIBUTING.md
Normal file
84
CONTRIBUTING.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Contributing
|
||||
|
||||
Welcome stranger!
|
||||
|
||||
If you have come here to learn how to contribute to mdBook, we have some tips for you!
|
||||
|
||||
First of all, don't hesitate to ask questions!
|
||||
Use the [issue tracker](https://github.com/rust-lang-nursery/mdBook/issues), no question is too simple.
|
||||
If we don't respond in a couple of days, ping us @Michael-F-Bryan, @budziq, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
|
||||
|
||||
### Issues to work on
|
||||
|
||||
Any issue is up for the grabbing, but if you are starting out, you might be interested in the
|
||||
[E-Easy issues](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
||||
Those are issues that are considered more straightforward for beginners to Rust or the codebase itself.
|
||||
These issues can be a good launching pad for more involved issues. Easy tasks for a first time contribution
|
||||
include documentation improvements, new tests, examples, updating dependencies, etc.
|
||||
|
||||
If you come from a web development background, you might be interested in issues related to web technologies tagged
|
||||
[A-JavaScript](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||
[A-Style](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
|
||||
[A-HTML](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
||||
[A-Mobile](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
|
||||
|
||||
When you decide you want to work on a specific issue, ping us on that issue so that we can assign it to you.
|
||||
Again, do not hesitate to ask questions. We will gladly mentor anyone that want to tackle an issue.
|
||||
|
||||
Issues on the issue tracker are categorized with the following labels:
|
||||
|
||||
- **A**-prefixed labels state which area of the project an issue relates to.
|
||||
- **E**-prefixed labels show an estimate of the experience necessary to fix the issue.
|
||||
- **M**-prefixed labels are meta-issues used for questions, discussions, or tracking issues
|
||||
- **S**-prefixed labels show the status of the issue
|
||||
- **T**-prefixed labels show the type of issue
|
||||
|
||||
### Building mdBook
|
||||
|
||||
mdBook builds on stable Rust, if you want to build mdBook from source, here are the steps to follow:
|
||||
|
||||
1. Navigate to the directory of your choice
|
||||
0. Clone this repository with git.
|
||||
|
||||
```
|
||||
git clone https://github.com/rust-lang-nursery/mdBook.git
|
||||
```
|
||||
0. Navigate into the newly created `mdBook` directory
|
||||
0. Run `cargo build`
|
||||
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
|
||||
|
||||
|
||||
### Making changes to the style
|
||||
|
||||
mdBook doesn't use CSS directly but uses [Stylus](http://stylus-lang.com/), a CSS-preprocessor which compiles to CSS.
|
||||
|
||||
When you want to change the style, it is important to not change the CSS directly because any manual modification to
|
||||
the CSS files will be overwritten when compiling the stylus files. Instead, you should make your changes directly in the
|
||||
[stylus files](https://github.com/rust-lang-nursery/mdBook/tree/master/src/theme/stylus) and regenerate the CSS.
|
||||
|
||||
For this to work, you first need [Node and NPM](https://nodejs.org/en/) installed on your machine.
|
||||
Then run the following command to install both [stylus](http://stylus-lang.com/) and [nib](https://tj.github.io/nib/), you might need `sudo` to install successfully.
|
||||
|
||||
```
|
||||
npm install -g stylus nib
|
||||
```
|
||||
|
||||
When that finished, you can simply regenerate the CSS files by building mdBook with the following command:
|
||||
|
||||
```
|
||||
cargo build --features=regenerate-css
|
||||
```
|
||||
|
||||
This should automatically call the appropriate stylus command to recompile the files to CSS and include them in the project.
|
||||
|
||||
### Making a pull-request
|
||||
|
||||
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
|
||||
One of the core maintainers will then approve the changes or request some changes before it gets merged.
|
||||
|
||||
If you want to make your pull-request even better, you might want to run [Clippy](https://github.com/Manishearth/rust-clippy)
|
||||
and [rustfmt](https://github.com/rust-lang-nursery/rustfmt) on the code first.
|
||||
This is not a requirement though and will never block a pull-request from being merged.
|
||||
|
||||
That's it, happy contributions! :tada: :tada: :tada:
|
||||
36
Cargo.toml
36
Cargo.toml
@@ -1,44 +1,52 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.0.21"
|
||||
version = "0.0.28"
|
||||
authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
|
||||
description = "create books from markdown files (like Gitbook)"
|
||||
documentation = "http://azerupi.github.io/mdBook/index.html"
|
||||
repository = "https://github.com/azerupi/mdBook"
|
||||
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
|
||||
repository = "https://github.com/rust-lang-nursery/mdBook"
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
build = "build.rs"
|
||||
exclude = [
|
||||
"book-example/*",
|
||||
"src/theme/stylus",
|
||||
"src/theme/stylus/**",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
clap = "2.19.2"
|
||||
handlebars = { version = "0.25.0", features = ["serde_type"] }
|
||||
serde = "0.9"
|
||||
serde_json = "0.9"
|
||||
pulldown-cmark = "0.0.8"
|
||||
clap = "2.24"
|
||||
handlebars = "0.29"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
error-chain = "0.11.0"
|
||||
serde_json = "1.0"
|
||||
pulldown-cmark = "0.1"
|
||||
lazy_static = "0.2"
|
||||
log = "0.3"
|
||||
env_logger = "0.4.0"
|
||||
toml = { version = "0.3", features = ["serde"] }
|
||||
toml = "0.4"
|
||||
open = "1.1"
|
||||
regex = "0.2.1"
|
||||
tempdir = "0.3.4"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "4.0", optional = true }
|
||||
time = { version = "0.1.34", optional = true }
|
||||
crossbeam = { version = "0.2.8", optional = true }
|
||||
crossbeam = { version = "0.3", optional = true }
|
||||
|
||||
# Serve feature
|
||||
iron = { version = "0.5", optional = true }
|
||||
staticfile = { version = "0.4", optional = true }
|
||||
ws = { version = "0.6", optional = true}
|
||||
ws = { version = "0.7", optional = true}
|
||||
|
||||
[build-dependencies]
|
||||
error-chain = "0.11"
|
||||
|
||||
# Tests
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3.4"
|
||||
select = "0.4"
|
||||
pretty_assertions = "0.4"
|
||||
walkdir = "1.0"
|
||||
|
||||
[features]
|
||||
default = ["output", "watch", "serve"]
|
||||
|
||||
37
README.md
37
README.md
@@ -4,7 +4,7 @@
|
||||
<tr>
|
||||
<td><strong>Linux / OS X</strong></td>
|
||||
<td>
|
||||
<a href="https://travis-ci.org/azerupi/mdBook"><img src="https://travis-ci.org/azerupi/mdBook.svg?branch=master"></a>
|
||||
<a href="https://travis-ci.org/rust-lang-nursery/mdBook"><img src="https://travis-ci.org/rust-lang-nursery/mdBook.svg?branch=master"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -16,27 +16,24 @@
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a href="https://crates.io/crates/mdbook"><img src="https://img.shields.io/crates/v/mdbook.svg"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/crates/l/mdbook.svg"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/rust-lang-nursery/mdBook.svg"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
mdBook is a utility to create modern online books from Markdown files.
|
||||
|
||||
**This project is still evolving.**
|
||||
See [#90](https://github.com/azerupi/mdBook/issues/90)
|
||||
|
||||
|
||||
## What does it look like?
|
||||
|
||||
The [**Documentation**](http://azerupi.github.io/mdBook/) for mdBook has been written in Markdown and is using mdBook to generate the online book-like website you can read. The documentation uses the latest version on GitHub and showcases the available features.
|
||||
The [**User Guide**](https://rust-lang-nursery.github.io/mdBook/) for mdBook has been written in Markdown and is using mdBook to generate the online book-like website you can read. The documentation uses the latest version on GitHub and showcases the available features.
|
||||
|
||||
## Installation
|
||||
|
||||
There are multiple ways to install mdBook.
|
||||
|
||||
1. **Binaries**
|
||||
Binaries are available for download [here](https://github.com/azerupi/mdBook/releases). Make sure to put the path to the binary into your `PATH`.
|
||||
Binaries are available for download [here](https://github.com/rust-lang-nursery/mdBook/releases). Make sure to put the path to the binary into your `PATH`.
|
||||
|
||||
2. **From Crates.io**
|
||||
This requires [Rust and Cargo](https://www.rust-lang.org/) to be installed. Once you have installed Rust, type the following in the terminal:
|
||||
@@ -46,11 +43,19 @@ There are multiple ways to install mdBook.
|
||||
|
||||
This will download and compile mdBook for you, the only thing left to do is to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
**Note for automatic deployment**
|
||||
If you are using a script to do automatic deployments using Travis or another CI server, we recommend that you specify a semver version range for mdBook when you install it through your script!
|
||||
This will constrain the server to install the latests **non-breaking** version of mdBook and will prevent your books from failing to build because we released a new version. For example:
|
||||
|
||||
```
|
||||
cargo install mdbook --vers "^0.1.0"
|
||||
```
|
||||
|
||||
3. **From Git**
|
||||
The version published to crates.io will ever so slightly be behind the version hosted here on GitHub. If you need the latest version you can build the git version of mdBook yourself. Cargo makes this ***super easy***!
|
||||
|
||||
```
|
||||
cargo install --git https://github.com/azerupi/mdBook.git
|
||||
cargo install --git https://github.com/rust-lang-nursery/mdBook.git
|
||||
```
|
||||
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
@@ -58,7 +63,7 @@ There are multiple ways to install mdBook.
|
||||
If you want to contribute to mdBook you will have to clone the repository on your local machine:
|
||||
|
||||
```
|
||||
git clone https://github.com/azerupi/mdBook.git
|
||||
git clone https://github.com/rust-lang-nursery/mdBook.git
|
||||
```
|
||||
`cd` into `mdBook/` and run
|
||||
|
||||
@@ -74,7 +79,7 @@ There are multiple ways to install mdBook.
|
||||
|
||||
mdBook will primarily be used as a command line tool, even though it exposes all its functionality as a Rust crate for integration in other projects.
|
||||
|
||||
Here are the main commands you will want to run. For a more exhaustive explanation, check out the [documentation](http://azerupi.github.io/mdBook/).
|
||||
Here are the main commands you will want to run. For a more exhaustive explanation, check out the [User Guide](http://rust-lang-nursery.github.io/mdBook/).
|
||||
|
||||
- `mdbook init`
|
||||
|
||||
@@ -90,7 +95,7 @@ Here are the main commands you will want to run. For a more exhaustive explanati
|
||||
|
||||
`book` and `src` are both directories. `src` contains the markdown files that will be used to render the output to the `book` directory.
|
||||
|
||||
Please, take a look at the [**Documentation**](http://azerupi.github.io/mdBook/cli/init.html) for more information and some neat tricks.
|
||||
Please, take a look at the [**Documentation**](http://rust-lang-nursery.github.io/mdBook/cli/init.html) for more information and some neat tricks.
|
||||
|
||||
- `mdbook build`
|
||||
|
||||
@@ -108,19 +113,17 @@ Here are the main commands you will want to run. For a more exhaustive explanati
|
||||
|
||||
Aside from the command line interface, this crate can also be used as a library. This means that you could integrate it in an existing project, like a web-app for example. Since the command line interface is just a wrapper around the library functionality, when you use this crate as a library you have full access to all the functionality of the command line interface with an easy to use API and more!
|
||||
|
||||
See the [Documentation](http://azerupi.github.io/mdBook/lib/lib.html) and the [API docs](http://azerupi.github.io/mdBook/mdbook/index.html) for more information.
|
||||
See the [User Guide](https://rust-lang-nursery.github.io/mdBook/) and the [API docs](https://docs.rs/mdbook/*/mdbook/) for more information.
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are highly appreciated and encouraged! Don't hesitate to participate to discussions in the issues, propose new features and ask for help.
|
||||
|
||||
If you are not very confident with Rust, **I will be glad to mentor as best as I can if you decide to tackle an issue or new feature.**
|
||||
If you are just starting out with Rust, there are a series of issus that are tagged [E-Easy](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy) and **we will gladly mentor you** so that you can successfully go through the process of fixing a bug or adding a new feature! Let us know if you need any help.
|
||||
|
||||
People who are not familiar with the code can look at [issues that are tagged **easy**](https://github.com/azerupi/mdBook/labels/Easy). A lot of issues are also related to web development, so people that are not comfortable with Rust can also participate! :wink:
|
||||
|
||||
You can pick any issue you want to work on. Usually it's a good idea to ask if someone is already working on it and if not to claim the issue.
|
||||
For more info about contributing, check out our [contribution guide](CONTRIBUTING.md) who helps you go through the build and contribution process!
|
||||
|
||||
|
||||
## License
|
||||
|
||||
All the code is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE](LICENSE) file.
|
||||
All the code in this repository is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE](LICENSE) file.
|
||||
|
||||
23
appveyor.yml
23
appveyor.yml
@@ -1,6 +1,7 @@
|
||||
environment:
|
||||
global:
|
||||
PROJECT_NAME: mdBook
|
||||
nodejs_version: "6"
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
@@ -20,20 +21,28 @@ environment:
|
||||
|
||||
# Install Rust and Cargo
|
||||
install:
|
||||
- ps: Start-FileDownload "https://static.rust-lang.org/dist/channel-rust-stable"
|
||||
- ps: $env:RUST_VERSION = Get-Content channel-rust-stable | select -first 1 | %{$_.split('-')[1]}
|
||||
- if NOT "%RUST_CHANNEL%" == "stable" set RUST_VERSION=%RUST_CHANNEL%
|
||||
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-${env:RUST_VERSION}-${env:TARGET}.exe"
|
||||
- rust-%RUST_VERSION%-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust"
|
||||
- SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin
|
||||
- rustc -V
|
||||
- ps: >-
|
||||
If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\msys64\mingw64\bin'
|
||||
} ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\msys64\mingw32\bin'
|
||||
}
|
||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
|
||||
- rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_CHANNEL%
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -Vv
|
||||
- cargo -V
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- node --version
|
||||
- npm --version
|
||||
- npm install -g stylus nib
|
||||
|
||||
build: false
|
||||
|
||||
# Equivalent to Travis' `script` phase
|
||||
test_script:
|
||||
- cargo build --verbose
|
||||
- cargo build --verbose --features=regenerate-css
|
||||
- cargo test --verbose
|
||||
|
||||
before_deploy:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
author = "Mathieu David"
|
||||
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
**mdBook** is a command line tool and Rust crate to create books using Markdown files. It's very similar to Gitbook but written in [Rust](http://www.rust-lang.org).
|
||||
|
||||
What you are reading serves as an example of the output of mdBook and at the same time as high-level docs.
|
||||
What you are reading serves as an example of the output of mdBook and at the same time as a high-level documentation.
|
||||
|
||||
mdBook is free and open source, you can find the source code on [Github](https://github.com/azerupi/mdBook). Issues and feature requests can be posted on the [Github Issue tracker](https://github.com/azerupi/mdBook/issues).
|
||||
mdBook is free and open source, you can find the source code on [Github](https://github.com/rust-lang-nursery/mdBook). Issues and feature requests can be posted on the [Github Issue tracker](https://github.com/rust-lang-nursery/mdBook/issues).
|
||||
|
||||
## API docs
|
||||
|
||||
Alongside this book you can also read the [API docs](mdbook/index.html) generated by Rustdoc if you would like
|
||||
to use mdBook as a crate or write a new renderer and need a more low-level overview.
|
||||
Alongside this book you can also read the [API docs](https://docs.rs/mdbook/*/mdbook/) generated by Rustdoc if you would like to use mdBook as a crate or write a new renderer and need a more low-level overview.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Summary
|
||||
|
||||
[Introduction](misc/introduction.md)
|
||||
|
||||
- [mdBook](README.md)
|
||||
- [Command Line Tool](cli/cli-tool.md)
|
||||
- [init](cli/init.md)
|
||||
|
||||
@@ -24,10 +24,10 @@ Run `mdbook help` in your terminal to verify if it works. Congratulations, you h
|
||||
|
||||
### Install Git version
|
||||
|
||||
The **[git version](https://github.com/azerupi/mdBook)** contains all the latest bug-fixes and features, that will be released in the next version on **Crates.io**, if you can't wait until the next release. You can build the git version yourself. Open your terminal and navigate to the directory of you choice. We need to clone the git repository and then build it with Cargo.
|
||||
The **[git version](https://github.com/rust-lang-nursery/mdBook)** contains all the latest bug-fixes and features, that will be released in the next version on **Crates.io**, if you can't wait until the next release. You can build the git version yourself. Open your terminal and navigate to the directory of you choice. We need to clone the git repository and then build it with Cargo.
|
||||
|
||||
```bash
|
||||
git clone --depth=1 https://github.com/azerupi/mdBook.git
|
||||
git clone --depth=1 https://github.com/rust-lang-nursery/mdBook.git
|
||||
cd mdBook
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
@@ -37,4 +37,4 @@ The `--dest-dir` (`-d`) option allows you to change the output directory for you
|
||||
|
||||
-----
|
||||
|
||||
***note:*** *the `serve` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/azerupi/mdBook/issues)*
|
||||
***note:*** *the `serve` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/rust-lang-nursery/mdBook/issues)*
|
||||
|
||||
@@ -23,4 +23,4 @@ The `--dest-dir` (`-d`) option allows you to change the output directory for you
|
||||
|
||||
-----
|
||||
|
||||
***note:*** *the `watch` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/azerupi/mdBook/issues)*
|
||||
***note:*** *the `watch` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/rust-lang-nursery/mdBook/issues)*
|
||||
|
||||
@@ -2,27 +2,154 @@
|
||||
|
||||
You can configure the parameters for your book in the ***book.toml*** file.
|
||||
|
||||
We encourage using the TOML format, but JSON is also recognized and parsed.
|
||||
|
||||
Here is an example of what a ***book.toml*** file might look like:
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
author = "Name"
|
||||
author = "John Doe"
|
||||
description = "The example book covers examples."
|
||||
dest = "output/my-book"
|
||||
|
||||
[build]
|
||||
build-dir = "my-example-book"
|
||||
create-missing = false
|
||||
|
||||
[output.html]
|
||||
additional-css = ["custom.css"]
|
||||
```
|
||||
|
||||
#### Supported variables
|
||||
## Supported configuration options
|
||||
|
||||
If relative paths are given, they will be relative to the book's root, i.e. the
|
||||
parent directory of the source directory.
|
||||
It is important to note that **any** relative path specified in the in the configuration will
|
||||
always be taken relative from the root of the book where the configuration file is located.
|
||||
|
||||
- **title:** The title of the book.
|
||||
- **author:** The author of the book.
|
||||
- **description:** The description, which is added as meta in the html head of each page.
|
||||
- **src:** The path to the book's source files (chapters in Markdown, SUMMARY.md, etc.). Defaults to `root/src`.
|
||||
- **dest:** The path to the directory where you want your book to be rendered. Defaults to `root/book`.
|
||||
- **theme_path:** The path to a custom theme directory. Defaults to `root/theme`.
|
||||
|
||||
***note:*** *the supported configurable parameters are scarce at the moment, but more will be added in the future*
|
||||
### General metadata
|
||||
|
||||
This is general information about your book.
|
||||
|
||||
- **title:** The title of the book
|
||||
- **authors:** The author(s) of the book
|
||||
- **description:** A description for the book, which is added as meta
|
||||
information in the html `<head>` of each page
|
||||
- **src:** By default, the source directory is found in the directory named
|
||||
`src` directly under the root folder. But this is configurable with the `src`
|
||||
key in the configuration file.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
|
||||
```
|
||||
|
||||
### Build options
|
||||
|
||||
This controls the build process of your book.
|
||||
|
||||
- **build-dir:** The directory to put the rendered book in. By default this is
|
||||
`book/` in the book's root directory.
|
||||
- **create-missing:** By default, any missing files specified in `SUMMARY.md`
|
||||
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.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
[build]
|
||||
build-dir = "build"
|
||||
create-missing = false
|
||||
```
|
||||
|
||||
### HTML renderer options
|
||||
The HTML renderer has a couple of options as well. All the options for the
|
||||
renderer need to be specified under the TOML table `[output.html]`.
|
||||
|
||||
The following configuration options are available:
|
||||
|
||||
pub playpen: Playpen,
|
||||
|
||||
- **theme:** mdBook comes with a default theme and all the resource files
|
||||
needed for it. But if this option is set, mdBook will selectively overwrite
|
||||
the theme files with the ones found in the specified folder.
|
||||
- **curly-quotes:** Convert straight quotes to curly quotes, except for
|
||||
those that occur in code blocks and code spans. Defaults to `false`.
|
||||
- **google-analytics:** If you use Google Analytics, this option lets you
|
||||
enable it by simply specifying your ID in the configuration file.
|
||||
- **additional-css:** If you need to slightly change the appearance of your
|
||||
book without overwriting the whole style, you can specify a set of
|
||||
stylesheets that will be loaded after the default ones where you can
|
||||
surgically change the style.
|
||||
- **additional-js:** If you need to add some behaviour to your book without
|
||||
removing the current behaviour, you can specify a set of javascript files
|
||||
that will be loaded alongside the default one.
|
||||
- **playpen:** A subtable for configuring various playpen settings.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
[output.html]
|
||||
theme = "my-theme"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
|
||||
[output.html.playpen]
|
||||
editor = "./path/to/editor"
|
||||
editable = false
|
||||
```
|
||||
|
||||
|
||||
## For Developers
|
||||
|
||||
If you are developing a plugin or alternate backend then whenever your code is
|
||||
called you will almost certainly be passed a reference to the book's `Config`.
|
||||
This can be treated roughly as a nested hashmap which lets you call methods like
|
||||
`get()` and `get_mut()` to get access to the config's contents.
|
||||
|
||||
By convention, plugin developers will have their settings as a subtable inside
|
||||
`plugins` (e.g. a link checker would put its settings in `plugins.link_check`)
|
||||
and backends should put their configuration under `output`, like the HTML
|
||||
renderer does in the previous examples.
|
||||
|
||||
As an example, some hypothetical `random` renderer would typically want to load
|
||||
its settings from the `Config` at the very start of its rendering process. The
|
||||
author can take advantage of serde to deserialize the generic `toml::Value`
|
||||
object retrieved from `Config` into a struct specific to its use case.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
struct RandomOutput {
|
||||
foo: u32,
|
||||
bar: String,
|
||||
baz: Vec<bool>,
|
||||
}
|
||||
|
||||
let src = r#"
|
||||
[output.random]
|
||||
foo = 5
|
||||
bar = "Hello World"
|
||||
baz = [true, true, false]
|
||||
"#;
|
||||
|
||||
let book_config = Config::from_str(src)?; // usually passed in by mdbook
|
||||
let random: Value = book_config.get("output.random").unwrap_or_default();
|
||||
let got: RandomOutput = random.try_into()?;
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
if let Some(baz) = book_config.get_deserialized::<Vec<bool>>("output.random.baz") {
|
||||
println!("{:?}", baz); // prints [true, true, false]
|
||||
|
||||
// do something interesting with baz
|
||||
}
|
||||
|
||||
// start the rendering process
|
||||
```
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
# MathJax Support
|
||||
|
||||
mdBook supports math equations through [MathJax](https://www.mathjax.org/).
|
||||
mdBook has optional support for math equations through [MathJax](https://www.mathjax.org/).
|
||||
|
||||
**However the normal method for indication math equations with `$$` does not work (yet?).**
|
||||
To enable MathJax, you need to add the `mathjax-support` key to your `book.toml` under the `output.html` section.
|
||||
|
||||
To indicate an inline equation \\( \int x = \frac{x^2}{2} \\) use
|
||||
```
|
||||
\\( \int x = \frac{x^2}{2} \\)
|
||||
```toml
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
```
|
||||
|
||||
To indicate a block equation
|
||||
>**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.
|
||||
|
||||
### 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:
|
||||
```
|
||||
\\( \int x dx = \frac{x^2}{2} + C \\)
|
||||
```
|
||||
|
||||
### Block equations
|
||||
Block equations are delimited by `\\[` and `\\]`. To render the following equation
|
||||
|
||||
\\[ \mu = \frac{1}{N} \sum_{i=0} x_i \\]
|
||||
|
||||
|
||||
use
|
||||
you would write:
|
||||
|
||||
```bash
|
||||
\\[ \mu = \frac{1}{N} \sum_{i=0} x_i \\]
|
||||
|
||||
@@ -87,5 +87,5 @@ In addition to the properties you can access, there are some handlebars helpers
|
||||
|
||||
------
|
||||
|
||||
*If you would like me to expose other properties or helpers, please [create a new issue](https://github.com/azerupi/mdBook/issues)
|
||||
*If you would like me to expose other properties or helpers, please [create a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
|
||||
and I will consider it.*
|
||||
|
||||
@@ -53,7 +53,7 @@ Will render as
|
||||
## Improve default theme
|
||||
|
||||
If you think the default theme doesn't look quite right for a specific language, or could be improved.
|
||||
Feel free to [submit a new issue](https://github.com/azerupi/mdBook/issues) explaining what you have in mind and I will take a look at it.
|
||||
Feel free to [submit a new issue](https://github.com/rust-lang-nursery/mdBook/issues) explaining what you have in mind and I will take a look at it.
|
||||
|
||||
You could also create a pull-request with the proposed improvements.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ The default renderer uses a [handlebars](http://handlebarsjs.com/) template to r
|
||||
included in the mdBook binary.
|
||||
|
||||
The theme is totally customizable, you can selectively replace every file from the theme by your own by adding a
|
||||
`theme` directory in your source folder. Create a new file with the name of the file you want to override
|
||||
`theme` directory next to `src` folder in your project root. Create a new file with the name of the file you want to override
|
||||
and now that file will be used instead of the default file.
|
||||
|
||||
Here are the files you can override:
|
||||
|
||||
@@ -9,14 +9,16 @@ extern crate mdbook;
|
||||
use mdbook::MDBook;
|
||||
use std::path::Path;
|
||||
|
||||
# #[allow(unused_variables)]
|
||||
fn main() {
|
||||
let mut book = MDBook::new(Path::new("my-book")) // Path to root
|
||||
.set_src(Path::new("src")) // Path from root to source directory
|
||||
.set_dest(Path::new("book")) // Path from root to output directory
|
||||
.read_config(); // Parse book.toml or book.json file for configuration
|
||||
let mut book = MDBook::new("my-book") // Path to root
|
||||
.with_source("src") // Path from root to source directory
|
||||
.with_destination("book") // Path from root to output directory
|
||||
.read_config() // Parse book.toml or book.json configuration file
|
||||
.expect("I don't handle configuration file error, but you should!");
|
||||
|
||||
book.build().unwrap(); // Render the book
|
||||
book.build().unwrap(); // Render the book
|
||||
}
|
||||
```
|
||||
|
||||
Check here for the [API docs](../mdbook/index.html) generated by rustdoc.
|
||||
Check here for the [API docs](mdbook/index.html) generated by rustdoc.
|
||||
|
||||
@@ -11,3 +11,5 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add
|
||||
- Wayne Nilsen ([waynenilsen](https://github.com/waynenilsen))
|
||||
- [funnkill](https://github.com/funkill)
|
||||
- Fu Gangqiang ([FuGangqiang](https://github.com/FuGangqiang))
|
||||
- [Michael-F-Bryan](https://github.com/Michael-F-Bryan)
|
||||
- [Chris Spiegel](https://github.com/cspiegel)
|
||||
|
||||
97
build.rs
97
build.rs
@@ -1,29 +1,98 @@
|
||||
// build.rs
|
||||
|
||||
use std::process::Command;
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
|
||||
fn main() {
|
||||
#[cfg(windows)]
|
||||
mod execs {
|
||||
use std::process::Command;
|
||||
|
||||
pub fn cmd(program: &str) -> Command {
|
||||
let mut cmd = Command::new("cmd");
|
||||
cmd.args(&["/c", program]);
|
||||
cmd
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
mod execs {
|
||||
use std::process::Command;
|
||||
|
||||
pub fn cmd(program: &str) -> Command {
|
||||
Command::new(program)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
error_chain!{
|
||||
foreign_links {
|
||||
Io(std::io::Error);
|
||||
}
|
||||
}
|
||||
|
||||
fn program_exists(program: &str) -> Result<()> {
|
||||
execs::cmd(program).arg("-v")
|
||||
.output()
|
||||
.chain_err(|| format!("Please install '{}'!", program))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn npm_package_exists(package: &str) -> Result<()> {
|
||||
let status = execs::cmd("npm").args(&["list", "-g"])
|
||||
.arg(package)
|
||||
.output();
|
||||
|
||||
match status {
|
||||
Ok(ref out) if out.status.success() => Ok(()),
|
||||
_ => {
|
||||
bail!("Missing npm package '{0}' install with: 'npm -g install {0}'",
|
||||
package)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Resource<'a> {
|
||||
Program(&'a str),
|
||||
Package(&'a str),
|
||||
}
|
||||
use Resource::{Package, Program};
|
||||
|
||||
impl<'a> Resource<'a> {
|
||||
pub fn exists(&self) -> Result<()> {
|
||||
match *self {
|
||||
Program(name) => program_exists(name),
|
||||
Package(name) => npm_package_exists(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") {
|
||||
// Check dependencies
|
||||
Program("npm").exists()?;
|
||||
Program("node").exists().or(Program("nodejs").exists())?;
|
||||
Package("nib").exists()?;
|
||||
Package("stylus").exists()?;
|
||||
|
||||
// Compile stylus stylesheet to css
|
||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
|
||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR")
|
||||
.chain_err(|| "Please run the script with: 'cargo build'!")?;
|
||||
let theme_dir = Path::new(&manifest_dir).join("src/theme/");
|
||||
let stylus_dir = theme_dir.join("stylus/book.styl");
|
||||
|
||||
if !Command::new("stylus")
|
||||
.arg(format!("{}", stylus_dir.to_str().unwrap()))
|
||||
.arg("--out")
|
||||
.arg(format!("{}", theme_dir.to_str().unwrap()))
|
||||
.arg("--use")
|
||||
.arg("nib")
|
||||
.status().unwrap()
|
||||
.success() {
|
||||
panic!("Stylus encoutered an error");
|
||||
if !execs::cmd("stylus").arg(stylus_dir)
|
||||
.arg("--out")
|
||||
.arg(theme_dir)
|
||||
.arg("--use")
|
||||
.arg("nib")
|
||||
.status()?
|
||||
.success()
|
||||
{
|
||||
bail!("Stylus encountered an error");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
quick_main!(run);
|
||||
|
||||
@@ -18,7 +18,7 @@ mk_tarball() {
|
||||
|
||||
pushd $td
|
||||
|
||||
tar czf $out_dir/${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}.tar.gz *
|
||||
tar czf $out_dir/${PROJECT_NAME}-${TRAVIS_TAG}-${TRAVIS_OS_NAME}.tar.gz *
|
||||
|
||||
popd $td
|
||||
rm -r $td
|
||||
|
||||
@@ -28,7 +28,7 @@ git init
|
||||
git config user.name "Mathieu David"
|
||||
git config user.email "mathieudavid@mathieudavid.org"
|
||||
|
||||
git remote add upstream "https://$GH_TOKEN@github.com/azerupi/mdBook.git"
|
||||
git remote add upstream "https://$GH_TOKEN@github.com/rust-lang-nursery/mdBook.git"
|
||||
git fetch upstream
|
||||
git reset upstream/gh-pages
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# `install` phase: install stuff needed for the `script` phase
|
||||
|
||||
set -ex
|
||||
|
||||
case "$TRAVIS_OS_NAME" in
|
||||
linux)
|
||||
host=x86_64-unknown-linux-gnu
|
||||
;;
|
||||
osx)
|
||||
host=x86_64-apple-darwin
|
||||
;;
|
||||
esac
|
||||
|
||||
mktempd() {
|
||||
echo $(mktemp -d 2>/dev/null || mktemp -d -t tmp)
|
||||
}
|
||||
|
||||
install_rustup() {
|
||||
local td=$(mktempd)
|
||||
|
||||
pushd $td
|
||||
curl -O https://static.rust-lang.org/rustup/dist/$host/rustup-setup
|
||||
chmod +x rustup-setup
|
||||
./rustup-setup -y
|
||||
popd
|
||||
|
||||
rm -r $td
|
||||
|
||||
rustup default $CHANNEL
|
||||
rustc -V
|
||||
cargo -V
|
||||
}
|
||||
|
||||
install_standard_crates() {
|
||||
if [ "$host" != "$TARGET" ]; then
|
||||
if [ ! "$CHANNEL" = "stable" ]; then
|
||||
rustup target add $TARGET
|
||||
else
|
||||
local version=$(rustc -V | cut -d' ' -f2)
|
||||
local tarball=rust-std-${version}-${TARGET}
|
||||
|
||||
local td=$(mktempd)
|
||||
curl -s https://static.rust-lang.org/dist/${tarball}.tar.gz | \
|
||||
tar --strip-components 1 -C $td -xz
|
||||
|
||||
$td/install.sh --prefix=$(rustc --print sysroot)
|
||||
|
||||
rm -r $td
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
install_rustup
|
||||
install_standard_crates
|
||||
}
|
||||
|
||||
main
|
||||
45
ci/script.sh
45
ci/script.sh
@@ -1,45 +0,0 @@
|
||||
# `script` phase: you usually build, test and generate docs in this phase
|
||||
|
||||
set -ex
|
||||
|
||||
# NOTE Workaround for rust-lang/rust#31907 - disable doc tests when cross compiling
|
||||
# This has been fixed in the nightly channel but it would take a while to reach the other channels
|
||||
disable_cross_doctests() {
|
||||
local host
|
||||
case "$TRAVIS_OS_NAME" in
|
||||
linux)
|
||||
host=x86_64-unknown-linux-gnu
|
||||
;;
|
||||
osx)
|
||||
host=x86_64-apple-darwin
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$host" != "$TARGET" ] && [ "$CHANNEL" != "nightly" ]; then
|
||||
if [ "$TRAVIS_OS_NAME" = "osx" ]; then
|
||||
brew install gnu-sed --default-names
|
||||
fi
|
||||
|
||||
find src -name '*.rs' -type f | xargs sed -i -e 's:\(//.\s*```\):\1 ignore,:g'
|
||||
fi
|
||||
}
|
||||
|
||||
run_test_suite() {
|
||||
# Extra test without default features to avoid bitrot. We only test on a single target (but with
|
||||
# all the channels) to avoid significantly increasing the build times
|
||||
if [ $TARGET = x86_64-unknown-linux-gnu ]; then
|
||||
cargo build --target $TARGET --no-default-features --verbose
|
||||
cargo test --target $TARGET --no-default-features --verbose
|
||||
cargo clean
|
||||
fi
|
||||
|
||||
cargo build --target $TARGET --verbose
|
||||
cargo test --target $TARGET --verbose
|
||||
}
|
||||
|
||||
main() {
|
||||
disable_cross_doctests
|
||||
run_test_suite
|
||||
}
|
||||
|
||||
main
|
||||
20
rustfmt.toml
20
rustfmt.toml
@@ -1,15 +1,7 @@
|
||||
write_mode = "Overwrite"
|
||||
array_layout = "Visual"
|
||||
chain_indent = "Visual"
|
||||
fn_args_layout = "Visual"
|
||||
fn_call_style = "Visual"
|
||||
format_strings = true
|
||||
generics_indent = "Visual"
|
||||
|
||||
max_width = 120
|
||||
ideal_width = 120
|
||||
fn_call_width = 100
|
||||
|
||||
fn_args_density = "Compressed"
|
||||
|
||||
enum_trailing_comma = true
|
||||
match_block_trailing_comma = true
|
||||
struct_trailing_comma = "Always"
|
||||
wrap_comments = true
|
||||
|
||||
report_todo = "Always"
|
||||
report_fixme = "Always"
|
||||
|
||||
46
src/bin/build.rs
Normal file
46
src/bin/build.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use std::path::PathBuf;
|
||||
use clap::{ArgMatches, SubCommand, App};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use {get_book_dir, open};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("build")
|
||||
.about("Build the book from the markdown files")
|
||||
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'The output directory for your \
|
||||
book{n}(Defaults to ./book when omitted)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"--no-create 'Will not create non-existent files linked from SUMMARY.md (deprecated: use book.toml instead)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'A directory for your book{n}(Defaults to Current Directory \
|
||||
when omitted)'",
|
||||
)
|
||||
}
|
||||
|
||||
// Build command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir).read_config()?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = PathBuf::from(dest_dir);
|
||||
}
|
||||
|
||||
// This flag is deprecated in favor of being set via `book.toml`.
|
||||
if args.is_present("no-create") {
|
||||
book.config.build.create_missing = false;
|
||||
}
|
||||
|
||||
book.build()?;
|
||||
|
||||
if args.is_present("open") {
|
||||
open(book.get_destination().join("index.html"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
75
src/bin/init.rs
Normal file
75
src/bin/init.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use get_book_dir;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("init")
|
||||
.about("Create boilerplate structure and files in the directory")
|
||||
// the {n} denotes a newline which will properly aligned in all help messages
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory \
|
||||
when omitted)'")
|
||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
||||
.arg_from_usage("--force 'skip confirmation prompts'")
|
||||
}
|
||||
|
||||
// Init command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir);
|
||||
|
||||
// Call the function that does the initialization
|
||||
book.init()?;
|
||||
|
||||
// If flag `--theme` is present, copy theme to src
|
||||
if args.is_present("theme") {
|
||||
// Skip this if `--force` is present
|
||||
if !args.is_present("force") {
|
||||
// Print warning
|
||||
print!("\nCopying the default theme to {:?}", book.get_source());
|
||||
println!("could potentially overwrite files already present in that directory.");
|
||||
print!("\nAre you sure you want to continue? (y/n) ");
|
||||
|
||||
// Read answer from user and exit if it's not 'yes'
|
||||
if !confirm() {
|
||||
println!("\nSkipping...\n");
|
||||
println!("All done, no errors...");
|
||||
::std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Call the function that copies the theme
|
||||
book.copy_theme()?;
|
||||
println!("\nTheme copied.");
|
||||
}
|
||||
|
||||
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`
|
||||
let is_dest_inside_root = book.get_destination().starts_with(&book.root);
|
||||
|
||||
if !args.is_present("force") && is_dest_inside_root {
|
||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||
|
||||
if confirm() {
|
||||
book.create_gitignore();
|
||||
println!("\n.gitignore created.");
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nAll done, no errors...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Simple function that user comfirmation
|
||||
fn confirm() -> bool {
|
||||
io::stdout().flush().unwrap();
|
||||
let mut s = String::new();
|
||||
io::stdin().read_line(&mut s).ok();
|
||||
match &*s.trim() {
|
||||
"Y" | "y" | "yes" | "Yes" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -1,97 +1,60 @@
|
||||
extern crate mdbook;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate log;
|
||||
extern crate env_logger;
|
||||
extern crate log;
|
||||
extern crate mdbook;
|
||||
extern crate open;
|
||||
|
||||
// Dependencies for the Watch feature
|
||||
#[cfg(feature = "watch")]
|
||||
extern crate notify;
|
||||
#[cfg(feature = "watch")]
|
||||
extern crate time;
|
||||
#[cfg(feature = "watch")]
|
||||
extern crate crossbeam;
|
||||
|
||||
// Dependencies for the Serve feature
|
||||
#[cfg(feature = "serve")]
|
||||
extern crate iron;
|
||||
#[cfg(feature = "serve")]
|
||||
extern crate staticfile;
|
||||
#[cfg(feature = "serve")]
|
||||
extern crate ws;
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use clap::{App, AppSettings, ArgMatches};
|
||||
use log::{LogLevelFilter, LogRecord};
|
||||
use env_logger::LogBuilder;
|
||||
|
||||
use clap::{App, ArgMatches, SubCommand, AppSettings};
|
||||
|
||||
// Uses for the Watch feature
|
||||
pub mod build;
|
||||
pub mod init;
|
||||
pub mod test;
|
||||
#[cfg(feature = "serve")]
|
||||
pub mod serve;
|
||||
#[cfg(feature = "watch")]
|
||||
use notify::Watcher;
|
||||
#[cfg(feature = "watch")]
|
||||
use std::time::Duration;
|
||||
#[cfg(feature = "watch")]
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
|
||||
use mdbook::MDBook;
|
||||
pub mod watch;
|
||||
|
||||
const NAME: &'static str = "mdbook";
|
||||
|
||||
fn main() {
|
||||
env_logger::init().unwrap();
|
||||
init_logger();
|
||||
|
||||
// Create a list of valid arguments and sub-commands
|
||||
let matches = App::new(NAME)
|
||||
.about("Create a book in form of a static website from markdown files")
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
// Get the version from our Cargo.toml using clap's crate_version!() macro
|
||||
.version(&*format!("v{}", crate_version!()))
|
||||
.setting(AppSettings::SubcommandRequired)
|
||||
.after_help("For more information about a specific command, try `mdbook <command> --help`\nSource code for mdbook available at: https://github.com/azerupi/mdBook")
|
||||
.subcommand(SubCommand::with_name("init")
|
||||
.about("Create boilerplate structure and files in the directory")
|
||||
// the {n} denotes a newline which will properly aligned in all help messages
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")
|
||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
||||
.arg_from_usage("--force 'skip confirmation prompts'"))
|
||||
.subcommand(SubCommand::with_name("build")
|
||||
.about("Build the book from the markdown files")
|
||||
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
|
||||
.arg_from_usage("--no-create 'Will not create non-existent files linked from SUMMARY.md'")
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'"))
|
||||
.subcommand(SubCommand::with_name("watch")
|
||||
.about("Watch the files for changes")
|
||||
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'"))
|
||||
.subcommand(SubCommand::with_name("serve")
|
||||
.about("Serve the book at http://localhost:3000. Rebuild and reload on change.")
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")
|
||||
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
|
||||
.arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
|
||||
.arg_from_usage("-w, --websocket-port=[ws-port] 'Use another port for the websocket connection (livereload){n}(Defaults to 3001)'")
|
||||
.arg_from_usage("-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'")
|
||||
.arg_from_usage("-a, --address=[address] 'Address that the browser can reach the websocket server from{n}(Defaults to the interface address)'")
|
||||
.arg_from_usage("-o, --open 'Open the book server in a web browser'"))
|
||||
.subcommand(SubCommand::with_name("test")
|
||||
.about("Test that code samples compile"))
|
||||
.get_matches();
|
||||
let app = App::new(NAME)
|
||||
.about("Create a book in form of a static website from markdown files")
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
// Get the version from our Cargo.toml using clap's crate_version!() macro
|
||||
.version(concat!("v",crate_version!()))
|
||||
.setting(AppSettings::SubcommandRequired)
|
||||
.after_help("For more information about a specific command, \
|
||||
try `mdbook <command> --help`\n\
|
||||
Source code for mdbook available \
|
||||
at: https://github.com/rust-lang-nursery/mdBook")
|
||||
.subcommand(init::make_subcommand())
|
||||
.subcommand(build::make_subcommand())
|
||||
.subcommand(test::make_subcommand());
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
let app = app.subcommand(watch::make_subcommand());
|
||||
#[cfg(feature = "serve")]
|
||||
let app = app.subcommand(serve::make_subcommand());
|
||||
|
||||
// Check which subcomamnd the user ran...
|
||||
let res = match matches.subcommand() {
|
||||
("init", Some(sub_matches)) => init(sub_matches),
|
||||
("build", Some(sub_matches)) => build(sub_matches),
|
||||
let res = match app.get_matches().subcommand() {
|
||||
("init", Some(sub_matches)) => init::execute(sub_matches),
|
||||
("build", Some(sub_matches)) => build::execute(sub_matches),
|
||||
#[cfg(feature = "watch")]
|
||||
("watch", Some(sub_matches)) => watch(sub_matches),
|
||||
("watch", Some(sub_matches)) => watch::execute(sub_matches),
|
||||
#[cfg(feature = "serve")]
|
||||
("serve", Some(sub_matches)) => serve(sub_matches),
|
||||
("test", Some(sub_matches)) => test(sub_matches),
|
||||
("serve", Some(sub_matches)) => serve::execute(sub_matches),
|
||||
("test", Some(sub_matches)) => test::execute(sub_matches),
|
||||
(_, _) => unreachable!(),
|
||||
};
|
||||
|
||||
@@ -101,207 +64,22 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Simple function that user comfirmation
|
||||
fn confirm() -> bool {
|
||||
io::stdout().flush().unwrap();
|
||||
let mut s = String::new();
|
||||
io::stdin().read_line(&mut s).ok();
|
||||
match &*s.trim() {
|
||||
"Y" | "y" | "yes" | "Yes" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Init command implementation
|
||||
fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir);
|
||||
|
||||
// Call the function that does the initialization
|
||||
try!(book.init());
|
||||
|
||||
// If flag `--theme` is present, copy theme to src
|
||||
if args.is_present("theme") {
|
||||
|
||||
// Skip this if `--force` is present
|
||||
if !args.is_present("force") {
|
||||
// Print warning
|
||||
print!("\nCopying the default theme to {:?}", book.get_src());
|
||||
println!("could potentially overwrite files already present in that directory.");
|
||||
print!("\nAre you sure you want to continue? (y/n) ");
|
||||
|
||||
// Read answer from user and exit if it's not 'yes'
|
||||
if !confirm() {
|
||||
println!("\nSkipping...\n");
|
||||
println!("All done, no errors...");
|
||||
::std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Call the function that copies the theme
|
||||
try!(book.copy_theme());
|
||||
println!("\nTheme copied.");
|
||||
|
||||
}
|
||||
|
||||
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`
|
||||
let is_dest_inside_root = book.get_dest().starts_with(book.get_root());
|
||||
|
||||
if !args.is_present("force") && is_dest_inside_root {
|
||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||
|
||||
if confirm() {
|
||||
book.create_gitignore();
|
||||
println!("\n.gitignore created.");
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nAll done, no errors...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// Build command implementation
|
||||
fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::new(&book_dir).read_config();
|
||||
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
|
||||
None => book
|
||||
fn init_logger() {
|
||||
let format = |record: &LogRecord| {
|
||||
let module_path = record.location().module_path();
|
||||
format!("{}:{}: {}", record.level(), module_path, record.args())
|
||||
};
|
||||
|
||||
if args.is_present("no-create") {
|
||||
book.create_missing = false;
|
||||
let mut builder = LogBuilder::new();
|
||||
builder.format(format).filter(None, LogLevelFilter::Info);
|
||||
|
||||
if let Ok(var) = env::var("RUST_LOG") {
|
||||
builder.parse(&var);
|
||||
}
|
||||
|
||||
try!(book.build());
|
||||
|
||||
if args.is_present("open") {
|
||||
open(book.get_dest().join("index.html"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
builder.init().unwrap();
|
||||
}
|
||||
|
||||
|
||||
// Watch command implementation
|
||||
#[cfg(feature = "watch")]
|
||||
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::new(&book_dir).read_config();
|
||||
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
|
||||
None => book
|
||||
};
|
||||
|
||||
if args.is_present("open") {
|
||||
try!(book.build());
|
||||
open(book.get_dest().join("index.html"));
|
||||
}
|
||||
|
||||
trigger_on_change(&mut book, |path, book| {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
if let Err(e) = book.build() {
|
||||
println!("Error while building: {:?}", e);
|
||||
}
|
||||
println!("");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// Watch command implementation
|
||||
#[cfg(feature = "serve")]
|
||||
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
const RELOAD_COMMAND: &'static str = "reload";
|
||||
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::new(&book_dir).read_config();
|
||||
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
|
||||
None => book
|
||||
};
|
||||
|
||||
let port = args.value_of("port").unwrap_or("3000");
|
||||
let ws_port = args.value_of("ws-port").unwrap_or("3001");
|
||||
let interface = args.value_of("interface").unwrap_or("localhost");
|
||||
let public_address = args.value_of("address").unwrap_or(interface);
|
||||
let open_browser = args.is_present("open");
|
||||
|
||||
let address = format!("{}:{}", interface, port);
|
||||
let ws_address = format!("{}:{}", interface, ws_port);
|
||||
|
||||
book.set_livereload(format!(r#"
|
||||
<script type="text/javascript">
|
||||
var socket = new WebSocket("ws://{}:{}");
|
||||
socket.onmessage = function (event) {{
|
||||
if (event.data === "{}") {{
|
||||
socket.close();
|
||||
location.reload(true); // force reload from server (not from cache)
|
||||
}}
|
||||
}};
|
||||
|
||||
window.onbeforeunload = function() {{
|
||||
socket.close();
|
||||
}}
|
||||
</script>
|
||||
"#, public_address, ws_port, RELOAD_COMMAND).to_owned());
|
||||
|
||||
try!(book.build());
|
||||
|
||||
let staticfile = staticfile::Static::new(book.get_dest());
|
||||
let iron = iron::Iron::new(staticfile);
|
||||
let _iron = iron.http(&*address).unwrap();
|
||||
|
||||
let ws_server = ws::WebSocket::new(|_| {
|
||||
|_| {
|
||||
Ok(())
|
||||
}
|
||||
}).unwrap();
|
||||
|
||||
let broadcaster = ws_server.broadcaster();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
ws_server.listen(&*ws_address).unwrap();
|
||||
});
|
||||
|
||||
println!("\nServing on {}", address);
|
||||
|
||||
if open_browser {
|
||||
open(format!("http://{}", address));
|
||||
}
|
||||
|
||||
trigger_on_change(&mut book, move |path, book| {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
match book.build() {
|
||||
Err(e) => println!("Error while building: {:?}", e),
|
||||
_ => broadcaster.send(RELOAD_COMMAND).unwrap(),
|
||||
}
|
||||
println!("");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir).read_config();
|
||||
|
||||
try!(book.test());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
||||
if let Some(dir) = args.value_of("dir") {
|
||||
// Check if path is relative from current dir, or absolute...
|
||||
@@ -321,60 +99,3 @@ fn open<P: AsRef<OsStr>>(path: P) {
|
||||
println!("Error opening web browser: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calls the closure when a book source file is changed. This is blocking!
|
||||
#[cfg(feature = "watch")]
|
||||
fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
||||
where F: Fn(&Path, &mut MDBook) -> ()
|
||||
{
|
||||
use notify::RecursiveMode::*;
|
||||
use notify::DebouncedEvent::*;
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
println!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
::std::process::exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.get_src(), Recursive) {
|
||||
println!("Error while watching {:?}:\n {:?}", book.get_src(), e);
|
||||
::std::process::exit(0);
|
||||
};
|
||||
|
||||
// Add the book.{json,toml} file to the watcher if it exists, because it's not
|
||||
// located in the source directory
|
||||
if watcher.watch(book.get_root().join("book.json"), NonRecursive).is_err() {
|
||||
// do nothing if book.json is not found
|
||||
}
|
||||
if watcher.watch(book.get_root().join("book.toml"), NonRecursive).is_err() {
|
||||
// do nothing if book.toml is not found
|
||||
}
|
||||
|
||||
println!("\nListening for changes...\n");
|
||||
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(event) => match event {
|
||||
NoticeWrite(path) |
|
||||
NoticeRemove(path) |
|
||||
Create(path) |
|
||||
Write(path) |
|
||||
Remove(path) |
|
||||
Rename(_, path) => {
|
||||
closure(&path, book);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("An error occured: {:?}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
127
src/bin/serve.rs
Normal file
127
src/bin/serve.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
extern crate iron;
|
||||
extern crate staticfile;
|
||||
extern crate ws;
|
||||
|
||||
use std;
|
||||
use std::path::PathBuf;
|
||||
use self::iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response,
|
||||
Set};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use {get_book_dir, open};
|
||||
#[cfg(feature = "watch")]
|
||||
use watch;
|
||||
|
||||
struct ErrorRecover;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("serve")
|
||||
.about(
|
||||
"Serve the book at http://localhost:3000. Rebuild and reload on change.",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'A directory for your book{n}(Defaults to \
|
||||
Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'The output directory for \
|
||||
your book{n}(Defaults to ./book when omitted)'",
|
||||
)
|
||||
.arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
|
||||
.arg_from_usage(
|
||||
"-w, --websocket-port=[ws-port] 'Use another port for the \
|
||||
websocket connection (livereload){n}(Defaults to 3001)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"-a, --address=[address] 'Address that the browser can reach the \
|
||||
websocket server from{n}(Defaults to the interface address)'",
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Open the book server in a web browser'")
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
const RELOAD_COMMAND: &'static str = "reload";
|
||||
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir).read_config()?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = PathBuf::from(dest_dir);
|
||||
}
|
||||
|
||||
let port = args.value_of("port").unwrap_or("3000");
|
||||
let ws_port = args.value_of("websocket-port").unwrap_or("3001");
|
||||
let interface = args.value_of("interface").unwrap_or("localhost");
|
||||
let public_address = args.value_of("address").unwrap_or(interface);
|
||||
let open_browser = args.is_present("open");
|
||||
|
||||
let address = format!("{}:{}", interface, port);
|
||||
let ws_address = format!("{}:{}", interface, ws_port);
|
||||
|
||||
book.livereload = Some(format!(r#"
|
||||
<script type="text/javascript">
|
||||
var socket = new WebSocket("ws://{}:{}");
|
||||
socket.onmessage = function (event) {{
|
||||
if (event.data === "{}") {{
|
||||
socket.close();
|
||||
location.reload(true); // force reload from server (not from cache)
|
||||
}}
|
||||
}};
|
||||
|
||||
window.onbeforeunload = function() {{
|
||||
socket.close();
|
||||
}}
|
||||
</script>
|
||||
"#,
|
||||
public_address,
|
||||
ws_port,
|
||||
RELOAD_COMMAND
|
||||
));
|
||||
|
||||
book.build()?;
|
||||
|
||||
let mut chain = Chain::new(staticfile::Static::new(book.get_destination()));
|
||||
chain.link_after(ErrorRecover);
|
||||
let _iron = Iron::new(chain).http(&*address).unwrap();
|
||||
|
||||
let ws_server = ws::WebSocket::new(|_| |_| Ok(())).unwrap();
|
||||
|
||||
let broadcaster = ws_server.broadcaster();
|
||||
|
||||
std::thread::spawn(move || { ws_server.listen(&*ws_address).unwrap(); });
|
||||
|
||||
let serving_url = format!("http://{}", address);
|
||||
println!("\nServing on: {}", serving_url);
|
||||
|
||||
if open_browser {
|
||||
open(serving_url);
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
watch::trigger_on_change(&mut book, move |path, book| {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
match book.build() {
|
||||
Err(e) => println!("Error while building: {:?}", e),
|
||||
_ => broadcaster.send(RELOAD_COMMAND).unwrap(),
|
||||
}
|
||||
println!("");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl AfterMiddleware for ErrorRecover {
|
||||
fn catch(&self, _: &mut Request, err: IronError) -> IronResult<Response> {
|
||||
match err.response.status {
|
||||
// each error will result in 404 response
|
||||
Some(_) => Ok(err.response.set(status::NotFound)),
|
||||
_ => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/bin/test.rs
Normal file
26
src/bin/test.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use get_book_dir;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("test")
|
||||
.about("Test that code samples compile")
|
||||
.arg_from_usage(
|
||||
"-L, --library-path [DIR]... 'directory to add to crate search path'",
|
||||
)
|
||||
}
|
||||
|
||||
// test command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let library_paths: Vec<&str> = args.values_of("library-path")
|
||||
.map(|v| v.collect())
|
||||
.unwrap_or_default();
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir).read_config()?;
|
||||
|
||||
book.test(library_paths)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
111
src/bin/watch.rs
Normal file
111
src/bin/watch.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
extern crate notify;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use self::notify::Watcher;
|
||||
use std::time::Duration;
|
||||
use std::sync::mpsc::channel;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use {get_book_dir, open};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("watch")
|
||||
.about("Watch the files for changes")
|
||||
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'The output directory for \
|
||||
your book{n}(Defaults to ./book when omitted)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'A directory for your book{n}(Defaults to \
|
||||
Current Directory when omitted)'",
|
||||
)
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir).read_config()?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = PathBuf::from(dest_dir);
|
||||
}
|
||||
|
||||
if args.is_present("open") {
|
||||
book.build()?;
|
||||
open(book.get_destination().join("index.html"));
|
||||
}
|
||||
|
||||
trigger_on_change(&mut book, |path, book| {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
if let Err(e) = book.build() {
|
||||
println!("Error while building: {:?}", e);
|
||||
}
|
||||
println!("");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Calls the closure when a book source file is changed. This is blocking!
|
||||
pub fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
||||
where
|
||||
F: Fn(&Path, &mut MDBook) -> (),
|
||||
{
|
||||
use self::notify::RecursiveMode::*;
|
||||
use self::notify::DebouncedEvent::*;
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
println!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
::std::process::exit(0)
|
||||
}
|
||||
};
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.get_source(), Recursive) {
|
||||
println!("Error while watching {:?}:\n {:?}", book.get_source(), e);
|
||||
::std::process::exit(0);
|
||||
};
|
||||
|
||||
// Add the theme directory to the watcher
|
||||
watcher.watch(book.theme_dir(), Recursive)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Add the book.{json,toml} file to the watcher if it exists, because it's not
|
||||
// located in the source directory
|
||||
if watcher.watch(book.root.join("book.json"), NonRecursive)
|
||||
.is_err()
|
||||
{
|
||||
// do nothing if book.json is not found
|
||||
}
|
||||
if watcher.watch(book.root.join("book.toml"), NonRecursive)
|
||||
.is_err()
|
||||
{
|
||||
// do nothing if book.toml is not found
|
||||
}
|
||||
|
||||
println!("\nListening for changes...\n");
|
||||
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(event) => {
|
||||
match event {
|
||||
Create(path) | Write(path) | Remove(path) | Rename(_, path) => {
|
||||
closure(&path, book);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("An error occured: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
extern crate toml;
|
||||
|
||||
use std::process::exit;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use serde_json;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BookConfig {
|
||||
root: PathBuf,
|
||||
pub dest: PathBuf,
|
||||
pub src: PathBuf,
|
||||
pub theme_path: PathBuf,
|
||||
|
||||
pub title: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
|
||||
pub indent_spaces: i32,
|
||||
multilingual: bool,
|
||||
}
|
||||
|
||||
impl BookConfig {
|
||||
pub fn new(root: &Path) -> Self {
|
||||
BookConfig {
|
||||
root: root.to_owned(),
|
||||
dest: root.join("book"),
|
||||
src: root.join("src"),
|
||||
theme_path: root.join("theme"),
|
||||
|
||||
title: String::new(),
|
||||
author: String::new(),
|
||||
description: String::new(),
|
||||
|
||||
indent_spaces: 4, // indentation used for SUMMARY.md
|
||||
multilingual: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_config(&mut self, root: &Path) -> &mut Self {
|
||||
|
||||
debug!("[fn]: read_config");
|
||||
|
||||
let read_file = |path: PathBuf| -> String {
|
||||
let mut data = String::new();
|
||||
let mut f: File = match File::open(&path) {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
error!("[*]: Failed to open {:?}", &path);
|
||||
exit(2);
|
||||
}
|
||||
};
|
||||
if f.read_to_string(&mut data).is_err() {
|
||||
error!("[*]: Failed to read {:?}", &path);
|
||||
exit(2);
|
||||
}
|
||||
data
|
||||
};
|
||||
|
||||
// Read book.toml or book.json if exists
|
||||
|
||||
if root.join("book.toml").exists() {
|
||||
|
||||
debug!("[*]: Reading config");
|
||||
let data = read_file(root.join("book.toml"));
|
||||
self.parse_from_toml_string(&data);
|
||||
|
||||
} else if root.join("book.json").exists() {
|
||||
|
||||
debug!("[*]: Reading config");
|
||||
let data = read_file(root.join("book.json"));
|
||||
self.parse_from_json_string(&data);
|
||||
|
||||
} else {
|
||||
debug!("[*]: No book.toml or book.json was found, using defaults.");
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn parse_from_toml_string(&mut self, data: &str) -> &mut Self {
|
||||
let config = match toml::from_str(data) {
|
||||
Ok(x) => {x},
|
||||
Err(e) => {
|
||||
error!("[*]: Toml parse errors in book.toml: {:?}", e);
|
||||
exit(2);
|
||||
}
|
||||
};
|
||||
|
||||
self.parse_from_btreemap(&config);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Parses the string to JSON and converts it to BTreeMap<String, toml::Value>.
|
||||
pub fn parse_from_json_string(&mut self, data: &str) -> &mut Self {
|
||||
|
||||
let c: serde_json::Value = match serde_json::from_str(data) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
error!("[*]: JSON parse errors in book.json: {:?}", e);
|
||||
exit(2);
|
||||
}
|
||||
};
|
||||
|
||||
let config = json_object_to_btreemap(c.as_object().unwrap());
|
||||
self.parse_from_btreemap(&config);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn parse_from_btreemap(&mut self, config: &BTreeMap<String, toml::Value>) -> &mut Self {
|
||||
|
||||
// Title, author, description
|
||||
if let Some(a) = config.get("title") {
|
||||
self.title = a.to_string().replace("\"", "");
|
||||
}
|
||||
if let Some(a) = config.get("author") {
|
||||
self.author = a.to_string().replace("\"", "");
|
||||
}
|
||||
if let Some(a) = config.get("description") {
|
||||
self.description = a.to_string().replace("\"", "");
|
||||
}
|
||||
|
||||
// Destination folder
|
||||
if let Some(a) = config.get("dest") {
|
||||
let mut dest = PathBuf::from(&a.to_string().replace("\"", ""));
|
||||
|
||||
// If path is relative make it absolute from the parent directory of src
|
||||
if dest.is_relative() {
|
||||
dest = self.get_root().join(&dest);
|
||||
}
|
||||
self.set_dest(&dest);
|
||||
}
|
||||
|
||||
// Source folder
|
||||
if let Some(a) = config.get("src") {
|
||||
let mut src = PathBuf::from(&a.to_string().replace("\"", ""));
|
||||
if src.is_relative() {
|
||||
src = self.get_root().join(&src);
|
||||
}
|
||||
self.set_src(&src);
|
||||
}
|
||||
|
||||
// Theme path folder
|
||||
if let Some(a) = config.get("theme_path") {
|
||||
let mut theme_path = PathBuf::from(&a.to_string().replace("\"", ""));
|
||||
if theme_path.is_relative() {
|
||||
theme_path = self.get_root().join(&theme_path);
|
||||
}
|
||||
self.set_theme_path(&theme_path);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
pub fn set_root(&mut self, root: &Path) -> &mut Self {
|
||||
self.root = root.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_dest(&self) -> &Path {
|
||||
&self.dest
|
||||
}
|
||||
|
||||
pub fn set_dest(&mut self, dest: &Path) -> &mut Self {
|
||||
self.dest = dest.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_src(&self) -> &Path {
|
||||
&self.src
|
||||
}
|
||||
|
||||
pub fn set_src(&mut self, src: &Path) -> &mut Self {
|
||||
self.src = src.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_theme_path(&self) -> &Path {
|
||||
&self.theme_path
|
||||
}
|
||||
|
||||
pub fn set_theme_path(&mut self, theme_path: &Path) -> &mut Self {
|
||||
self.theme_path = theme_path.to_owned();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json_object_to_btreemap(json: &serde_json::Map<String, serde_json::Value>) -> BTreeMap<String, toml::Value> {
|
||||
let mut config: BTreeMap<String, toml::Value> = BTreeMap::new();
|
||||
|
||||
for (key, value) in json.iter() {
|
||||
config.insert(
|
||||
String::from_str(key).unwrap(),
|
||||
json_value_to_toml_value(value.to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
pub fn json_value_to_toml_value(json: serde_json::Value) -> toml::Value {
|
||||
match json {
|
||||
serde_json::Value::Null => toml::Value::String("".to_string()),
|
||||
serde_json::Value::Bool(x) => toml::Value::Boolean(x),
|
||||
serde_json::Value::Number(ref x) if x.is_i64() => toml::Value::Integer(x.as_i64().unwrap()),
|
||||
serde_json::Value::Number(ref x) if x.is_u64() => toml::Value::Integer(x.as_i64().unwrap()),
|
||||
serde_json::Value::Number(x) => toml::Value::Float(x.as_f64().unwrap()),
|
||||
serde_json::Value::String(x) => toml::Value::String(x),
|
||||
serde_json::Value::Array(x) => {
|
||||
toml::Value::Array(x.iter().map(|v| json_value_to_toml_value(v.to_owned())).collect())
|
||||
},
|
||||
serde_json::Value::Object(x) => {
|
||||
toml::Value::Table(json_object_to_btreemap(&x))
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use std::path::Path;
|
||||
use serde_json;
|
||||
use book::bookconfig::*;
|
||||
|
||||
#[test]
|
||||
fn it_parses_json_config() {
|
||||
let text = r#"
|
||||
{
|
||||
"title": "mdBook Documentation",
|
||||
"description": "Create book from markdown files. Like Gitbook but implemented in Rust",
|
||||
"author": "Mathieu David"
|
||||
}"#;
|
||||
|
||||
// TODO don't require path argument, take pwd
|
||||
let mut config = BookConfig::new(Path::new("."));
|
||||
|
||||
config.parse_from_json_string(&text.to_string());
|
||||
|
||||
let mut expected = BookConfig::new(Path::new("."));
|
||||
expected.title = "mdBook Documentation".to_string();
|
||||
expected.author = "Mathieu David".to_string();
|
||||
expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string();
|
||||
|
||||
assert_eq!(format!("{:#?}", config), format!("{:#?}", expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_parses_toml_config() {
|
||||
let text = r#"
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
author = "Mathieu David"
|
||||
"#;
|
||||
|
||||
// TODO don't require path argument, take pwd
|
||||
let mut config = BookConfig::new(Path::new("."));
|
||||
|
||||
config.parse_from_toml_string(&text.to_string());
|
||||
|
||||
let mut expected = BookConfig::new(Path::new("."));
|
||||
expected.title = "mdBook Documentation".to_string();
|
||||
expected.author = "Mathieu David".to_string();
|
||||
expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string();
|
||||
|
||||
assert_eq!(format!("{:#?}", config), format!("{:#?}", expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_parses_json_nested_array_to_toml() {
|
||||
|
||||
// Example from:
|
||||
// toml-0.2.1/tests/valid/arrays-nested.json
|
||||
|
||||
let text = r#"
|
||||
{
|
||||
"nest": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "array", "value": [
|
||||
{"type": "string", "value": "a"}
|
||||
]},
|
||||
{"type": "array", "value": [
|
||||
{"type": "string", "value": "b"}
|
||||
]}
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let c: serde_json::Value = serde_json::from_str(&text).unwrap();
|
||||
|
||||
let result = json_object_to_btreemap(&c.as_object().unwrap());
|
||||
|
||||
let expected = r#"{
|
||||
"nest": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"a"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"b"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
}"#;
|
||||
|
||||
assert_eq!(format!("{:#?}", result), expected);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn it_parses_json_arrays_to_toml() {
|
||||
|
||||
// Example from:
|
||||
// toml-0.2.1/tests/valid/arrays.json
|
||||
|
||||
let text = r#"
|
||||
{
|
||||
"ints": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "integer", "value": "1"},
|
||||
{"type": "integer", "value": "2"},
|
||||
{"type": "integer", "value": "3"}
|
||||
]
|
||||
},
|
||||
"floats": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "float", "value": "1.1"},
|
||||
{"type": "float", "value": "2.1"},
|
||||
{"type": "float", "value": "3.1"}
|
||||
]
|
||||
},
|
||||
"strings": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "string", "value": "a"},
|
||||
{"type": "string", "value": "b"},
|
||||
{"type": "string", "value": "c"}
|
||||
]
|
||||
},
|
||||
"dates": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "datetime", "value": "1987-07-05T17:45:00Z"},
|
||||
{"type": "datetime", "value": "1979-05-27T07:32:00Z"},
|
||||
{"type": "datetime", "value": "2006-06-01T11:00:00Z"}
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let c: serde_json::Value = serde_json::from_str(&text).unwrap();
|
||||
|
||||
let result = json_object_to_btreemap(&c.as_object().unwrap());
|
||||
|
||||
let expected = r#"{
|
||||
"dates": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"datetime"
|
||||
),
|
||||
"value": String(
|
||||
"1987-07-05T17:45:00Z"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"datetime"
|
||||
),
|
||||
"value": String(
|
||||
"1979-05-27T07:32:00Z"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"datetime"
|
||||
),
|
||||
"value": String(
|
||||
"2006-06-01T11:00:00Z"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
"floats": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"float"
|
||||
),
|
||||
"value": String(
|
||||
"1.1"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"float"
|
||||
),
|
||||
"value": String(
|
||||
"2.1"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"float"
|
||||
),
|
||||
"value": String(
|
||||
"3.1"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
"ints": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"integer"
|
||||
),
|
||||
"value": String(
|
||||
"1"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"integer"
|
||||
),
|
||||
"value": String(
|
||||
"2"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"integer"
|
||||
),
|
||||
"value": String(
|
||||
"3"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
"strings": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"a"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"b"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"c"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
}"#;
|
||||
|
||||
assert_eq!(format!("{:#?}", result), expected);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use serde::{Serialize, Serializer};
|
||||
use serde::ser::SerializeStruct;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BookItem {
|
||||
Chapter(String, Chapter), // String = section
|
||||
@@ -26,7 +27,6 @@ pub struct BookItems<'a> {
|
||||
|
||||
impl Chapter {
|
||||
pub fn new(name: String, path: PathBuf) -> Self {
|
||||
|
||||
Chapter {
|
||||
name: name,
|
||||
path: path,
|
||||
@@ -37,10 +37,13 @@ impl Chapter {
|
||||
|
||||
|
||||
impl Serialize for Chapter {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
|
||||
let mut struct_ = try!(serializer.serialize_struct("Chapter", 2));
|
||||
try!(struct_.serialize_field("name", &self.name));
|
||||
try!(struct_.serialize_field("path", &self.path));
|
||||
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut struct_ = serializer.serialize_struct("Chapter", 2)?;
|
||||
struct_.serialize_field("name", &self.name)?;
|
||||
struct_.serialize_field("path", &self.path)?;
|
||||
struct_.end()
|
||||
}
|
||||
}
|
||||
@@ -60,7 +63,7 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
Some((parent_items, parent_idx)) => {
|
||||
self.items = parent_items;
|
||||
self.current_index = parent_idx + 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let cur = &self.items[self.current_index];
|
||||
@@ -70,10 +73,10 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
self.stack.push((self.items, self.current_index));
|
||||
self.items = &ch.sub_items[..];
|
||||
self.current_index = 0;
|
||||
},
|
||||
}
|
||||
BookItem::Spacer => {
|
||||
self.current_index += 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return Some(cur);
|
||||
|
||||
421
src/book/mod.rs
421
src/book/mod.rs
@@ -1,41 +1,28 @@
|
||||
pub mod bookitem;
|
||||
pub mod bookconfig;
|
||||
|
||||
pub mod bookconfig_test;
|
||||
|
||||
pub use self::bookitem::{BookItem, BookItems};
|
||||
pub use self::bookconfig::BookConfig;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{self, File};
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::io::ErrorKind;
|
||||
use std::process::Command;
|
||||
use tempdir::TempDir;
|
||||
|
||||
use {theme, parse, utils};
|
||||
use renderer::{Renderer, HtmlHandlebars};
|
||||
use {parse, theme, utils};
|
||||
use renderer::{HtmlHandlebars, Renderer};
|
||||
use preprocess;
|
||||
use errors::*;
|
||||
|
||||
use config::Config;
|
||||
|
||||
pub struct MDBook {
|
||||
root: PathBuf,
|
||||
dest: PathBuf,
|
||||
src: PathBuf,
|
||||
theme_path: PathBuf,
|
||||
|
||||
pub title: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
pub root: PathBuf,
|
||||
pub config: Config,
|
||||
|
||||
pub content: Vec<BookItem>,
|
||||
renderer: Box<Renderer>,
|
||||
|
||||
livereload: Option<String>,
|
||||
|
||||
/// Should `mdbook build` create files referenced from SUMMARY.md if they
|
||||
/// don't exist
|
||||
pub create_missing: bool,
|
||||
pub livereload: Option<String>,
|
||||
}
|
||||
|
||||
impl MDBook {
|
||||
@@ -46,14 +33,16 @@ impl MDBook {
|
||||
/// ```no_run
|
||||
/// # extern crate mdbook;
|
||||
/// # use mdbook::MDBook;
|
||||
/// # use std::path::Path;
|
||||
/// # #[allow(unused_variables)]
|
||||
/// # fn main() {
|
||||
/// let book = MDBook::new(Path::new("root_dir"));
|
||||
/// let book = MDBook::new("root_dir");
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// In this example, `root_dir` will be the root directory of our book and is specified in function
|
||||
/// of the current working directory by using a relative path instead of an absolute path.
|
||||
/// In this example, `root_dir` will be the root directory of our book
|
||||
/// and is specified in function of the current working directory
|
||||
/// by using a relative path instead of an
|
||||
/// absolute path.
|
||||
///
|
||||
/// Default directory paths:
|
||||
///
|
||||
@@ -61,42 +50,37 @@ impl MDBook {
|
||||
/// - output: `root/book`
|
||||
/// - theme: `root/theme`
|
||||
///
|
||||
/// They can both be changed by using [`set_src()`](#method.set_src) and [`set_dest()`](#method.set_dest)
|
||||
|
||||
pub fn new(root: &Path) -> MDBook {
|
||||
/// They can both be changed by using [`set_src()`](#method.set_src) and
|
||||
/// [`set_dest()`](#method.set_dest)
|
||||
|
||||
pub fn new<P: Into<PathBuf>>(root: P) -> MDBook {
|
||||
let root = root.into();
|
||||
if !root.exists() || !root.is_dir() {
|
||||
warn!("{:?} No directory with that name", root);
|
||||
}
|
||||
|
||||
MDBook {
|
||||
root: root.to_owned(),
|
||||
dest: root.join("book"),
|
||||
src: root.join("src"),
|
||||
theme_path: root.join("theme"),
|
||||
|
||||
title: String::new(),
|
||||
author: String::new(),
|
||||
description: String::new(),
|
||||
root: root,
|
||||
config: Config::default(),
|
||||
|
||||
content: vec![],
|
||||
renderer: Box::new(HtmlHandlebars::new()),
|
||||
|
||||
livereload: None,
|
||||
create_missing: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a flat depth-first iterator over the elements of the book, it returns an [BookItem enum](bookitem.html):
|
||||
/// Returns a flat depth-first iterator over the elements of the book,
|
||||
/// it returns an [BookItem enum](bookitem.html):
|
||||
/// `(section: String, bookitem: &BookItem)`
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate mdbook;
|
||||
/// # use mdbook::MDBook;
|
||||
/// # use mdbook::BookItem;
|
||||
/// # use std::path::Path;
|
||||
/// # #[allow(unused_variables)]
|
||||
/// # fn main() {
|
||||
/// # let mut book = MDBook::new(Path::new("mybook"));
|
||||
/// # let book = MDBook::new("mybook");
|
||||
/// for item in book.iter() {
|
||||
/// match item {
|
||||
/// &BookItem::Chapter(ref section, ref chapter) => {},
|
||||
@@ -123,7 +107,8 @@ impl MDBook {
|
||||
}
|
||||
}
|
||||
|
||||
/// `init()` creates some boilerplate files and directories to get you started with your book.
|
||||
/// `init()` creates some boilerplate files and directories
|
||||
/// to get you started with your book.
|
||||
///
|
||||
/// ```text
|
||||
/// book-test/
|
||||
@@ -133,73 +118,73 @@ impl MDBook {
|
||||
/// └── SUMMARY.md
|
||||
/// ```
|
||||
///
|
||||
/// It uses the paths given as source and output directories and adds a `SUMMARY.md` and a
|
||||
/// It uses the paths given as source and output directories
|
||||
/// and adds a `SUMMARY.md` and a
|
||||
/// `chapter_1.md` to the source directory.
|
||||
|
||||
pub fn init(&mut self) -> Result<(), Box<Error>> {
|
||||
|
||||
pub fn init(&mut self) -> Result<()> {
|
||||
debug!("[fn]: init");
|
||||
|
||||
if !self.root.exists() {
|
||||
fs::create_dir_all(&self.root).unwrap();
|
||||
info!("{:?} created", &self.root);
|
||||
info!("{:?} created", self.root.display());
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
if !self.dest.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", self.dest);
|
||||
try!(fs::create_dir_all(&self.dest));
|
||||
let dest = self.get_destination();
|
||||
if !dest.exists() {
|
||||
debug!("[*]: {} does not exist, trying to create directory", dest.display());
|
||||
fs::create_dir_all(dest)?;
|
||||
}
|
||||
|
||||
if !self.src.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", self.src);
|
||||
try!(fs::create_dir_all(&self.src));
|
||||
|
||||
let src = self.get_source();
|
||||
if !src.exists() {
|
||||
debug!("[*]: {} does not exist, trying to create directory", src.display());
|
||||
fs::create_dir_all(&src)?;
|
||||
}
|
||||
|
||||
let summary = self.src.join("SUMMARY.md");
|
||||
let summary = src.join("SUMMARY.md");
|
||||
|
||||
if !summary.exists() {
|
||||
|
||||
// Summary does not exist, create it
|
||||
|
||||
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", self.src.join("SUMMARY.md"));
|
||||
let mut f = try!(File::create(&self.src.join("SUMMARY.md")));
|
||||
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md",
|
||||
&summary);
|
||||
let mut f = File::create(&summary)?;
|
||||
|
||||
debug!("[*]: Writing to SUMMARY.md");
|
||||
|
||||
try!(writeln!(f, "# Summary"));
|
||||
try!(writeln!(f, ""));
|
||||
try!(writeln!(f, "- [Chapter 1](./chapter_1.md)"));
|
||||
writeln!(f, "# Summary")?;
|
||||
writeln!(f, "")?;
|
||||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||
}
|
||||
}
|
||||
|
||||
// parse SUMMARY.md, and create the missing item related file
|
||||
try!(self.parse_summary());
|
||||
self.parse_summary()?;
|
||||
|
||||
debug!("[*]: constructing paths for missing files");
|
||||
for item in self.iter() {
|
||||
debug!("[*]: item: {:?}", item);
|
||||
let ch = match *item {
|
||||
BookItem::Spacer => continue,
|
||||
BookItem::Chapter(_, ref ch) |
|
||||
BookItem::Affix(ref ch) => ch,
|
||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => ch,
|
||||
};
|
||||
if !ch.path.as_os_str().is_empty() {
|
||||
let path = self.src.join(&ch.path);
|
||||
let path = self.get_source().join(&ch.path);
|
||||
|
||||
if !path.exists() {
|
||||
if !self.create_missing {
|
||||
return Err(format!(
|
||||
"'{}' referenced from SUMMARY.md does not exist.",
|
||||
path.to_string_lossy()).into());
|
||||
if !self.config.build.create_missing {
|
||||
return Err(
|
||||
format!("'{}' referenced from SUMMARY.md does not exist.", path.to_string_lossy()).into(),
|
||||
);
|
||||
}
|
||||
debug!("[*]: {:?} does not exist, trying to create file", path);
|
||||
try!(::std::fs::create_dir_all(path.parent().unwrap()));
|
||||
let mut f = try!(File::create(path));
|
||||
::std::fs::create_dir_all(path.parent().unwrap())?;
|
||||
let mut f = File::create(path)?;
|
||||
|
||||
// debug!("[*]: Writing to {:?}", path);
|
||||
try!(writeln!(f, "# {}", ch.name));
|
||||
writeln!(f, "# {}", ch.name)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,20 +196,20 @@ impl MDBook {
|
||||
pub fn create_gitignore(&self) {
|
||||
let gitignore = self.get_gitignore();
|
||||
|
||||
if !gitignore.exists() {
|
||||
// Gitignore does not exist, create it
|
||||
let destination = self.get_destination();
|
||||
|
||||
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`. If it
|
||||
// is not, `strip_prefix` will return an Error.
|
||||
if !self.get_dest().starts_with(&self.root) {
|
||||
return;
|
||||
}
|
||||
|
||||
let relative = self.get_dest()
|
||||
.strip_prefix(&self.root)
|
||||
.expect("Destination is not relative to root.");
|
||||
let relative = relative.to_str()
|
||||
.expect("Path could not be yielded into a string slice.");
|
||||
// Check that the gitignore does not extist and that the destination path
|
||||
// begins with the root path
|
||||
// We assume tha if it does begin with the root path it is contained within.
|
||||
// This assumption
|
||||
// will not hold true for paths containing double dots to go back up e.g.
|
||||
// `root/../destination`
|
||||
if !gitignore.exists() && destination.starts_with(&self.root) {
|
||||
let relative = destination
|
||||
.strip_prefix(&self.root)
|
||||
.expect("Could not strip the root prefix, path is not relative to root")
|
||||
.to_str()
|
||||
.expect("Could not convert to &str");
|
||||
|
||||
debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore);
|
||||
|
||||
@@ -232,26 +217,25 @@ impl MDBook {
|
||||
|
||||
debug!("[*]: Writing to .gitignore");
|
||||
|
||||
writeln!(f, "{}", relative).expect("Could not write to file.");
|
||||
writeln!(f, "/{}", relative).expect("Could not write to file.");
|
||||
}
|
||||
}
|
||||
|
||||
/// The `build()` method is the one where everything happens. First it parses `SUMMARY.md` to
|
||||
/// construct the book's structure in the form of a `Vec<BookItem>` and then calls `render()`
|
||||
/// The `build()` method is the one where everything happens.
|
||||
/// First it parses `SUMMARY.md` to construct the book's structure
|
||||
/// in the form of a `Vec<BookItem>` and then calls `render()`
|
||||
/// method of the current renderer.
|
||||
///
|
||||
/// It is the renderer who generates all the output files.
|
||||
pub fn build(&mut self) -> Result<(), Box<Error>> {
|
||||
pub fn build(&mut self) -> Result<()> {
|
||||
debug!("[fn]: build");
|
||||
|
||||
try!(self.init());
|
||||
self.init()?;
|
||||
|
||||
// Clean output directory
|
||||
try!(utils::fs::remove_dir_content(&self.dest));
|
||||
utils::fs::remove_dir_content(&self.get_destination())?;
|
||||
|
||||
try!(self.renderer.render(&self));
|
||||
|
||||
Ok(())
|
||||
self.renderer.render(self)
|
||||
}
|
||||
|
||||
|
||||
@@ -259,122 +243,135 @@ impl MDBook {
|
||||
self.root.join(".gitignore")
|
||||
}
|
||||
|
||||
pub fn copy_theme(&self) -> Result<(), Box<Error>> {
|
||||
pub fn copy_theme(&self) -> Result<()> {
|
||||
debug!("[fn]: copy_theme");
|
||||
|
||||
let theme_dir = self.src.join("theme");
|
||||
let themedir = self.theme_dir();
|
||||
|
||||
if !theme_dir.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", theme_dir);
|
||||
try!(fs::create_dir(&theme_dir));
|
||||
if !themedir.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory",
|
||||
themedir);
|
||||
fs::create_dir(&themedir)?;
|
||||
}
|
||||
|
||||
// index.hbs
|
||||
let mut index = try!(File::create(&theme_dir.join("index.hbs")));
|
||||
try!(index.write_all(theme::INDEX));
|
||||
let mut index = File::create(themedir.join("index.hbs"))?;
|
||||
index.write_all(theme::INDEX)?;
|
||||
|
||||
// header.hbs
|
||||
let mut header = File::create(themedir.join("header.hbs"))?;
|
||||
header.write_all(theme::HEADER)?;
|
||||
|
||||
// book.css
|
||||
let mut css = try!(File::create(&theme_dir.join("book.css")));
|
||||
try!(css.write_all(theme::CSS));
|
||||
let mut css = File::create(themedir.join("book.css"))?;
|
||||
css.write_all(theme::CSS)?;
|
||||
|
||||
// favicon.png
|
||||
let mut favicon = try!(File::create(&theme_dir.join("favicon.png")));
|
||||
try!(favicon.write_all(theme::FAVICON));
|
||||
let mut favicon = File::create(themedir.join("favicon.png"))?;
|
||||
favicon.write_all(theme::FAVICON)?;
|
||||
|
||||
// book.js
|
||||
let mut js = try!(File::create(&theme_dir.join("book.js")));
|
||||
try!(js.write_all(theme::JS));
|
||||
let mut js = File::create(themedir.join("book.js"))?;
|
||||
js.write_all(theme::JS)?;
|
||||
|
||||
// highlight.css
|
||||
let mut highlight_css = try!(File::create(&theme_dir.join("highlight.css")));
|
||||
try!(highlight_css.write_all(theme::HIGHLIGHT_CSS));
|
||||
let mut highlight_css = File::create(themedir.join("highlight.css"))?;
|
||||
highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
|
||||
|
||||
// highlight.js
|
||||
let mut highlight_js = try!(File::create(&theme_dir.join("highlight.js")));
|
||||
try!(highlight_js.write_all(theme::HIGHLIGHT_JS));
|
||||
let mut highlight_js = File::create(themedir.join("highlight.js"))?;
|
||||
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_file<P: AsRef<Path>>(&self, filename: P, content: &[u8]) -> Result<(), Box<Error>> {
|
||||
let path = self.get_dest().join(filename);
|
||||
try!(utils::fs::create_file(&path).and_then(|mut file| {
|
||||
file.write_all(content)
|
||||
}).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("Could not create {}: {}", path.display(), e))
|
||||
}));
|
||||
Ok(())
|
||||
pub fn write_file<P: AsRef<Path>>(&self, filename: P, content: &[u8]) -> Result<()> {
|
||||
let path = self.get_destination().join(filename);
|
||||
|
||||
utils::fs::create_file(&path)?.write_all(content)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Parses the `book.json` file (if it exists) to extract the configuration parameters.
|
||||
/// Parses the `book.json` file (if it exists) to extract
|
||||
/// the configuration parameters.
|
||||
/// The `book.json` file should be in the root directory of the book.
|
||||
/// The root directory is the one specified when creating a new `MDBook`
|
||||
|
||||
pub fn read_config(mut self) -> Self {
|
||||
pub fn read_config(mut self) -> Result<Self> {
|
||||
let config_path = self.root.join("book.toml");
|
||||
|
||||
let config = BookConfig::new(&self.root)
|
||||
.read_config(&self.root)
|
||||
.to_owned();
|
||||
if config_path.exists() {
|
||||
debug!("[*] Loading the config from {}", config_path.display());
|
||||
self.config = Config::from_disk(&config_path)?;
|
||||
} else {
|
||||
self.config = Config::default();
|
||||
}
|
||||
|
||||
self.title = config.title;
|
||||
self.description = config.description;
|
||||
self.author = config.author;
|
||||
|
||||
self.dest = config.dest;
|
||||
self.src = config.src;
|
||||
self.theme_path = config.theme_path;
|
||||
|
||||
self
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// You can change the default renderer to another one by using this method. The only requirement
|
||||
/// is for your renderer to implement the [Renderer trait](../../renderer/renderer/trait.Renderer.html)
|
||||
/// You can change the default renderer to another one
|
||||
/// by using this method. The only requirement
|
||||
/// is for your renderer to implement the
|
||||
/// [Renderer trait](../../renderer/renderer/trait.Renderer.html)
|
||||
///
|
||||
/// ```no_run
|
||||
/// extern crate mdbook;
|
||||
/// use mdbook::MDBook;
|
||||
/// use mdbook::renderer::HtmlHandlebars;
|
||||
/// # use std::path::Path;
|
||||
///
|
||||
/// # #[allow(unused_variables)]
|
||||
/// fn main() {
|
||||
/// let mut book = MDBook::new(Path::new("mybook"))
|
||||
/// let book = MDBook::new("mybook")
|
||||
/// .set_renderer(Box::new(HtmlHandlebars::new()));
|
||||
///
|
||||
/// // In this example we replace the default renderer by the default renderer...
|
||||
/// // Don't forget to put your renderer in a Box
|
||||
/// // In this example we replace the default renderer
|
||||
/// // by the default renderer...
|
||||
/// // Don't forget to put your renderer in a Box
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// **note:** Don't forget to put your renderer in a `Box` before passing it to `set_renderer()`
|
||||
/// **note:** Don't forget to put your renderer in a `Box`
|
||||
/// before passing it to `set_renderer()`
|
||||
|
||||
pub fn set_renderer(mut self, renderer: Box<Renderer>) -> Self {
|
||||
self.renderer = renderer;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn test(&mut self) -> Result<(), Box<Error>> {
|
||||
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
|
||||
// read in the chapters
|
||||
try!(self.parse_summary());
|
||||
self.parse_summary().chain_err(|| "Couldn't parse summary")?;
|
||||
let library_args: Vec<&str> = (0..library_paths.len())
|
||||
.map(|_| "-L")
|
||||
.zip(library_paths.into_iter())
|
||||
.flat_map(|x| vec![x.0, x.1])
|
||||
.collect();
|
||||
let temp_dir = TempDir::new("mdbook")?;
|
||||
for item in self.iter() {
|
||||
|
||||
if let BookItem::Chapter(_, ref ch) = *item {
|
||||
if ch.path != PathBuf::new() {
|
||||
|
||||
let path = self.get_src().join(&ch.path);
|
||||
|
||||
if !ch.path.as_os_str().is_empty() {
|
||||
let path = self.get_source().join(&ch.path);
|
||||
let base = path.parent()
|
||||
.ok_or_else(|| String::from("Invalid bookitem path!"))?;
|
||||
let content = utils::fs::file_to_string(&path)?;
|
||||
// Parse and expand links
|
||||
let content = preprocess::links::replace_all(&content, base)?;
|
||||
println!("[*]: Testing file: {:?}", path);
|
||||
|
||||
let output_result = Command::new("rustdoc")
|
||||
.arg(&path)
|
||||
.arg("--test")
|
||||
.output();
|
||||
let output = try!(output_result);
|
||||
// write preprocessed file to tempdir
|
||||
let path = temp_dir.path().join(&ch.path);
|
||||
let mut tmpf = utils::fs::create_file(&path)?;
|
||||
tmpf.write_all(content.as_bytes())?;
|
||||
|
||||
let output = Command::new("rustdoc").arg(&path)
|
||||
.arg("--test")
|
||||
.args(&library_args)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(Box::new(io::Error::new(ErrorKind::Other, format!(
|
||||
"{}\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)))) as Box<Error>);
|
||||
bail!(ErrorKind::Subprocess("Rustdoc returned an error".to_string(),
|
||||
output));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,102 +379,26 @@ impl MDBook {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
pub fn set_dest(mut self, dest: &Path) -> Self {
|
||||
|
||||
// Handle absolute and relative paths
|
||||
if dest.is_absolute() {
|
||||
self.dest = dest.to_owned();
|
||||
} else {
|
||||
let dest = self.root.join(dest).to_owned();
|
||||
self.dest = dest;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_dest(&self) -> &Path {
|
||||
&self.dest
|
||||
}
|
||||
|
||||
pub fn set_src(mut self, src: &Path) -> Self {
|
||||
|
||||
// Handle absolute and relative paths
|
||||
if src.is_absolute() {
|
||||
self.src = src.to_owned();
|
||||
} else {
|
||||
let src = self.root.join(src).to_owned();
|
||||
self.src = src;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_src(&self) -> &Path {
|
||||
&self.src
|
||||
}
|
||||
|
||||
pub fn set_title(mut self, title: &str) -> Self {
|
||||
self.title = title.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
pub fn set_author(mut self, author: &str) -> Self {
|
||||
self.author = author.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_author(&self) -> &str {
|
||||
&self.author
|
||||
}
|
||||
|
||||
pub fn set_description(mut self, description: &str) -> Self {
|
||||
self.description = description.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_description(&self) -> &str {
|
||||
&self.description
|
||||
}
|
||||
|
||||
pub fn set_livereload(&mut self, livereload: String) -> &mut Self {
|
||||
self.livereload = Some(livereload);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unset_livereload(&mut self) -> &Self {
|
||||
self.livereload = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_livereload(&self) -> Option<&String> {
|
||||
self.livereload.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_theme_path(mut self, theme_path: &Path) -> Self {
|
||||
self.theme_path = if theme_path.is_absolute() {
|
||||
theme_path.to_owned()
|
||||
} else {
|
||||
self.root.join(theme_path).to_owned()
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_theme_path(&self) -> &Path {
|
||||
&self.theme_path
|
||||
}
|
||||
|
||||
// Construct book
|
||||
fn parse_summary(&mut self) -> Result<(), Box<Error>> {
|
||||
fn parse_summary(&mut self) -> Result<()> {
|
||||
// When append becomes stable, use self.content.append() ...
|
||||
self.content = try!(parse::construct_bookitems(&self.src.join("SUMMARY.md")));
|
||||
let summary = self.get_source().join("SUMMARY.md");
|
||||
self.content = parse::construct_bookitems(&summary)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_destination(&self) -> PathBuf {
|
||||
self.root.join(&self.config.build.build_dir)
|
||||
}
|
||||
|
||||
pub fn get_source(&self) -> PathBuf {
|
||||
self.root.join(&self.config.book.src)
|
||||
}
|
||||
|
||||
pub fn theme_dir(&self) -> PathBuf {
|
||||
match self.config.html_config().and_then(|h| h.theme) {
|
||||
Some(d) => self.root.join(d),
|
||||
None => self.root.join("theme"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
425
src/config.rs
Normal file
425
src/config.rs
Normal file
@@ -0,0 +1,425 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use toml::{self, Value};
|
||||
use toml::value::Table;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use errors::*;
|
||||
|
||||
/// The overall configuration object for MDBook.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct Config {
|
||||
/// Metadata about the book.
|
||||
pub book: BookConfig,
|
||||
pub build: BuildConfig,
|
||||
rest: Table,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load a `Config` from some string.
|
||||
pub fn from_str(src: &str) -> Result<Config> {
|
||||
toml::from_str(src).chain_err(|| Error::from("Invalid configuration file"))
|
||||
}
|
||||
|
||||
/// Load the configuration file from disk.
|
||||
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
|
||||
let mut buffer = String::new();
|
||||
File::open(config_file).chain_err(|| "Unable to open the configuration file")?
|
||||
.read_to_string(&mut buffer)
|
||||
.chain_err(|| "Couldn't read the file")?;
|
||||
|
||||
Config::from_str(&buffer)
|
||||
}
|
||||
|
||||
/// Fetch an arbitrary item from the `Config` as a `toml::Value`.
|
||||
///
|
||||
/// You can use dotted indices to access nested items (e.g.
|
||||
/// `output.html.playpen` will fetch the "playpen" out of the html output
|
||||
/// table).
|
||||
pub fn get(&self, key: &str) -> Option<&Value> {
|
||||
let pieces: Vec<_> = key.split(".").collect();
|
||||
recursive_get(&pieces, &self.rest)
|
||||
}
|
||||
|
||||
/// Fetch a value from the `Config` so you can mutate it.
|
||||
pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Value> {
|
||||
let pieces: Vec<_> = key.split(".").collect();
|
||||
recursive_get_mut(&pieces, &mut self.rest)
|
||||
}
|
||||
|
||||
/// Convenience method for getting the html renderer's configuration.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is for compatibility only. It will be removed completely once the
|
||||
/// rendering and plugin system is established.
|
||||
pub fn html_config(&self) -> Option<HtmlConfig> {
|
||||
self.get_deserialized("output.html").ok()
|
||||
}
|
||||
|
||||
/// Convenience function to fetch a value from the config and deserialize it
|
||||
/// into some arbitrary type.
|
||||
pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
|
||||
let name = name.as_ref();
|
||||
|
||||
if let Some(value) = self.get(name) {
|
||||
value.clone()
|
||||
.try_into()
|
||||
.chain_err(|| "Couldn't deserialize the value")
|
||||
} else {
|
||||
bail!("Key not found, {:?}", name)
|
||||
}
|
||||
}
|
||||
|
||||
fn from_legacy(mut table: Table) -> Config {
|
||||
let mut cfg = Config::default();
|
||||
|
||||
// we use a macro here instead of a normal loop because the $out
|
||||
// variable can be different types. This way we can make type inference
|
||||
// figure out what try_into() deserializes to.
|
||||
macro_rules! get_and_insert {
|
||||
($table:expr, $key:expr => $out:expr) => {
|
||||
if let Some(value) = $table.remove($key).and_then(|v| v.try_into().ok()) {
|
||||
$out = value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get_and_insert!(table, "title" => cfg.book.title);
|
||||
get_and_insert!(table, "authors" => cfg.book.authors);
|
||||
get_and_insert!(table, "source" => cfg.book.src);
|
||||
get_and_insert!(table, "description" => cfg.book.description);
|
||||
|
||||
// This complicated chain of and_then's is so we can move
|
||||
// "output.html.destination" to "build.build_dir" and parse it into a
|
||||
// PathBuf.
|
||||
let destination: Option<PathBuf> = table.get_mut("output")
|
||||
.and_then(|output| output.as_table_mut())
|
||||
.and_then(|output| output.get_mut("html"))
|
||||
.and_then(|html| html.as_table_mut())
|
||||
.and_then(|html| html.remove("destination"))
|
||||
.and_then(|dest| dest.try_into().ok());
|
||||
|
||||
if let Some(dest) = destination {
|
||||
cfg.build.build_dir = dest;
|
||||
}
|
||||
|
||||
cfg.rest = table;
|
||||
cfg
|
||||
}
|
||||
}
|
||||
|
||||
fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> {
|
||||
if key.is_empty() {
|
||||
return None;
|
||||
} else if key.len() == 1 {
|
||||
return table.get(key[0]);
|
||||
}
|
||||
|
||||
let first = key[0];
|
||||
let rest = &key[1..];
|
||||
|
||||
if let Some(&Value::Table(ref nested)) = table.get(first) {
|
||||
recursive_get(rest, nested)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn recursive_get_mut<'a>(key: &[&str], table: &'a mut Table) -> Option<&'a mut Value> {
|
||||
// TODO: Figure out how to abstract over mutability to reduce copy-pasta
|
||||
if key.is_empty() {
|
||||
return None;
|
||||
} else if key.len() == 1 {
|
||||
return table.get_mut(key[0]);
|
||||
}
|
||||
|
||||
let first = key[0];
|
||||
let rest = &key[1..];
|
||||
|
||||
if let Some(&mut Value::Table(ref mut nested)) = table.get_mut(first) {
|
||||
recursive_get_mut(rest, nested)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Config {
|
||||
fn deserialize<D: Deserializer<'de>>(de: D) -> ::std::result::Result<Self, D::Error> {
|
||||
let raw = Value::deserialize(de)?;
|
||||
|
||||
let mut table = match raw {
|
||||
Value::Table(t) => t,
|
||||
_ => {
|
||||
use serde::de::Error;
|
||||
return Err(D::Error::custom(
|
||||
"A config file should always be a toml table",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if is_legacy_format(&table) {
|
||||
warn!("It looks like you are using the legacy book.toml format.");
|
||||
warn!("We'll parse it for now, but you should probably convert to the new format.");
|
||||
warn!("See the mdbook documentation for more details, although as a rule of thumb");
|
||||
warn!("just move all top level configuration entries like `title`, `author` and");
|
||||
warn!("`description` under a table called `[book]`, move the `destination` entry");
|
||||
warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
|
||||
warn!("`[build]`, and it should all work.");
|
||||
warn!("Documentation: http://rust-lang-nursery.github.io/mdBook/format/config.html");
|
||||
return Ok(Config::from_legacy(table));
|
||||
}
|
||||
|
||||
let book: BookConfig = table.remove("book")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let build: BuildConfig = table.remove("build")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Config {
|
||||
book: book,
|
||||
build: build,
|
||||
rest: table,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn is_legacy_format(table: &Table) -> bool {
|
||||
let top_level_items = ["title", "author", "authors"];
|
||||
|
||||
top_level_items.iter().any(|key| table.contains_key(&key.to_string()))
|
||||
}
|
||||
|
||||
|
||||
/// Configuration options which are specific to the book and required for
|
||||
/// loading it from disk.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct BookConfig {
|
||||
/// The book's title.
|
||||
pub title: Option<String>,
|
||||
/// The book's authors.
|
||||
pub authors: Vec<String>,
|
||||
/// An optional description for the book.
|
||||
pub description: Option<String>,
|
||||
/// Location of the book source relative to the book's root directory.
|
||||
pub src: PathBuf,
|
||||
/// Does this book support more than one language?
|
||||
pub multilingual: bool,
|
||||
}
|
||||
|
||||
impl Default for BookConfig {
|
||||
fn default() -> BookConfig {
|
||||
BookConfig {
|
||||
title: None,
|
||||
authors: Vec::new(),
|
||||
description: None,
|
||||
src: PathBuf::from("src"),
|
||||
multilingual: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the build procedure.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct BuildConfig {
|
||||
/// Where to put built artefacts relative to the book's root directory.
|
||||
pub build_dir: PathBuf,
|
||||
/// Should non-existent markdown files specified in `SETTINGS.md` be created
|
||||
/// if they don't exist?
|
||||
pub create_missing: bool,
|
||||
}
|
||||
|
||||
impl Default for BuildConfig {
|
||||
fn default() -> BuildConfig {
|
||||
BuildConfig {
|
||||
build_dir: PathBuf::from("book"),
|
||||
create_missing: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct HtmlConfig {
|
||||
pub theme: Option<PathBuf>,
|
||||
pub curly_quotes: bool,
|
||||
pub mathjax_support: bool,
|
||||
pub google_analytics: Option<String>,
|
||||
pub additional_css: Vec<PathBuf>,
|
||||
pub additional_js: Vec<PathBuf>,
|
||||
pub playpen: Playpen,
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the the HTML renderer handles the playpen.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Playpen {
|
||||
pub editor: PathBuf,
|
||||
pub editable: bool,
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const COMPLEX_CONFIG: &'static str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
|
||||
description = "A completely useless book"
|
||||
multilingual = true
|
||||
src = "source"
|
||||
|
||||
[build]
|
||||
build-dir = "outputs"
|
||||
create-missing = false
|
||||
|
||||
[output.html]
|
||||
theme = "./themedir"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["./foo/bar/baz.css"]
|
||||
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
editor = "ace"
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn load_a_complex_config_file() {
|
||||
let src = COMPLEX_CONFIG;
|
||||
|
||||
let book_should_be = BookConfig {
|
||||
title: Some(String::from("Some Book")),
|
||||
authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
|
||||
description: Some(String::from("A completely useless book")),
|
||||
multilingual: true,
|
||||
src: PathBuf::from("source"),
|
||||
..Default::default()
|
||||
};
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("outputs"),
|
||||
create_missing: false,
|
||||
};
|
||||
let playpen_should_be = Playpen {
|
||||
editable: true,
|
||||
editor: PathBuf::from("ace"),
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
|
||||
theme: Some(PathBuf::from("./themedir")),
|
||||
playpen: playpen_should_be,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
|
||||
assert_eq!(got.book, book_should_be);
|
||||
assert_eq!(got.build, build_should_be);
|
||||
assert_eq!(got.html_config().unwrap(), html_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_arbitrary_output_type() {
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
struct RandomOutput {
|
||||
foo: u32,
|
||||
bar: String,
|
||||
baz: Vec<bool>,
|
||||
}
|
||||
|
||||
let src = r#"
|
||||
[output.random]
|
||||
foo = 5
|
||||
bar = "Hello World"
|
||||
baz = [true, true, false]
|
||||
"#;
|
||||
|
||||
let should_be = RandomOutput {
|
||||
foo: 5,
|
||||
bar: String::from("Hello World"),
|
||||
baz: vec![true, true, false],
|
||||
};
|
||||
|
||||
let cfg = Config::from_str(src).unwrap();
|
||||
let got: RandomOutput = cfg.get_deserialized("output.random").unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
let baz: Vec<bool> = cfg.get_deserialized("output.random.baz").unwrap();
|
||||
let baz_should_be = vec![true, true, false];
|
||||
|
||||
assert_eq!(baz, baz_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutate_some_stuff() {
|
||||
// really this is just a sanity check to make sure the borrow checker
|
||||
// is happy...
|
||||
let src = COMPLEX_CONFIG;
|
||||
let mut config = Config::from_str(src).unwrap();
|
||||
let key = "output.html.playpen.editable";
|
||||
|
||||
assert_eq!(config.get(key).unwrap(), &Value::Boolean(true));
|
||||
*config.get_mut(key).unwrap() = Value::Boolean(false);
|
||||
assert_eq!(config.get(key).unwrap(), &Value::Boolean(false));
|
||||
}
|
||||
|
||||
/// The config file format has slightly changed (metadata stuff is now under
|
||||
/// the `book` table instead of being at the top level) so we're adding a
|
||||
/// **temporary** compatibility check. You should be able to still load the
|
||||
/// old format, emitting a warning.
|
||||
#[test]
|
||||
fn can_still_load_the_previous_format() {
|
||||
let src = r#"
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
source = "./source"
|
||||
|
||||
[output.html]
|
||||
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
|
||||
theme = "my-theme"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
"#;
|
||||
|
||||
let book_should_be = BookConfig {
|
||||
title: Some(String::from("mdBook Documentation")),
|
||||
description: Some(String::from(
|
||||
"Create book from markdown files. Like Gitbook but implemented in Rust",
|
||||
)),
|
||||
authors: vec![String::from("Mathieu David")],
|
||||
src: PathBuf::from("./source"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("my-book"),
|
||||
create_missing: true,
|
||||
};
|
||||
|
||||
let html_should_be = HtmlConfig {
|
||||
theme: Some(PathBuf::from("my-theme")),
|
||||
curly_quotes: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")],
|
||||
additional_js: vec![PathBuf::from("custom.js")],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.book, book_should_be);
|
||||
assert_eq!(got.build, build_should_be);
|
||||
assert_eq!(got.html_config().unwrap(), html_should_be);
|
||||
}
|
||||
}
|
||||
81
src/lib.rs
81
src/lib.rs
@@ -3,8 +3,8 @@
|
||||
//! **mdBook** is similar to Gitbook but implemented in Rust.
|
||||
//! It offers a command line interface, but can also be used as a regular crate.
|
||||
//!
|
||||
//! This is the API doc, but you can find a [less "low-level" documentation here](../index.html) that
|
||||
//! contains information about the command line tool, format, structure etc.
|
||||
//! This is the API doc, but you can find a [less "low-level" documentation here](../index.html)
|
||||
//! that contains information about the command line tool, format, structure etc.
|
||||
//! It is also rendered with mdBook to showcase the features and default theme.
|
||||
//!
|
||||
//! Some reasons why you would want to use the crate (over the cli):
|
||||
@@ -21,15 +21,17 @@
|
||||
//! extern crate mdbook;
|
||||
//!
|
||||
//! use mdbook::MDBook;
|
||||
//! use std::path::Path;
|
||||
//! use std::path::PathBuf;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let mut book = MDBook::new(Path::new("my-book")) // Path to root
|
||||
//! .set_src(Path::new("src")) // Path from root to source directory
|
||||
//! .set_dest(Path::new("book")) // Path from root to output directory
|
||||
//! .read_config(); // Parse book.json file for configuration
|
||||
//!
|
||||
//! book.build().unwrap(); // Render the book
|
||||
//! let mut md = MDBook::new("my-book");
|
||||
//!
|
||||
//! // tweak the book configuration a bit
|
||||
//! md.config.book.src = PathBuf::from("source");
|
||||
//! md.config.build.build_dir = PathBuf::from("book");
|
||||
//!
|
||||
//! // Render the book
|
||||
//! md.build().unwrap();
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -45,12 +47,12 @@
|
||||
//! #
|
||||
//! # use mdbook::MDBook;
|
||||
//! # use mdbook::renderer::HtmlHandlebars;
|
||||
//! # use std::path::Path;
|
||||
//! #
|
||||
//! # #[allow(unused_variables)]
|
||||
//! # fn main() {
|
||||
//! # let your_renderer = HtmlHandlebars::new();
|
||||
//! #
|
||||
//! let book = MDBook::new(Path::new("my-book")).set_renderer(Box::new(your_renderer));
|
||||
//! let book = MDBook::new("my-book").set_renderer(Box::new(your_renderer));
|
||||
//! # }
|
||||
//! ```
|
||||
//! If you make a renderer, you get the book constructed in form of `Vec<BookItems>` and you get
|
||||
@@ -60,30 +62,67 @@
|
||||
//!
|
||||
//! ## utils
|
||||
//!
|
||||
//! I have regrouped some useful functions in the [utils](utils/index.html) module, like the following function
|
||||
//! I have regrouped some useful functions in the [utils](utils/index.html) module, like the
|
||||
//! following function [`utils::fs::create_file(path:
|
||||
//! &Path)`](utils/fs/fn.create_file.html)
|
||||
//!
|
||||
//! ```ignore
|
||||
//! utils::fs::create_path(path: &Path)
|
||||
//! ```
|
||||
//! This function creates all the directories in a given path if they do not exist
|
||||
//! This function creates a file and returns it. But before creating the file
|
||||
//! it checks every directory in the path to see if it exists, and if it does
|
||||
//! not it will be created.
|
||||
//!
|
||||
//! Make sure to take a look at it.
|
||||
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
extern crate error_chain;
|
||||
extern crate handlebars;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate regex;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
extern crate tempdir;
|
||||
extern crate toml;
|
||||
|
||||
#[macro_use] extern crate log;
|
||||
pub mod book;
|
||||
mod parse;
|
||||
mod preprocess;
|
||||
pub mod book;
|
||||
pub mod config;
|
||||
pub mod renderer;
|
||||
pub mod theme;
|
||||
pub mod utils;
|
||||
|
||||
pub use book::MDBook;
|
||||
pub use book::BookItem;
|
||||
pub use book::BookConfig;
|
||||
pub use renderer::Renderer;
|
||||
|
||||
/// The error types used through out this crate.
|
||||
pub mod errors {
|
||||
error_chain!{
|
||||
foreign_links {
|
||||
Io(::std::io::Error);
|
||||
HandlebarsRender(::handlebars::RenderError);
|
||||
HandlebarsTemplate(Box<::handlebars::TemplateError>);
|
||||
Utf8(::std::string::FromUtf8Error);
|
||||
}
|
||||
|
||||
errors {
|
||||
Subprocess(message: String, output: ::std::process::Output) {
|
||||
description("A subprocess failed")
|
||||
display("{}: {}", message, String::from_utf8_lossy(&output.stdout))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Box to halve the size of Error
|
||||
impl From<::handlebars::TemplateError> for Error {
|
||||
fn from(e: ::handlebars::TemplateError) -> Error {
|
||||
From::from(Box::new(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
use std::path::PathBuf;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Result, Error, ErrorKind};
|
||||
use std::io::{Error, ErrorKind, Read, Result};
|
||||
use book::bookitem::{BookItem, Chapter};
|
||||
|
||||
pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> {
|
||||
debug!("[fn]: construct_bookitems");
|
||||
let mut summary = String::new();
|
||||
try!(try!(File::open(path)).read_to_string(&mut summary));
|
||||
File::open(path)?.read_to_string(&mut summary)?;
|
||||
|
||||
debug!("[*]: Parse SUMMARY.md");
|
||||
let top_items = try!(parse_level(&mut summary.split('\n').collect(), 0, vec![0]));
|
||||
let top_items = parse_level(&mut summary.split('\n').collect(), 0, vec![0])?;
|
||||
debug!("[*]: Done parsing SUMMARY.md");
|
||||
Ok(top_items)
|
||||
}
|
||||
|
||||
fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32>) -> Result<Vec<BookItem>> {
|
||||
fn parse_level(summary: &mut Vec<&str>,
|
||||
current_level: i32,
|
||||
mut section: Vec<i32>)
|
||||
-> Result<Vec<BookItem>> {
|
||||
debug!("[fn]: parse_level");
|
||||
let mut items: Vec<BookItem> = vec![];
|
||||
|
||||
@@ -22,9 +25,10 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
while !summary.is_empty() {
|
||||
let item: BookItem;
|
||||
// Indentation level of the line to parse
|
||||
let level = try!(level(summary[0], 4));
|
||||
let level = level(summary[0], 4)?;
|
||||
|
||||
// if level < current_level we remove the last digit of section, exit the current function,
|
||||
// if level < current_level we remove the last digit of section,
|
||||
// exit the current function,
|
||||
// and return the parsed level to the calling function.
|
||||
if level < current_level {
|
||||
break;
|
||||
@@ -35,62 +39,70 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
// Level can not be root level !!
|
||||
// Add a sub-number to section
|
||||
section.push(0);
|
||||
let last = items.pop().expect("There should be at least one item since this can't be the root level");
|
||||
let last = items.pop().expect(
|
||||
"There should be at least one item since this can't be the root level",
|
||||
);
|
||||
|
||||
if let BookItem::Chapter(ref s, ref ch) = last {
|
||||
let mut ch = ch.clone();
|
||||
ch.sub_items = try!(parse_level(summary, level, section.clone()));
|
||||
ch.sub_items = parse_level(summary, level, section.clone())?;
|
||||
items.push(BookItem::Chapter(s.clone(), ch));
|
||||
|
||||
// Remove the last number from the section, because we got back to our level..
|
||||
section.pop();
|
||||
continue;
|
||||
} else {
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
Prefix, \
|
||||
Suffix and Spacer elements can only exist on the root level.\n
|
||||
Suffix and Spacer elements can only exist \
|
||||
on the root level.\n
|
||||
\
|
||||
Prefix elements can only exist before any chapter and there can be \
|
||||
no chapters after suffix elements."));
|
||||
Prefix elements can only exist before \
|
||||
any chapter and there can be \
|
||||
no chapters after suffix elements.",
|
||||
));
|
||||
};
|
||||
|
||||
} else {
|
||||
// level and current_level are the same, parse the line
|
||||
item = if let Some(parsed_item) = parse_line(summary[0]) {
|
||||
|
||||
// Eliminate possible errors and set section to -1 after suffix
|
||||
match parsed_item {
|
||||
// error if level != 0 and BookItem is != Chapter
|
||||
BookItem::Affix(_) | BookItem::Spacer if level > 0 => {
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
\
|
||||
Prefix, Suffix and Spacer elements can only exist on the \
|
||||
root level.\n
|
||||
Prefix, Suffix and Spacer elements \
|
||||
can only exist on the root level.\n
|
||||
Prefix \
|
||||
elements can only exist before any chapter and there can be \
|
||||
no chapters after suffix elements."))
|
||||
},
|
||||
elements can only exist before any chapter and \
|
||||
there can be no chapters after suffix elements.",
|
||||
))
|
||||
}
|
||||
|
||||
// error if BookItem == Chapter and section == -1
|
||||
BookItem::Chapter(_, _) if section[0] == -1 => {
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
\
|
||||
Prefix, Suffix and Spacer elements can only exist on the \
|
||||
root level.\n
|
||||
Prefix, Suffix and Spacer elements can only \
|
||||
exist on the root level.\n
|
||||
Prefix \
|
||||
elements can only exist before any chapter and there can be \
|
||||
no chapters after suffix elements."))
|
||||
},
|
||||
elements can only exist before any chapter and \
|
||||
there can be no chapters after suffix elements.",
|
||||
))
|
||||
}
|
||||
|
||||
// Set section = -1 after suffix
|
||||
BookItem::Affix(_) if section[0] > 0 => {
|
||||
section[0] = -1;
|
||||
},
|
||||
}
|
||||
|
||||
_ => {},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match parsed_item {
|
||||
@@ -98,12 +110,12 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
// Increment section
|
||||
let len = section.len() - 1;
|
||||
section[len] += 1;
|
||||
let s = section.iter().fold("".to_owned(), |s, i| s + &i.to_string() + ".");
|
||||
let s = section.iter()
|
||||
.fold("".to_owned(), |s, i| s + &i.to_string() + ".");
|
||||
BookItem::Chapter(s, ch)
|
||||
},
|
||||
}
|
||||
_ => parsed_item,
|
||||
}
|
||||
|
||||
} else {
|
||||
// If parse_line does not return Some(_) continue...
|
||||
summary.remove(0);
|
||||
@@ -140,8 +152,10 @@ fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
|
||||
if spaces > 0 {
|
||||
debug!("[SUMMARY.md]:");
|
||||
debug!("\t[line]: {}", line);
|
||||
debug!("[*]: There is an indentation error on this line. Indentation should be {} spaces", spaces_in_tab);
|
||||
return Err(Error::new(ErrorKind::Other, format!("Indentation error on line:\n\n{}", line)));
|
||||
debug!("[*]: There is an indentation error on this line. Indentation should be {} spaces",
|
||||
spaces_in_tab);
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
format!("Indentation error on line:\n\n{}", line)));
|
||||
}
|
||||
|
||||
Ok(level)
|
||||
@@ -171,7 +185,7 @@ fn parse_line(l: &str) -> Option<BookItem> {
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
}
|
||||
// Non-list element
|
||||
'[' => {
|
||||
debug!("[*]: Line is a link element");
|
||||
@@ -181,8 +195,8 @@ fn parse_line(l: &str) -> Option<BookItem> {
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
253
src/preprocess/links.rs
Normal file
253
src/preprocess/links.rs
Normal file
@@ -0,0 +1,253 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use regex::{CaptureMatches, Captures, Regex};
|
||||
use utils::fs::file_to_string;
|
||||
use errors::*;
|
||||
|
||||
const ESCAPE_CHAR: char = '\\';
|
||||
|
||||
pub fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> Result<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 mut previous_end_index = 0;
|
||||
let mut replaced = String::new();
|
||||
|
||||
for playpen in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..playpen.start_index]);
|
||||
replaced.push_str(&playpen.render_with_path(&path)?);
|
||||
previous_end_index = playpen.end_index;
|
||||
}
|
||||
|
||||
replaced.push_str(&s[previous_end_index..]);
|
||||
Ok(replaced)
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, PartialEq, Debug, Clone)]
|
||||
enum LinkType<'a> {
|
||||
Escaped,
|
||||
Include(PathBuf),
|
||||
Playpen(PathBuf, Vec<&'a str>),
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, PartialEq, Debug, Clone)]
|
||||
struct Link<'a> {
|
||||
start_index: usize,
|
||||
end_index: usize,
|
||||
link: LinkType<'a>,
|
||||
link_text: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Link<'a> {
|
||||
fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
|
||||
let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
|
||||
(_, Some(typ), Some(rest)) => {
|
||||
let mut path_props = rest.as_str().split_whitespace();
|
||||
let file_path = path_props.next().map(PathBuf::from);
|
||||
let props: Vec<&str> = path_props.collect();
|
||||
|
||||
match (typ.as_str(), file_path) {
|
||||
("include", Some(pth)) => Some(LinkType::Include(pth)),
|
||||
("playpen", Some(pth)) => Some(LinkType::Playpen(pth, props)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
(Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => {
|
||||
Some(LinkType::Escaped)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
link_type.and_then(|lnk| {
|
||||
cap.get(0).map(|mat| {
|
||||
Link {
|
||||
start_index: mat.start(),
|
||||
end_index: mat.end(),
|
||||
link: lnk,
|
||||
link_text: mat.as_str(),
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
|
||||
let base = base.as_ref();
|
||||
match self.link {
|
||||
// omit the escape char
|
||||
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
||||
LinkType::Include(ref pat) => {
|
||||
file_to_string(base.join(pat)).chain_err(|| {
|
||||
format!("Could not read file for \
|
||||
link {}",
|
||||
self.link_text)
|
||||
})
|
||||
}
|
||||
LinkType::Playpen(ref pat, ref attrs) => {
|
||||
let contents = file_to_string(base.join(pat)).chain_err(|| {
|
||||
format!("Could not \
|
||||
read file \
|
||||
for link {}",
|
||||
self.link_text)
|
||||
})?;
|
||||
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
||||
Ok(format!("```{}{}\n{}\n```\n", ftype, attrs.join(","), contents))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LinkIter<'a>(CaptureMatches<'a, 'a>);
|
||||
|
||||
impl<'a> Iterator for LinkIter<'a> {
|
||||
type Item = Link<'a>;
|
||||
fn next(&mut self) -> Option<Link<'a>> {
|
||||
for cap in &mut self.0 {
|
||||
if let Some(inc) = Link::from_capture(cap) {
|
||||
return Some(inc);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
LinkIter(RE.captures_iter(contents))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//
|
||||
|
||||
#[test]
|
||||
fn test_find_links_no_link() {
|
||||
let s = "Some random text without link...";
|
||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_partial_link() {
|
||||
let s = "Some random text with {{#playpen...";
|
||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||
let s = "Some random text with {{#include...";
|
||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||
let s = "Some random text with \\{{#include...";
|
||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_empty_link() {
|
||||
let s = "Some random text with {{#playpen}} and {{#playpen }} {{}} {{#}}...";
|
||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_unknown_link_type() {
|
||||
let s = "Some random text with {{#playpenz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
|
||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_simple_link() {
|
||||
let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
|
||||
assert_eq!(res,
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 42,
|
||||
link: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
|
||||
link_text: "{{#playpen file.rs}}",
|
||||
},
|
||||
Link {
|
||||
start_index: 47,
|
||||
end_index: 68,
|
||||
link: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
|
||||
link_text: "{{#playpen test.rs }}",
|
||||
}]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_escaped_link() {
|
||||
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
|
||||
assert_eq!(res,
|
||||
vec![Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: LinkType::Escaped,
|
||||
link_text: "\\{{#playpen file.rs editable}}",
|
||||
}]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_with_properties() {
|
||||
let s = "Some random text with escaped playpen {{#playpen file.rs editable }} and some more\n \
|
||||
text {{#playpen my.rs editable no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(res,
|
||||
vec![Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
|
||||
link_text: "{{#playpen file.rs editable }}",
|
||||
},
|
||||
Link {
|
||||
start_index: 89,
|
||||
end_index: 136,
|
||||
link: LinkType::Playpen(PathBuf::from("my.rs"),
|
||||
vec!["editable", "no_run", "should_panic"]),
|
||||
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
||||
}]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_all_link_types() {
|
||||
let s = "Some random text with escaped playpen {{#include file.rs}} and \\{{#contents are \
|
||||
insignifficant in escaped link}} some more\n text {{#playpen my.rs editable no_run \
|
||||
should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(res.len(), 3);
|
||||
assert_eq!(res[0],
|
||||
Link {
|
||||
start_index: 38,
|
||||
end_index: 58,
|
||||
link: LinkType::Include(PathBuf::from("file.rs")),
|
||||
link_text: "{{#include file.rs}}",
|
||||
});
|
||||
assert_eq!(res[1],
|
||||
Link {
|
||||
start_index: 63,
|
||||
end_index: 112,
|
||||
link: LinkType::Escaped,
|
||||
link_text: "\\{{#contents are insignifficant in escaped link}}",
|
||||
});
|
||||
assert_eq!(res[2],
|
||||
Link {
|
||||
start_index: 130,
|
||||
end_index: 177,
|
||||
link: LinkType::Playpen(PathBuf::from("my.rs"),
|
||||
vec!["editable", "no_run", "should_panic"]),
|
||||
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
||||
});
|
||||
}
|
||||
1
src/preprocess/mod.rs
Normal file
1
src/preprocess/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod links;
|
||||
@@ -1,14 +1,17 @@
|
||||
use renderer::html_handlebars::helpers;
|
||||
use preprocess;
|
||||
use renderer::Renderer;
|
||||
use book::MDBook;
|
||||
use book::bookitem::BookItem;
|
||||
use book::bookitem::{BookItem, Chapter};
|
||||
use config::{Config, Playpen, HtmlConfig};
|
||||
use {utils, theme};
|
||||
use regex::{Regex, Captures};
|
||||
use theme::{Theme, playpen_editor};
|
||||
use errors::*;
|
||||
use regex::{Captures, Regex};
|
||||
|
||||
use std::ascii::AsciiExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{self, File};
|
||||
use std::error::Error;
|
||||
use std::io::{self, Read};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
@@ -17,7 +20,6 @@ use handlebars::Handlebars;
|
||||
|
||||
use serde_json;
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HtmlHandlebars;
|
||||
|
||||
@@ -25,174 +27,369 @@ impl HtmlHandlebars {
|
||||
pub fn new() -> Self {
|
||||
HtmlHandlebars
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for HtmlHandlebars {
|
||||
fn render(&self, book: &MDBook) -> Result<(), Box<Error>> {
|
||||
debug!("[fn]: render");
|
||||
let mut handlebars = Handlebars::new();
|
||||
fn render_item(&self,
|
||||
item: &BookItem,
|
||||
mut ctx: RenderItemContext,
|
||||
print_content: &mut String)
|
||||
-> Result<()> {
|
||||
// FIXME: This should be made DRY-er and rely less on mutable state
|
||||
match *item {
|
||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch)
|
||||
if !ch.path.as_os_str().is_empty() =>
|
||||
{
|
||||
let path = ctx.book.get_source().join(&ch.path);
|
||||
let content = utils::fs::file_to_string(&path)?;
|
||||
let base = path.parent()
|
||||
.ok_or_else(|| String::from("Invalid bookitem path!"))?;
|
||||
|
||||
// Load theme
|
||||
let theme = theme::Theme::new(book.get_theme_path());
|
||||
// Parse and expand links
|
||||
let content = preprocess::links::replace_all(&content, base)?;
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
print_content.push_str(&content);
|
||||
|
||||
// Register template
|
||||
debug!("[*]: Register handlebars template");
|
||||
try!(handlebars.register_template_string("index", try!(String::from_utf8(theme.index))));
|
||||
// Update the context with data for this file
|
||||
let path = ch.path.to_str().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::Other,
|
||||
"Could not convert path \
|
||||
to str")
|
||||
})?;
|
||||
|
||||
// Register helpers
|
||||
debug!("[*]: Register handlebars helpers");
|
||||
// Non-lexical lifetimes needed :'(
|
||||
let title: String;
|
||||
{
|
||||
let book_title = ctx.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
title = ch.name.clone() + " - " + book_title;
|
||||
}
|
||||
|
||||
ctx.data.insert("path".to_owned(), json!(path));
|
||||
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)));
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("[*]: Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
|
||||
let filepath = Path::new(&ch.path).with_extension("html");
|
||||
let rendered = self.post_process(
|
||||
rendered,
|
||||
&normalize_path(filepath.to_str().ok_or_else(|| Error::from(
|
||||
format!("Bad file name: {}", filepath.display()),
|
||||
))?),
|
||||
&ctx.book.config.html_config().unwrap_or_default().playpen,
|
||||
);
|
||||
|
||||
// Write to file
|
||||
info!("[*] Creating {:?} ✓", filepath.display());
|
||||
ctx.book.write_file(filepath, &rendered.into_bytes())?;
|
||||
|
||||
if ctx.is_index {
|
||||
self.render_index(ctx.book, ch, &ctx.destination)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an index.html from the first element in SUMMARY.md
|
||||
fn render_index(&self, book: &MDBook, ch: &Chapter, destination: &Path) -> Result<()> {
|
||||
debug!("[*]: index.html");
|
||||
|
||||
let mut content = String::new();
|
||||
|
||||
File::open(destination.join(&ch.path.with_extension("html")))?
|
||||
.read_to_string(&mut content)?;
|
||||
|
||||
// This could cause a problem when someone displays
|
||||
// code containing <base href=...>
|
||||
// on the front page, however this case should be very very rare...
|
||||
content = content.lines()
|
||||
.filter(|line| !line.contains("<base href="))
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
book.write_file("index.html", content.as_bytes())?;
|
||||
|
||||
info!("[*] Creating index.html from {:?} ✓",
|
||||
book.get_destination().join(&ch.path.with_extension("html")));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
|
||||
fn post_process(&self,
|
||||
rendered: String,
|
||||
filepath: &str,
|
||||
playpen_config: &Playpen)
|
||||
-> String {
|
||||
let rendered = build_header_links(&rendered, filepath);
|
||||
let rendered = fix_anchor_links(&rendered, filepath);
|
||||
let rendered = fix_code_blocks(&rendered);
|
||||
let rendered = add_playpen_pre(&rendered, playpen_config);
|
||||
|
||||
rendered
|
||||
}
|
||||
|
||||
fn copy_static_files(&self, book: &MDBook, theme: &Theme, html_config: &HtmlConfig) -> Result<()> {
|
||||
book.write_file("book.js", &theme.js)?;
|
||||
book.write_file("book.css", &theme.css)?;
|
||||
book.write_file("favicon.png", &theme.favicon)?;
|
||||
book.write_file("jquery.js", &theme.jquery)?;
|
||||
book.write_file("highlight.css", &theme.highlight_css)?;
|
||||
book.write_file("tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||
book.write_file("ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||
book.write_file("highlight.js", &theme.highlight_js)?;
|
||||
book.write_file("clipboard.min.js", &theme.clipboard_js)?;
|
||||
book.write_file("store.js", &theme.store_js)?;
|
||||
book.write_file("_FontAwesome/css/font-awesome.css", theme::FONT_AWESOME)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2)?;
|
||||
book.write_file("_FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF)?;
|
||||
|
||||
let playpen_config = &html_config.playpen;
|
||||
|
||||
// Ace is a very large dependency, so only load it when requested
|
||||
if playpen_config.editable {
|
||||
// Load the editor
|
||||
let editor = playpen_editor::PlaypenEditor::new(&playpen_config.editor);
|
||||
book.write_file("editor.js", &editor.js)?;
|
||||
book.write_file("ace.js", &editor.ace_js)?;
|
||||
book.write_file("mode-rust.js", &editor.mode_rust_js)?;
|
||||
book.write_file("theme-dawn.js", &editor.theme_dawn_js)?;
|
||||
book.write_file("theme-tomorrow_night.js", &editor.theme_tomorrow_night_js)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to write a file to the build directory, normalizing
|
||||
/// the path to be relative to the book root.
|
||||
fn write_custom_file(&self, custom_file: &Path, book: &MDBook) -> Result<()> {
|
||||
let mut data = Vec::new();
|
||||
let mut f = File::open(custom_file)?;
|
||||
f.read_to_end(&mut data)?;
|
||||
|
||||
let name = match custom_file.strip_prefix(&book.root) {
|
||||
Ok(p) => p.to_str().expect("Could not convert to str"),
|
||||
Err(_) => {
|
||||
custom_file.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str")
|
||||
}
|
||||
};
|
||||
|
||||
book.write_file(name, &data)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
// 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"))));
|
||||
}
|
||||
|
||||
fn register_hbs_helpers(&self, handlebars: &mut Handlebars) {
|
||||
handlebars.register_helper("toc", Box::new(helpers::toc::RenderToc));
|
||||
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
|
||||
handlebars.register_helper("next", Box::new(helpers::navigation::next));
|
||||
}
|
||||
|
||||
let mut data = try!(make_data(book));
|
||||
/// Copy across any additional CSS and JavaScript files which the book
|
||||
/// has been configured to use.
|
||||
fn copy_additional_css_and_js(&self, book: &MDBook) -> Result<()> {
|
||||
let html = book.config.html_config().unwrap_or_default();
|
||||
|
||||
// Print version
|
||||
let mut print_content: String = String::new();
|
||||
let custom_files = html.additional_css
|
||||
.iter()
|
||||
.chain(html.additional_js.iter());
|
||||
|
||||
// Check if dest directory exists
|
||||
debug!("[*]: Check if destination directory exists");
|
||||
if fs::create_dir_all(book.get_dest()).is_err() {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other,
|
||||
"Unexpected error when constructing destination path")));
|
||||
for custom_file in custom_files {
|
||||
self.write_custom_file(custom_file, book)?;
|
||||
}
|
||||
|
||||
// Render a file for every entry in the book
|
||||
let mut index = true;
|
||||
for item in book.iter() {
|
||||
|
||||
match *item {
|
||||
BookItem::Chapter(_, ref ch) |
|
||||
BookItem::Affix(ref ch) => {
|
||||
if ch.path != PathBuf::new() {
|
||||
|
||||
let path = book.get_src().join(&ch.path);
|
||||
|
||||
debug!("[*]: Opening file: {:?}", path);
|
||||
let mut f = try!(File::open(&path));
|
||||
let mut content: String = String::new();
|
||||
|
||||
debug!("[*]: Reading file");
|
||||
try!(f.read_to_string(&mut content));
|
||||
|
||||
// Parse for playpen links
|
||||
if let Some(p) = path.parent() {
|
||||
content = helpers::playpen::render_playpen(&content, p);
|
||||
}
|
||||
|
||||
// Render markdown using the pulldown-cmark crate
|
||||
content = utils::render_markdown(&content);
|
||||
print_content.push_str(&content);
|
||||
|
||||
// Update the context with data for this file
|
||||
let path = ch.path.to_str().ok_or_else(||
|
||||
io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))?;
|
||||
data.insert("path".to_owned(), json!(path));
|
||||
data.insert("content".to_owned(), json!(content));
|
||||
data.insert("chapter_title".to_owned(), json!(ch.name));
|
||||
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 = try!(handlebars.render("index", &data));
|
||||
|
||||
let filename = Path::new(&ch.path).with_extension("html");
|
||||
|
||||
// Do several kinds of post-processing
|
||||
let rendered = build_header_links(rendered, filename.to_str().unwrap_or(""));
|
||||
let rendered = fix_anchor_links(rendered, filename.to_str().unwrap_or(""));
|
||||
let rendered = fix_code_blocks(rendered);
|
||||
let rendered = add_playpen_pre(rendered);
|
||||
|
||||
// Write to file
|
||||
info!("[*] Creating {:?} ✓", filename.display());
|
||||
try!(book.write_file(filename, &rendered.into_bytes()));
|
||||
|
||||
// Create an index.html from the first element in SUMMARY.md
|
||||
if index {
|
||||
debug!("[*]: index.html");
|
||||
|
||||
let mut content = String::new();
|
||||
let _source = try!(File::open(book.get_dest().join(&ch.path.with_extension("html"))))
|
||||
.read_to_string(&mut content);
|
||||
|
||||
// This could cause a problem when someone displays code containing <base href=...>
|
||||
// on the front page, however this case should be very very rare...
|
||||
content = content.lines()
|
||||
.filter(|line| !line.contains("<base href="))
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
try!(book.write_file("index.html", content.as_bytes()));
|
||||
|
||||
info!("[*] Creating index.html from {:?} ✓",
|
||||
book.get_dest().join(&ch.path.with_extension("html")));
|
||||
index = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Print version
|
||||
|
||||
// Update the context with data for this file
|
||||
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"))));
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("[*]: Render template");
|
||||
|
||||
let rendered = try!(handlebars.render("index", &data));
|
||||
|
||||
// do several kinds of post-processing
|
||||
let rendered = build_header_links(rendered, "print.html");
|
||||
let rendered = fix_anchor_links(rendered, "print.html");
|
||||
let rendered = fix_code_blocks(rendered);
|
||||
let rendered = add_playpen_pre(rendered);
|
||||
|
||||
try!(book.write_file(Path::new("print").with_extension("html"), &rendered.into_bytes()));
|
||||
info!("[*] Creating print.html ✓");
|
||||
|
||||
// Copy static files (js, css, images, ...)
|
||||
|
||||
debug!("[*] Copy static files");
|
||||
try!(book.write_file("book.js", &theme.js));
|
||||
try!(book.write_file("book.css", &theme.css));
|
||||
try!(book.write_file("favicon.png", &theme.favicon));
|
||||
try!(book.write_file("jquery.js", &theme.jquery));
|
||||
try!(book.write_file("highlight.css", &theme.highlight_css));
|
||||
try!(book.write_file("tomorrow-night.css", &theme.tomorrow_night_css));
|
||||
try!(book.write_file("highlight.js", &theme.highlight_js));
|
||||
try!(book.write_file("_FontAwesome/css/font-awesome.css", theme::FONT_AWESOME));
|
||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.eot", theme::FONT_AWESOME_EOT));
|
||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.svg", theme::FONT_AWESOME_SVG));
|
||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.ttf", theme::FONT_AWESOME_TTF));
|
||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff", theme::FONT_AWESOME_WOFF));
|
||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2", theme::FONT_AWESOME_WOFF2));
|
||||
try!(book.write_file("_FontAwesome/fonts/FontAwesome.ttf", theme::FONT_AWESOME_TTF));
|
||||
|
||||
// Copy all remaining files
|
||||
try!(utils::fs::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"]));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>, Box<Error>> {
|
||||
|
||||
impl Renderer for HtmlHandlebars {
|
||||
fn render(&self, book: &MDBook) -> Result<()> {
|
||||
let html_config = book.config.html_config().unwrap_or_default();
|
||||
|
||||
debug!("[fn]: render");
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
let theme_dir = match html_config.theme {
|
||||
Some(ref theme) => theme,
|
||||
None => Path::new("theme"),
|
||||
};
|
||||
|
||||
let theme = theme::Theme::new(theme_dir);
|
||||
|
||||
debug!("[*]: Register the index handlebars template");
|
||||
handlebars.register_template_string(
|
||||
"index",
|
||||
String::from_utf8(theme.index.clone())?,
|
||||
)?;
|
||||
|
||||
debug!("[*]: Register the header handlebars template");
|
||||
handlebars.register_partial(
|
||||
"header",
|
||||
String::from_utf8(theme.header.clone())?,
|
||||
)?;
|
||||
|
||||
debug!("[*]: Register handlebars helpers");
|
||||
self.register_hbs_helpers(&mut handlebars);
|
||||
|
||||
let mut data = make_data(book, &book.config)?;
|
||||
|
||||
// Print version
|
||||
let mut print_content = String::new();
|
||||
|
||||
// TODO: The Renderer trait should really pass in where it wants us to build to...
|
||||
let destination = book.get_destination();
|
||||
|
||||
debug!("[*]: Check if destination directory exists");
|
||||
fs::create_dir_all(&destination)
|
||||
.chain_err(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
for (i, item) in book.iter().enumerate() {
|
||||
let ctx = RenderItemContext {
|
||||
book: book,
|
||||
handlebars: &handlebars,
|
||||
destination: destination.to_path_buf(),
|
||||
data: data.clone(),
|
||||
is_index: i == 0,
|
||||
html_config: html_config.clone(),
|
||||
};
|
||||
self.render_item(item, ctx, &mut print_content)?;
|
||||
}
|
||||
|
||||
// Print version
|
||||
self.configure_print_version(&mut data, &print_content);
|
||||
if let Some(ref title) = book.config.book.title {
|
||||
data.insert("title".to_owned(), json!(title));
|
||||
}
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("[*]: Render template");
|
||||
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
|
||||
let rendered = self.post_process(rendered,
|
||||
"print.html",
|
||||
&html_config.playpen);
|
||||
|
||||
book.write_file(Path::new("print").with_extension("html"),
|
||||
&rendered.into_bytes())?;
|
||||
info!("[*] Creating print.html ✓");
|
||||
|
||||
// Copy static files (js, css, images, ...)
|
||||
debug!("[*] Copy static files");
|
||||
self.copy_static_files(book, &theme, &html_config)?;
|
||||
self.copy_additional_css_and_js(book)?;
|
||||
|
||||
// Copy all remaining files
|
||||
let src = book.get_source();
|
||||
utils::fs::copy_files_except_ext(&src, &destination, true, &["md"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_data(book: &MDBook, config: &Config) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
debug!("[fn]: 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("title".to_owned(), json!(book.get_title()));
|
||||
data.insert("description".to_owned(), json!(book.get_description()));
|
||||
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(livereload) = book.get_livereload() {
|
||||
if let Some(ref livereload) = book.livereload {
|
||||
data.insert("livereload".to_owned(), json!(livereload));
|
||||
}
|
||||
|
||||
// Add google analytics tag
|
||||
if let Some(ref ga) = config.html_config().and_then(|html| html.google_analytics) {
|
||||
data.insert("google_analytics".to_owned(), json!(ga));
|
||||
}
|
||||
|
||||
if html.mathjax_support {
|
||||
data.insert("mathjax_support".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional style
|
||||
if !html.additional_css.is_empty() {
|
||||
let mut css = Vec::new();
|
||||
for style in &html.additional_css {
|
||||
match style.strip_prefix(&book.root) {
|
||||
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => {
|
||||
css.push(style.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str"))
|
||||
}
|
||||
}
|
||||
}
|
||||
data.insert("additional_css".to_owned(), json!(css));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional script
|
||||
if !html.additional_js.is_empty() {
|
||||
let mut js = Vec::new();
|
||||
for script in &html.additional_js {
|
||||
match script.strip_prefix(&book.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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
data.insert("additional_js".to_owned(), json!(js));
|
||||
}
|
||||
|
||||
if html.playpen.editable {
|
||||
data.insert("playpens_editable".to_owned(), json!(true));
|
||||
data.insert("editor_js".to_owned(), json!("editor.js"));
|
||||
data.insert("ace_js".to_owned(), json!("ace.js"));
|
||||
data.insert("mode_rust_js".to_owned(), json!("mode-rust.js"));
|
||||
data.insert("theme_dawn_js".to_owned(), json!("theme-dawn.js"));
|
||||
data.insert("theme_tomorrow_night_js".to_owned(),
|
||||
json!("theme-tomorrow_night.js"));
|
||||
}
|
||||
|
||||
let mut chapters = vec![];
|
||||
|
||||
for item in book.iter() {
|
||||
@@ -202,21 +399,26 @@ fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>
|
||||
match *item {
|
||||
BookItem::Affix(ref ch) => {
|
||||
chapter.insert("name".to_owned(), json!(ch.name));
|
||||
let path = ch.path.to_str().ok_or_else(||
|
||||
io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))?;
|
||||
let path = ch.path.to_str().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::Other,
|
||||
"Could not convert path \
|
||||
to str")
|
||||
})?;
|
||||
chapter.insert("path".to_owned(), json!(path));
|
||||
},
|
||||
}
|
||||
BookItem::Chapter(ref s, ref ch) => {
|
||||
chapter.insert("section".to_owned(), json!(s));
|
||||
chapter.insert("name".to_owned(), json!(ch.name));
|
||||
let path = ch.path.to_str().ok_or_else(||
|
||||
io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))?;
|
||||
let path = ch.path.to_str().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::Other,
|
||||
"Could not convert path \
|
||||
to str")
|
||||
})?;
|
||||
chapter.insert("path".to_owned(), json!(path));
|
||||
},
|
||||
}
|
||||
BookItem::Spacer => {
|
||||
chapter.insert("spacer".to_owned(), json!("_spacer_"));
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
chapters.push(chapter);
|
||||
@@ -228,107 +430,152 @@ fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn build_header_links(html: String, filename: &str) -> String {
|
||||
/// 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 {
|
||||
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];
|
||||
let text = &caps[2];
|
||||
let mut id = text.to_string();
|
||||
let repl_sub = vec!["<em>", "</em>", "<code>", "</code>",
|
||||
"<strong>", "</strong>",
|
||||
"<", ">", "&", "'", """];
|
||||
for sub in repl_sub {
|
||||
id = id.replace(sub, "");
|
||||
}
|
||||
let id = id.chars().filter_map(|c| {
|
||||
if c.is_alphanumeric() || c == '-' || c == '_' {
|
||||
if c.is_ascii() {
|
||||
Some(c.to_ascii_lowercase())
|
||||
} else {
|
||||
Some(c)
|
||||
}
|
||||
} else if c.is_whitespace() && c.is_ascii() {
|
||||
Some('-')
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect::<String>();
|
||||
regex.replace_all(html, |caps: &Captures| {
|
||||
let level = caps[1].parse()
|
||||
.expect("Regex should ensure we only ever get numbers here");
|
||||
|
||||
let id_count = *id_counter.get(&id).unwrap_or(&0);
|
||||
id_counter.insert(id.clone(), id_count + 1);
|
||||
wrap_header_with_link(level, &caps[2], &mut id_counter, filepath)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
let id = if id_count > 0 {
|
||||
format!("{}-{}", id, id_count)
|
||||
} else {
|
||||
id
|
||||
};
|
||||
/// Wraps a single header tag with a link, making sure each tag gets its own
|
||||
/// unique ID by appending an auto-incremented number (if necessary).
|
||||
fn wrap_header_with_link(level: usize,
|
||||
content: &str,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
filepath: &str)
|
||||
-> String {
|
||||
let raw_id = id_from_content(content);
|
||||
|
||||
format!("<a class=\"header\" href=\"{filename}#{id}\" id=\"{id}\"><h{level}>{text}</h{level}></a>",
|
||||
level=level, id=id, text=text, filename=filename)
|
||||
}).into_owned()
|
||||
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
|
||||
|
||||
let id = match *id_count {
|
||||
0 => raw_id,
|
||||
other => format!("{}-{}", raw_id, other),
|
||||
};
|
||||
|
||||
*id_count += 1;
|
||||
|
||||
format!(
|
||||
r##"<a class="header" href="{filepath}#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content,
|
||||
filepath = filepath
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate an id for use with anchors which is derived from a "normalised"
|
||||
/// string.
|
||||
fn id_from_content(content: &str) -> String {
|
||||
let mut content = content.to_string();
|
||||
|
||||
// Skip any tags or html-encoded stuff
|
||||
const REPL_SUB: &[&str] = &["<em>",
|
||||
"</em>",
|
||||
"<code>",
|
||||
"</code>",
|
||||
"<strong>",
|
||||
"</strong>",
|
||||
"<",
|
||||
">",
|
||||
"&",
|
||||
"'",
|
||||
"""];
|
||||
for sub in REPL_SUB {
|
||||
content = content.replace(sub, "");
|
||||
}
|
||||
|
||||
// Remove spaces and hastags indicating a header
|
||||
let trimmed = content.trim().trim_left_matches('#').trim();
|
||||
|
||||
normalize_id(trimmed)
|
||||
}
|
||||
|
||||
// anchors to the same page (href="#anchor") do not work because of
|
||||
// <base href="../"> pointing to the root folder. This function *fixes*
|
||||
// that in a very inelegant way
|
||||
fn fix_anchor_links(html: String, filename: &str) -> String {
|
||||
fn fix_anchor_links(html: &str, filepath: &str) -> String {
|
||||
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
|
||||
regex.replace_all(&html, |caps: &Captures| {
|
||||
regex.replace_all(html, |caps: &Captures| {
|
||||
let before = &caps[1];
|
||||
let anchor = &caps[2];
|
||||
let after = &caps[3];
|
||||
|
||||
format!("<a{before}href=\"{filename}#{anchor}\"{after}>",
|
||||
before=before, filename=filename, anchor=anchor, after=after)
|
||||
}).into_owned()
|
||||
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:
|
||||
// The rust book uses annotations for rustdoc to test code snippets,
|
||||
// like the following:
|
||||
// ```rust,should_panic
|
||||
// fn main() {
|
||||
// // Code here
|
||||
// }
|
||||
// ```
|
||||
// This function replaces all commas by spaces in the code block classes
|
||||
fn fix_code_blocks(html: String) -> String {
|
||||
fn fix_code_blocks(html: &str) -> String {
|
||||
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
||||
regex.replace_all(&html, |caps: &Captures| {
|
||||
regex.replace_all(html, |caps: &Captures| {
|
||||
let before = &caps[1];
|
||||
let classes = &caps[2].replace(",", " ");
|
||||
let after = &caps[3];
|
||||
|
||||
format!("<code{before}class=\"{classes}\"{after}>", before=before, classes=classes, after=after)
|
||||
}).into_owned()
|
||||
format!(r#"<code{before}class="{classes}"{after}>"#,
|
||||
before = before,
|
||||
classes = classes,
|
||||
after = after)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn add_playpen_pre(html: String) -> String {
|
||||
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| {
|
||||
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") {
|
||||
if (classes.contains("language-rust") && !classes.contains("ignore")) ||
|
||||
classes.contains("mdbook-runnable")
|
||||
{
|
||||
// wrap the contents in an external pre block
|
||||
|
||||
if text.contains("fn main") {
|
||||
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=\"{}\"># #![allow(unused_variables)]
|
||||
{}#fn main() {{
|
||||
{}
|
||||
#}}</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
|
||||
format!("{}", text)
|
||||
text.to_owned()
|
||||
}
|
||||
}).into_owned()
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn partition_source(s: &str) -> (String, String) {
|
||||
@@ -338,8 +585,7 @@ fn partition_source(s: &str) -> (String, String) {
|
||||
|
||||
for line in s.lines() {
|
||||
let trimline = line.trim();
|
||||
let header = trimline.chars().all(|c| c.is_whitespace()) ||
|
||||
trimline.starts_with("#![");
|
||||
let header = trimline.chars().all(|c| c.is_whitespace()) || trimline.starts_with("#![");
|
||||
if !header || after_header {
|
||||
after_header = true;
|
||||
after.push_str(line);
|
||||
@@ -351,4 +597,87 @@ fn partition_source(s: &str) -> (String, String) {
|
||||
}
|
||||
|
||||
(before, after)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct RenderItemContext<'a> {
|
||||
handlebars: &'a Handlebars,
|
||||
book: &'a MDBook,
|
||||
destination: PathBuf,
|
||||
data: serde_json::Map<String, serde_json::Value>,
|
||||
is_index: bool,
|
||||
html_config: HtmlConfig,
|
||||
}
|
||||
|
||||
pub fn normalize_path(path: &str) -> String {
|
||||
use std::path::is_separator;
|
||||
path.chars()
|
||||
.map(|ch| if is_separator(ch) { '/' } else { ch })
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
pub fn normalize_id(content: &str) -> String {
|
||||
content.chars()
|
||||
.filter_map(|ch| if ch.is_alphanumeric() || ch == '_' || ch == '-' {
|
||||
Some(ch.to_ascii_lowercase())
|
||||
} else if ch.is_whitespace() {
|
||||
Some('-')
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn original_build_header_links() {
|
||||
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>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1>",
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#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>"##,
|
||||
),
|
||||
(
|
||||
"<h4></h4>",
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#" 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>"##,
|
||||
),
|
||||
(
|
||||
"<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>"##,
|
||||
),
|
||||
];
|
||||
|
||||
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);
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchor_generation() {
|
||||
assert_eq!(id_from_content("## `--passes`: add more rustdoc passes"),
|
||||
"--passes-add-more-rustdoc-passes");
|
||||
assert_eq!(id_from_content("## Method-call expressions"),
|
||||
"method-call-expressions");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod navigation;
|
||||
pub mod toc;
|
||||
pub mod playpen;
|
||||
|
||||
@@ -1,200 +1,234 @@
|
||||
use std::path::Path;
|
||||
use std::collections::{VecDeque, BTreeMap};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Renderable};
|
||||
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
|
||||
|
||||
|
||||
// Handlebars helper for navigation
|
||||
type StringMap = BTreeMap<String, String>;
|
||||
|
||||
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: previous (handlebars helper)");
|
||||
|
||||
debug!("[*]: Get data from context");
|
||||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = rc.context().navigate(rc.get_path(), &VecDeque::new(), "chapters").to_owned();
|
||||
|
||||
let current = rc.context().navigate(rc.get_path(), &VecDeque::new(), "path")
|
||||
.to_string()
|
||||
.replace("\"", "");
|
||||
|
||||
|
||||
debug!("[*]: Decode chapters from JSON");
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String, String>> = match serde_json::from_str(&chapters.to_string()) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return Err(RenderError::new("Could not decode the JSON data")),
|
||||
};
|
||||
let mut previous: Option<BTreeMap<String, String>> = None;
|
||||
|
||||
|
||||
debug!("[*]: Search for current Chapter");
|
||||
// Search for current chapter and return previous entry
|
||||
for item in decoded {
|
||||
|
||||
match item.get("path") {
|
||||
Some(path) if !path.is_empty() => {
|
||||
if path == ¤t {
|
||||
|
||||
debug!("[*]: Found current chapter");
|
||||
if let Some(previous) = previous {
|
||||
|
||||
debug!("[*]: Creating BTreeMap to inject in context");
|
||||
// Create new BTreeMap to extend the context: 'title' and 'link'
|
||||
let mut previous_chapter = BTreeMap::new();
|
||||
|
||||
// Chapter title
|
||||
match previous.get("name") {
|
||||
Some(n) => {
|
||||
debug!("[*]: Inserting title: {}", n);
|
||||
previous_chapter.insert("title".to_owned(), json!(n))
|
||||
},
|
||||
None => {
|
||||
debug!("[*]: No title found for chapter");
|
||||
return Err(RenderError::new("No title found for chapter in JSON data"));
|
||||
},
|
||||
};
|
||||
|
||||
// Chapter link
|
||||
|
||||
match previous.get("path") {
|
||||
Some(p) => {
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
let path = Path::new(p).with_extension("html");
|
||||
debug!("[*]: Inserting link: {:?}", path);
|
||||
|
||||
match path.to_str() {
|
||||
Some(p) => {
|
||||
previous_chapter.insert("link".to_owned(), json!(p.replace("\\", "/")));
|
||||
},
|
||||
None => return Err(RenderError::new("Link could not be converted to str")),
|
||||
}
|
||||
},
|
||||
None => return Err(RenderError::new("No path found for chapter in JSON data")),
|
||||
}
|
||||
|
||||
debug!("[*]: Inject in context");
|
||||
// Inject in current context
|
||||
let updated_context = rc.context().extend(&previous_chapter);
|
||||
|
||||
debug!("[*]: Render template");
|
||||
// Render template
|
||||
match _h.template() {
|
||||
Some(t) => {
|
||||
*rc.context_mut() = updated_context;
|
||||
try!(t.render(r, rc));
|
||||
},
|
||||
None => return Err(RenderError::new("Error with the handlebars template")),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
break;
|
||||
} else {
|
||||
previous = Some(item.clone());
|
||||
}
|
||||
},
|
||||
_ => continue,
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Ok(())
|
||||
/// Target for `find_chapter`.
|
||||
enum Target {
|
||||
Previous,
|
||||
Next,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
/// Returns target if found.
|
||||
fn find(&self,
|
||||
base_path: &String,
|
||||
current_path: &String,
|
||||
current_item: &StringMap,
|
||||
previous_item: &StringMap,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
match self {
|
||||
&Target::Next => {
|
||||
let previous_path = previous_item.get("path").ok_or_else(|| {
|
||||
RenderError::new("No path found for chapter in JSON data")
|
||||
})?;
|
||||
|
||||
if previous_path == base_path {
|
||||
return Ok(Some(current_item.clone()));
|
||||
}
|
||||
},
|
||||
|
||||
&Target::Previous => {
|
||||
if current_path == base_path {
|
||||
return Ok(Some(previous_item.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: next (handlebars helper)");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_chapter(
|
||||
rc: &mut RenderContext,
|
||||
target: Target
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
debug!("[*]: Get data from context");
|
||||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = rc.context().navigate(rc.get_path(), &VecDeque::new(), "chapters").to_owned();
|
||||
|
||||
let current = rc.context().navigate(rc.get_path(), &VecDeque::new(), "path")
|
||||
.to_string()
|
||||
.replace("\"", "");
|
||||
let chapters = rc.evaluate_absolute("chapters").and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
|
||||
debug!("[*]: Decode chapters from JSON");
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String, String>> = match serde_json::from_str(&chapters.to_string()) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return Err(RenderError::new("Could not decode the JSON data")),
|
||||
};
|
||||
let mut previous: Option<BTreeMap<String, String>> = None;
|
||||
let base_path = rc.evaluate_absolute("path")?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
debug!("[*]: Search for current Chapter");
|
||||
// Search for current chapter and return previous entry
|
||||
for item in decoded {
|
||||
let mut previous: Option<StringMap> = None;
|
||||
|
||||
debug!("[*]: Search for chapter");
|
||||
|
||||
for item in chapters {
|
||||
match item.get("path") {
|
||||
|
||||
Some(path) if !path.is_empty() => {
|
||||
|
||||
if let Some(previous) = previous {
|
||||
|
||||
let previous_path = match previous.get("path") {
|
||||
Some(p) => p,
|
||||
None => return Err(RenderError::new("No path found for chapter in JSON data")),
|
||||
};
|
||||
|
||||
if previous_path == ¤t {
|
||||
|
||||
debug!("[*]: Found current chapter");
|
||||
debug!("[*]: Creating BTreeMap to inject in context");
|
||||
// Create new BTreeMap to extend the context: 'title' and 'link'
|
||||
let mut next_chapter = BTreeMap::new();
|
||||
|
||||
match item.get("name") {
|
||||
Some(n) => {
|
||||
debug!("[*]: Inserting title: {}", n);
|
||||
next_chapter.insert("title".to_owned(), json!(n));
|
||||
},
|
||||
None => return Err(RenderError::new("No title found for chapter in JSON data")),
|
||||
}
|
||||
|
||||
|
||||
let link = Path::new(path).with_extension("html");
|
||||
debug!("[*]: Inserting link: {:?}", link);
|
||||
|
||||
match link.to_str() {
|
||||
Some(l) => {
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
next_chapter.insert("link".to_owned(), json!(l.replace("\\", "/")));
|
||||
},
|
||||
None => return Err(RenderError::new("Link could not converted to str")),
|
||||
}
|
||||
|
||||
debug!("[*]: Inject in context");
|
||||
// Inject in current context
|
||||
let updated_context = rc.context().extend(&next_chapter);
|
||||
|
||||
debug!("[*]: Render template");
|
||||
|
||||
// Render template
|
||||
match _h.template() {
|
||||
Some(t) => {
|
||||
*rc.context_mut() = updated_context;
|
||||
try!(t.render(r, rc));
|
||||
},
|
||||
None => return Err(RenderError::new("Error with the handlebars template")),
|
||||
}
|
||||
|
||||
break;
|
||||
if let Some(item) = target.find(&base_path, &path, &item, &previous)? {
|
||||
return Ok(Some(item));
|
||||
}
|
||||
}
|
||||
|
||||
previous = Some(item.clone());
|
||||
},
|
||||
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn render(
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
rc: &mut RenderContext,
|
||||
chapter: &StringMap,
|
||||
) -> Result<(), RenderError> {
|
||||
debug!("[*]: Creating BTreeMap to inject in context");
|
||||
|
||||
let mut context = BTreeMap::new();
|
||||
|
||||
chapter.get("name")
|
||||
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
|
||||
.map(|name| context.insert("title".to_owned(), json!(name)))?;
|
||||
|
||||
chapter.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
|
||||
.and_then(|p| {
|
||||
Path::new(p).with_extension("html")
|
||||
.to_str()
|
||||
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
|
||||
.map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/"))))
|
||||
})?;
|
||||
|
||||
debug!("[*]: Render template");
|
||||
|
||||
_h.template()
|
||||
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
||||
.and_then(|t| {
|
||||
let mut local_rc = rc.with_context(Context::wraps(&context)?);
|
||||
t.render(r, &mut local_rc)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: previous (handlebars helper)");
|
||||
|
||||
if let Some(previous) = find_chapter(rc, Target::Previous)? {
|
||||
render(_h, r, rc, &previous)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: next (handlebars helper)");
|
||||
|
||||
if let Some(next) = find_chapter(rc, Target::Next)? {
|
||||
render(_h, r, rc, &next)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
static TEMPLATE: &'static str =
|
||||
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
|
||||
|
||||
#[test]
|
||||
fn test_next_previous() {
|
||||
let data = json!({
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
|
||||
assert_eq!(
|
||||
h.template_render(TEMPLATE, &data).unwrap(),
|
||||
"one: one.html|three: three.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first() {
|
||||
let data = json!({
|
||||
"name": "one",
|
||||
"path": "one.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
|
||||
assert_eq!(
|
||||
h.template_render(TEMPLATE, &data).unwrap(),
|
||||
"|two: two.html");
|
||||
}
|
||||
#[test]
|
||||
fn test_last() {
|
||||
let data = json!({
|
||||
"name": "three",
|
||||
"path": "three.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
|
||||
assert_eq!(
|
||||
h.template_render(TEMPLATE, &data).unwrap(),
|
||||
"two: two.html|");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
|
||||
pub fn render_playpen(s: &str, path: &Path) -> 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 mut previous_end_index = 0;
|
||||
let mut replaced = String::new();
|
||||
|
||||
for playpen in find_playpens(s, path) {
|
||||
|
||||
if playpen.escaped {
|
||||
replaced.push_str(&s[previous_end_index..playpen.start_index - 1]);
|
||||
replaced.push_str(&s[playpen.start_index..playpen.end_index]);
|
||||
previous_end_index = playpen.end_index;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the file exists
|
||||
if !playpen.rust_file.exists() || !playpen.rust_file.is_file() {
|
||||
warn!("[-] No file exists for {{{{#playpen }}}}\n {}", playpen.rust_file.to_str().unwrap());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Open file & read file
|
||||
let mut file = if let Ok(f) = File::open(&playpen.rust_file) {
|
||||
f
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let mut file_content = String::new();
|
||||
if file.read_to_string(&mut file_content).is_err() {
|
||||
continue;
|
||||
};
|
||||
|
||||
let replacement = String::new() + "<pre><code class=\"language-rust\">" + &file_content +
|
||||
"</code></pre>";
|
||||
|
||||
replaced.push_str(&s[previous_end_index..playpen.start_index]);
|
||||
replaced.push_str(&replacement);
|
||||
previous_end_index = playpen.end_index;
|
||||
// println!("Playpen{{ {}, {}, {:?}, {} }}", playpen.start_index, playpen.end_index, playpen.rust_file,
|
||||
// playpen.editable);
|
||||
}
|
||||
|
||||
replaced.push_str(&s[previous_end_index..]);
|
||||
|
||||
replaced
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, PartialEq, Debug)]
|
||||
struct Playpen {
|
||||
start_index: usize,
|
||||
end_index: usize,
|
||||
rust_file: PathBuf,
|
||||
editable: bool,
|
||||
escaped: bool,
|
||||
}
|
||||
|
||||
fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> {
|
||||
let mut playpens = vec![];
|
||||
for (i, _) in s.match_indices("{{#playpen") {
|
||||
debug!("[*]: find_playpen");
|
||||
|
||||
let mut escaped = false;
|
||||
|
||||
if i > 0 {
|
||||
if let Some(c) = s[i - 1..].chars().nth(0) {
|
||||
if c == '\\' {
|
||||
escaped = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// DON'T forget the "+ i" else you have an index out of bounds error !!
|
||||
let end_i = if let Some(n) = s[i..].find("}}") {
|
||||
n
|
||||
} else {
|
||||
continue;
|
||||
} + i + 2;
|
||||
|
||||
debug!("s[{}..{}] = {}", i, end_i, s[i..end_i].to_string());
|
||||
|
||||
// If there is nothing between "{{#playpen" and "}}" skip
|
||||
if end_i - 2 - (i + 10) < 1 {
|
||||
continue;
|
||||
}
|
||||
if s[i + 10..end_i - 2].trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("{}", s[i + 10..end_i - 2].to_string());
|
||||
|
||||
// Split on whitespaces
|
||||
let params: Vec<&str> = s[i + 10..end_i - 2].split_whitespace().collect();
|
||||
let editable = params
|
||||
.get(1)
|
||||
.map(|p| p.find("editable").is_some())
|
||||
.unwrap_or(false);
|
||||
|
||||
playpens.push(Playpen {
|
||||
start_index: i,
|
||||
end_index: end_i,
|
||||
rust_file: base_path.join(PathBuf::from(params[0])),
|
||||
editable: editable,
|
||||
escaped: escaped,
|
||||
})
|
||||
}
|
||||
|
||||
playpens
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_no_playpen() {
|
||||
let s = "Some random text without playpen...";
|
||||
assert!(find_playpens(s, Path::new("")) == vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_partial_playpen() {
|
||||
let s = "Some random text with {{#playpen...";
|
||||
assert!(find_playpens(s, Path::new("")) == vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_empty_playpen() {
|
||||
let s = "Some random text with {{#playpen}} and {{#playpen }}...";
|
||||
assert!(find_playpens(s, Path::new("")) == vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_simple_playpen() {
|
||||
let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";
|
||||
|
||||
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("")));
|
||||
|
||||
assert!(find_playpens(s, Path::new("")) ==
|
||||
vec![Playpen {
|
||||
start_index: 22,
|
||||
end_index: 42,
|
||||
rust_file: PathBuf::from("file.rs"),
|
||||
editable: false,
|
||||
escaped: false,
|
||||
},
|
||||
Playpen {
|
||||
start_index: 47,
|
||||
end_index: 68,
|
||||
rust_file: PathBuf::from("test.rs"),
|
||||
editable: false,
|
||||
escaped: false,
|
||||
}]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_complex_playpen() {
|
||||
let s = "Some random text with {{#playpen file.rs editable}} and {{#playpen test.rs editable }}...";
|
||||
|
||||
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("dir")));
|
||||
|
||||
assert!(find_playpens(s, Path::new("dir")) ==
|
||||
vec![Playpen {
|
||||
start_index: 22,
|
||||
end_index: 51,
|
||||
rust_file: PathBuf::from("dir/file.rs"),
|
||||
editable: true,
|
||||
escaped: false,
|
||||
},
|
||||
Playpen {
|
||||
start_index: 56,
|
||||
end_index: 86,
|
||||
rust_file: PathBuf::from("dir/test.rs"),
|
||||
editable: true,
|
||||
escaped: false,
|
||||
}]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_escaped_playpen() {
|
||||
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
|
||||
|
||||
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("")));
|
||||
|
||||
assert!(find_playpens(s, Path::new("")) ==
|
||||
vec![
|
||||
Playpen{start_index: 39, end_index: 68, rust_file: PathBuf::from("file.rs"), editable: true, escaped: true},
|
||||
]);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::path::Path;
|
||||
use std::collections::{VecDeque, BTreeMap};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper};
|
||||
use pulldown_cmark::{Parser, html, Event, Tag};
|
||||
use handlebars::{Handlebars, Helper, HelperDef, RenderContext, RenderError};
|
||||
use pulldown_cmark::{html, Event, Parser, Tag};
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -11,77 +11,78 @@ pub struct RenderToc;
|
||||
|
||||
impl HelperDef for RenderToc {
|
||||
fn call(&self, _h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
|
||||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = rc.context().navigate(rc.get_path(), &VecDeque::new(), "chapters").to_owned();
|
||||
let current = rc.context().navigate(rc.get_path(), &VecDeque::new(), "path").to_string().replace("\"", "");
|
||||
try!(rc.writer.write_all("<ul class=\"chapter\">".as_bytes()));
|
||||
let chapters = rc.evaluate_absolute("chapters").and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
let current = rc.evaluate_absolute("path")?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String, String>> = serde_json::from_str(&chapters.to_string()).unwrap();
|
||||
rc.writer.write_all(b"<ul class=\"chapter\">")?;
|
||||
|
||||
let mut current_level = 1;
|
||||
|
||||
for item in decoded {
|
||||
|
||||
for item in chapters {
|
||||
// Spacer
|
||||
if item.get("spacer").is_some() {
|
||||
try!(rc.writer.write_all("<li class=\"spacer\"></li>".as_bytes()));
|
||||
rc.writer.write_all(b"<li class=\"spacer\"></li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let level = if let Some(s) = item.get("section") {
|
||||
s.matches(".").count()
|
||||
s.matches('.').count()
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
if level > current_level {
|
||||
while level > current_level {
|
||||
try!(rc.writer.write_all("<li>".as_bytes()));
|
||||
try!(rc.writer.write_all("<ul class=\"section\">".as_bytes()));
|
||||
rc.writer.write_all(b"<li>")?;
|
||||
rc.writer.write_all(b"<ul class=\"section\">")?;
|
||||
current_level += 1;
|
||||
}
|
||||
try!(rc.writer.write_all("<li>".as_bytes()));
|
||||
rc.writer.write_all(b"<li>")?;
|
||||
} else if level < current_level {
|
||||
while level < current_level {
|
||||
try!(rc.writer.write_all("</ul>".as_bytes()));
|
||||
try!(rc.writer.write_all("</li>".as_bytes()));
|
||||
rc.writer.write_all(b"</ul>")?;
|
||||
rc.writer.write_all(b"</li>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
try!(rc.writer.write_all("<li>".as_bytes()));
|
||||
rc.writer.write_all(b"<li>")?;
|
||||
} else {
|
||||
try!(rc.writer.write_all("<li".as_bytes()));
|
||||
rc.writer.write_all(b"<li")?;
|
||||
if item.get("section").is_none() {
|
||||
try!(rc.writer.write_all(" class=\"affix\"".as_bytes()));
|
||||
rc.writer.write_all(b" class=\"affix\"")?;
|
||||
}
|
||||
try!(rc.writer.write_all(">".as_bytes()));
|
||||
rc.writer.write_all(b">")?;
|
||||
}
|
||||
|
||||
// Link
|
||||
let path_exists = if let Some(path) = item.get("path") {
|
||||
if !path.is_empty() {
|
||||
try!(rc.writer.write_all("<a href=\"".as_bytes()));
|
||||
rc.writer.write_all(b"<a href=\"")?;
|
||||
|
||||
let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)"))
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
.replace("\\", "/");
|
||||
|
||||
// Add link
|
||||
try!(rc.writer.write_all(Path::new(item.get("path")
|
||||
.expect("Error: path should be Some(_)"))
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
.replace("\\", "/")
|
||||
.as_bytes()));
|
||||
|
||||
try!(rc.writer.write_all("\"".as_bytes()));
|
||||
rc.writer.write_all(tmp.as_bytes())?;
|
||||
rc.writer.write_all(b"\"")?;
|
||||
|
||||
if path == ¤t {
|
||||
try!(rc.writer.write_all(" class=\"active\"".as_bytes()));
|
||||
rc.writer.write_all(b" class=\"active\"")?;
|
||||
}
|
||||
|
||||
try!(rc.writer.write_all(">".as_bytes()));
|
||||
rc.writer.write_all(b">")?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -92,47 +93,44 @@ impl HelperDef for RenderToc {
|
||||
|
||||
// Section does not necessarily exist
|
||||
if let Some(section) = item.get("section") {
|
||||
try!(rc.writer.write_all("<strong>".as_bytes()));
|
||||
try!(rc.writer.write_all(section.as_bytes()));
|
||||
try!(rc.writer.write_all("</strong> ".as_bytes()));
|
||||
rc.writer.write_all(b"<strong>")?;
|
||||
rc.writer.write_all(section.as_bytes())?;
|
||||
rc.writer.write_all(b"</strong> ")?;
|
||||
}
|
||||
|
||||
if let Some(name) = item.get("name") {
|
||||
// Render only inline code blocks
|
||||
|
||||
// filter all events that are not inline code blocks
|
||||
let parser = Parser::new(name).filter(|event| {
|
||||
match *event {
|
||||
Event::Start(Tag::Code) |
|
||||
Event::End(Tag::Code) |
|
||||
Event::InlineHtml(_) |
|
||||
Event::Text(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
let parser = Parser::new(name).filter(|event| match *event {
|
||||
Event::Start(Tag::Code) |
|
||||
Event::End(Tag::Code) |
|
||||
Event::InlineHtml(_) |
|
||||
Event::Text(_) => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// render markdown to html
|
||||
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
|
||||
html::push_html(&mut markdown_parsed_name, parser);
|
||||
|
||||
// write to the handlebars template
|
||||
try!(rc.writer.write_all(markdown_parsed_name.as_bytes()));
|
||||
rc.writer.write_all(markdown_parsed_name.as_bytes())?;
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
try!(rc.writer.write_all("</a>".as_bytes()));
|
||||
rc.writer.write_all(b"</a>")?;
|
||||
}
|
||||
|
||||
try!(rc.writer.write_all("</li>".as_bytes()));
|
||||
|
||||
rc.writer.write_all(b"</li>")?;
|
||||
}
|
||||
while current_level > 1 {
|
||||
try!(rc.writer.write_all("</ul>".as_bytes()));
|
||||
try!(rc.writer.write_all("</li>".as_bytes()));
|
||||
rc.writer.write_all(b"</ul>")?;
|
||||
rc.writer.write_all(b"</li>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
|
||||
try!(rc.writer.write_all("</ul>".as_bytes()));
|
||||
rc.writer.write_all(b"</ul>")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ pub use self::html_handlebars::HtmlHandlebars;
|
||||
|
||||
mod html_handlebars;
|
||||
|
||||
use std::error::Error;
|
||||
use errors::*;
|
||||
|
||||
pub trait Renderer {
|
||||
fn render(&self, book: &::book::MDBook) -> Result<(), Box<Error>>;
|
||||
fn render(&self, book: &::book::MDBook) -> Result<()>;
|
||||
}
|
||||
|
||||
71
src/theme/ayu-highlight.css
Normal file
71
src/theme/ayu-highlight.css
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
Based off of the Ayu theme
|
||||
Original by Dempfi (https://github.com/dempfi/ayu)
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
background: #191f26;
|
||||
color: #e6e1cf;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote,
|
||||
.hljs-meta {
|
||||
color: #5c6773;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-attr,
|
||||
.hljs-regexp,
|
||||
.hljs-link,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #ff7733;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #ffee99;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-bullet {
|
||||
color: #b8cc52;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-built_in,
|
||||
.hljs-section {
|
||||
color: #ffb454;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-symbol {
|
||||
color: #ff7733;
|
||||
}
|
||||
|
||||
.hljs-name {
|
||||
color: #36a3d9;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #00568d;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -3,8 +3,12 @@ body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
code {
|
||||
font-family: "Source Code Pro", "Menlo", "DejaVu Sans Mono", monospace;
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
.left {
|
||||
@@ -16,6 +20,9 @@ code {
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.play-button.hidden {
|
||||
display: none;
|
||||
}
|
||||
h2,
|
||||
h3 {
|
||||
margin-top: 2.5em;
|
||||
@@ -41,7 +48,7 @@ table thead td {
|
||||
font-weight: 700;
|
||||
}
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
@@ -102,43 +109,31 @@ table thead td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.page-wrapper {
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
left: 315px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
padding-left: 300px;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
min-height: 100%;
|
||||
-webkit-transition: left 0.5s;
|
||||
-moz-transition: left 0.5s;
|
||||
-o-transition: left 0.5s;
|
||||
-ms-transition: left 0.5s;
|
||||
transition: left 0.5s;
|
||||
-webkit-transition: padding-left 0.5s;
|
||||
-moz-transition: padding-left 0.5s;
|
||||
-o-transition: padding-left 0.5s;
|
||||
-ms-transition: padding-left 0.5s;
|
||||
transition: padding-left 0.5s;
|
||||
}
|
||||
@media only screen and (max-width: 1060px) {
|
||||
.page-wrapper {
|
||||
left: 15px;
|
||||
padding-right: 15px;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
.sidebar-hidden .page-wrapper {
|
||||
left: 15px;
|
||||
padding-left: 0;
|
||||
}
|
||||
.sidebar-visible .page-wrapper {
|
||||
left: 315px;
|
||||
padding-left: 300px;
|
||||
}
|
||||
.page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding-right: 15px;
|
||||
overflow-y: auto;
|
||||
outline: 0;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.content {
|
||||
margin-left: auto;
|
||||
@@ -209,7 +204,7 @@ table thead td {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 50px /* Height of menu-bar */;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
@@ -249,16 +244,31 @@ table thead td {
|
||||
text-decoration: none;
|
||||
}
|
||||
.previous {
|
||||
left: 0;
|
||||
left: 315px;
|
||||
-webkit-transition: left 0.5s;
|
||||
-moz-transition: left 0.5s;
|
||||
-o-transition: left 0.5s;
|
||||
-ms-transition: left 0.5s;
|
||||
transition: left 0.5s;
|
||||
}
|
||||
@media only screen and (max-width: 1060px) {
|
||||
.previous {
|
||||
left: 15px;
|
||||
}
|
||||
}
|
||||
.next {
|
||||
right: 15px;
|
||||
}
|
||||
.sidebar-hidden .previous {
|
||||
left: 15px;
|
||||
}
|
||||
.sidebar-visible .previous {
|
||||
left: 315px;
|
||||
}
|
||||
.theme-popup {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
z-index: 1000;
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
@@ -267,6 +277,7 @@ table thead td {
|
||||
padding: 2px 10px;
|
||||
line-height: 25px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.theme-popup .theme:hover:first-child,
|
||||
.theme-popup .theme:hover:last-child {
|
||||
@@ -295,7 +306,6 @@ table thead td {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: 50px;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.next {
|
||||
@@ -342,7 +352,8 @@ table thead td {
|
||||
.light .nav-chapters,
|
||||
.light .nav-chapters:visited,
|
||||
.light .mobile-nav-chapters,
|
||||
.light .mobile-nav-chapters:visited {
|
||||
.light .mobile-nav-chapters:visited,
|
||||
.light .menu-bar a i {
|
||||
color: #ccc;
|
||||
}
|
||||
.light .menu-bar i:hover,
|
||||
@@ -357,7 +368,8 @@ table thead td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.light .content a:link,
|
||||
.light a:visited {
|
||||
.light a:visited,
|
||||
.light a > .hljs {
|
||||
color: #4183c4;
|
||||
}
|
||||
.light .theme-popup {
|
||||
@@ -398,14 +410,18 @@ table thead td {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
color: #6e6b5e;
|
||||
}
|
||||
.light a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.light pre {
|
||||
position: relative;
|
||||
}
|
||||
.light pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #364149;
|
||||
@@ -457,7 +473,8 @@ table thead td {
|
||||
.coal .nav-chapters,
|
||||
.coal .nav-chapters:visited,
|
||||
.coal .mobile-nav-chapters,
|
||||
.coal .mobile-nav-chapters:visited {
|
||||
.coal .mobile-nav-chapters:visited,
|
||||
.coal .menu-bar a i {
|
||||
color: #43484d;
|
||||
}
|
||||
.coal .menu-bar i:hover,
|
||||
@@ -472,7 +489,8 @@ table thead td {
|
||||
background-color: #292c2f;
|
||||
}
|
||||
.coal .content a:link,
|
||||
.coal a:visited {
|
||||
.coal a:visited,
|
||||
.coal a > .hljs {
|
||||
color: #2b79a2;
|
||||
}
|
||||
.coal .theme-popup {
|
||||
@@ -513,14 +531,18 @@ table thead td {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
color: #c5c8c6;
|
||||
}
|
||||
.coal a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.coal pre {
|
||||
position: relative;
|
||||
}
|
||||
.coal pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #a1adb8;
|
||||
@@ -572,7 +594,8 @@ table thead td {
|
||||
.navy .nav-chapters,
|
||||
.navy .nav-chapters:visited,
|
||||
.navy .mobile-nav-chapters,
|
||||
.navy .mobile-nav-chapters:visited {
|
||||
.navy .mobile-nav-chapters:visited,
|
||||
.navy .menu-bar a i {
|
||||
color: #737480;
|
||||
}
|
||||
.navy .menu-bar i:hover,
|
||||
@@ -587,7 +610,8 @@ table thead td {
|
||||
background-color: #282d3f;
|
||||
}
|
||||
.navy .content a:link,
|
||||
.navy a:visited {
|
||||
.navy a:visited,
|
||||
.navy a > .hljs {
|
||||
color: #2b79a2;
|
||||
}
|
||||
.navy .theme-popup {
|
||||
@@ -628,14 +652,18 @@ table thead td {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
color: #c5c8c6;
|
||||
}
|
||||
.navy a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.navy pre {
|
||||
position: relative;
|
||||
}
|
||||
.navy pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #c8c9db;
|
||||
@@ -687,7 +715,8 @@ table thead td {
|
||||
.rust .nav-chapters,
|
||||
.rust .nav-chapters:visited,
|
||||
.rust .mobile-nav-chapters,
|
||||
.rust .mobile-nav-chapters:visited {
|
||||
.rust .mobile-nav-chapters:visited,
|
||||
.rust .menu-bar a i {
|
||||
color: #737480;
|
||||
}
|
||||
.rust .menu-bar i:hover,
|
||||
@@ -702,7 +731,8 @@ table thead td {
|
||||
background-color: #3b2e2a;
|
||||
}
|
||||
.rust .content a:link,
|
||||
.rust a:visited {
|
||||
.rust a:visited,
|
||||
.rust a > .hljs {
|
||||
color: #2b79a2;
|
||||
}
|
||||
.rust .theme-popup {
|
||||
@@ -743,14 +773,18 @@ table thead td {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
color: #6e6b5e;
|
||||
}
|
||||
.rust a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.rust pre {
|
||||
position: relative;
|
||||
}
|
||||
.rust pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #c8c9db;
|
||||
@@ -765,6 +799,127 @@ table thead td {
|
||||
.rust pre > .result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.ayu {
|
||||
color: #c5c5c5;
|
||||
background-color: #0f1419;
|
||||
/* Inline code */
|
||||
}
|
||||
.ayu .content .header:link,
|
||||
.ayu .content .header:visited {
|
||||
color: #c5c5c5;
|
||||
pointer: cursor;
|
||||
}
|
||||
.ayu .content .header:link:hover,
|
||||
.ayu .content .header:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.ayu .sidebar {
|
||||
background-color: #14191f;
|
||||
color: #c8c9db;
|
||||
}
|
||||
.ayu .chapter li {
|
||||
color: #5c6773;
|
||||
}
|
||||
.ayu .chapter li a {
|
||||
color: #c8c9db;
|
||||
}
|
||||
.ayu .chapter li .active,
|
||||
.ayu .chapter li a:hover {
|
||||
/* Animate color change */
|
||||
color: #ffb454;
|
||||
}
|
||||
.ayu .chapter .spacer {
|
||||
background-color: #2d334f;
|
||||
}
|
||||
.ayu .menu-bar,
|
||||
.ayu .menu-bar:visited,
|
||||
.ayu .nav-chapters,
|
||||
.ayu .nav-chapters:visited,
|
||||
.ayu .mobile-nav-chapters,
|
||||
.ayu .mobile-nav-chapters:visited,
|
||||
.ayu .menu-bar a i {
|
||||
color: #737480;
|
||||
}
|
||||
.ayu .menu-bar i:hover,
|
||||
.ayu .nav-chapters:hover,
|
||||
.ayu .mobile-nav-chapters i:hover {
|
||||
color: #b7b9cc;
|
||||
}
|
||||
.ayu .mobile-nav-chapters i:hover {
|
||||
color: #c8c9db;
|
||||
}
|
||||
.ayu .mobile-nav-chapters {
|
||||
background-color: #14191f;
|
||||
}
|
||||
.ayu .content a:link,
|
||||
.ayu a:visited,
|
||||
.ayu a > .hljs {
|
||||
color: #0096cf;
|
||||
}
|
||||
.ayu .theme-popup {
|
||||
color: #c5c5c5;
|
||||
background: #14191f;
|
||||
border: 1px solid #5c6773;
|
||||
}
|
||||
.ayu .theme-popup .theme:hover {
|
||||
background-color: #191f26;
|
||||
}
|
||||
.ayu .theme-popup .default {
|
||||
color: #737480;
|
||||
}
|
||||
.ayu blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
color: #c5c5c5;
|
||||
background-color: #262933;
|
||||
border-top: 0.1em solid #2f333f;
|
||||
border-bottom: 0.1em solid #2f333f;
|
||||
}
|
||||
.ayu table td {
|
||||
border-color: #182028;
|
||||
}
|
||||
.ayu table tbody tr:nth-child(2n) {
|
||||
background: #141b22;
|
||||
}
|
||||
.ayu table thead {
|
||||
background: #324354;
|
||||
}
|
||||
.ayu table thead td {
|
||||
border: none;
|
||||
}
|
||||
.ayu table thead tr {
|
||||
border: 1px #324354 solid;
|
||||
}
|
||||
.ayu :not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
color: #ffb454;
|
||||
}
|
||||
.ayu a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ayu pre {
|
||||
position: relative;
|
||||
}
|
||||
.ayu pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #c8c9db;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ayu pre > .buttons :hover {
|
||||
color: #ffb454;
|
||||
}
|
||||
.ayu pre > .buttons i {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.ayu pre > .result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@media only print {
|
||||
#sidebar,
|
||||
#menu-bar,
|
||||
@@ -776,6 +931,9 @@ table thead td {
|
||||
left: 0;
|
||||
overflow-y: initial;
|
||||
}
|
||||
#page-wrapper.page-wrapper {
|
||||
padding-left: 0px;
|
||||
}
|
||||
#content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
@@ -786,11 +944,13 @@ table thead td {
|
||||
}
|
||||
code {
|
||||
background-color: #666;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
pre > .buttons {
|
||||
z-index: 2;
|
||||
}
|
||||
a,
|
||||
a:visited,
|
||||
a:active,
|
||||
@@ -818,3 +978,25 @@ table thead td {
|
||||
word-wrap: break-word /* Internet Explorer 5.5+ */;
|
||||
}
|
||||
}
|
||||
.tooltiptext {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
-webkit-transform: translateX(-50%);
|
||||
-moz-transform: translateX(-50%);
|
||||
-o-transform: translateX(-50%);
|
||||
-ms-transform: translateX(-50%);
|
||||
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
|
||||
left: -8px; /* Half of the width of the icon */
|
||||
top: -35px;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px 8px;
|
||||
margin: 5px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.tooltipped .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@@ -7,22 +7,31 @@ $( document ).ready(function() {
|
||||
window.onunload = function(){};
|
||||
|
||||
// Set theme
|
||||
var theme = localStorage.getItem('theme');
|
||||
if (theme === null) { theme = 'light'; }
|
||||
var theme = store.get('mdbook-theme');
|
||||
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||
|
||||
set_theme(theme);
|
||||
|
||||
|
||||
// Syntax highlighting Configuration
|
||||
hljs.configure({
|
||||
tabReplace: ' ', // 4 spaces
|
||||
languages: [], // Languages used for auto-detection
|
||||
});
|
||||
|
||||
$('code').each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
|
||||
if (window.ace) {
|
||||
// language-rust class needs to be removed for editable
|
||||
// blocks or highlightjs will capture events
|
||||
$('code.editable').removeClass('language-rust');
|
||||
|
||||
$('code').not('.editable').each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} else {
|
||||
$('code').each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
}
|
||||
|
||||
// Adding the hljs class gives code blocks the color css
|
||||
// even if highlighting doesn't apply
|
||||
$('code').addClass('hljs');
|
||||
@@ -51,31 +60,22 @@ $( document ).ready(function() {
|
||||
});
|
||||
|
||||
// Interesting DOM Elements
|
||||
var html = $("html");
|
||||
var sidebar = $("#sidebar");
|
||||
var page_wrapper = $("#page-wrapper");
|
||||
var content = $("#content");
|
||||
|
||||
// Help keyboard navigation by always focusing on page content
|
||||
$(".page").focus();
|
||||
|
||||
// Toggle sidebar
|
||||
$("#sidebar-toggle").click(function(event){
|
||||
if ( html.hasClass("sidebar-hidden") ) {
|
||||
html.removeClass("sidebar-hidden").addClass("sidebar-visible");
|
||||
localStorage.setItem('sidebar', 'visible');
|
||||
} else if ( html.hasClass("sidebar-visible") ) {
|
||||
html.removeClass("sidebar-visible").addClass("sidebar-hidden");
|
||||
localStorage.setItem('sidebar', 'hidden');
|
||||
} else {
|
||||
if(sidebar.position().left === 0){
|
||||
html.addClass("sidebar-hidden");
|
||||
localStorage.setItem('sidebar', 'hidden');
|
||||
} else {
|
||||
html.addClass("sidebar-visible");
|
||||
localStorage.setItem('sidebar', 'visible');
|
||||
}
|
||||
$("#sidebar-toggle").click(sidebarToggle);
|
||||
|
||||
// Hide sidebar on section link click if it occupies large space
|
||||
// in relation to the whole screen (phone in portrait)
|
||||
$("#sidebar a").click(function(event){
|
||||
if (sidebar.width() > window.screen.width * 0.4) {
|
||||
sidebarToggle();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Scroll sidebar to current active section
|
||||
var activeSection = sidebar.find(".active");
|
||||
if(activeSection.length) {
|
||||
@@ -83,16 +83,6 @@ $( document ).ready(function() {
|
||||
}
|
||||
|
||||
|
||||
// Print button
|
||||
$("#print-button").click(function(){
|
||||
var printWindow = window.open("print.html");
|
||||
});
|
||||
|
||||
if( url.substring(url.lastIndexOf('/')+1) == "print.html" ) {
|
||||
window.print();
|
||||
}
|
||||
|
||||
|
||||
// Theme button
|
||||
$("#theme-toggle").click(function(){
|
||||
if($('.theme-popup').length) {
|
||||
@@ -102,30 +92,60 @@ $( document ).ready(function() {
|
||||
.append($('<div class="theme" id="light">Light <span class="default">(default)</span><div>'))
|
||||
.append($('<div class="theme" id="rust">Rust</div>'))
|
||||
.append($('<div class="theme" id="coal">Coal</div>'))
|
||||
.append($('<div class="theme" id="navy">Navy</div>'));
|
||||
.append($('<div class="theme" id="navy">Navy</div>'))
|
||||
.append($('<div class="theme" id="ayu">Ayu</div>'));
|
||||
|
||||
|
||||
popup.insertAfter(this);
|
||||
|
||||
$('.theme').click(function(){
|
||||
var theme = $(this).attr('id');
|
||||
|
||||
set_theme(theme);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Hide theme selector popup when clicking outside of it
|
||||
$(document).click(function(event){
|
||||
var popup = $('.theme-popup');
|
||||
if(popup.length) {
|
||||
var target = $(event.target);
|
||||
if(!target.closest('.theme').length && !target.closest('#theme-toggle').length) {
|
||||
popup.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function set_theme(theme) {
|
||||
let ace_theme;
|
||||
|
||||
if (theme == 'coal' || theme == 'navy') {
|
||||
$("[href='ayu-highlight.css']").prop('disabled', true);
|
||||
$("[href='tomorrow-night.css']").prop('disabled', false);
|
||||
$("[href='highlight.css']").prop('disabled', true);
|
||||
|
||||
ace_theme = "ace/theme/tomorrow_night";
|
||||
} else if (theme == 'ayu') {
|
||||
$("[href='ayu-highlight.css']").prop('disabled', false);
|
||||
$("[href='tomorrow-night.css']").prop('disabled', true);
|
||||
$("[href='highlight.css']").prop('disabled', true);
|
||||
|
||||
ace_theme = "ace/theme/tomorrow_night";
|
||||
} else {
|
||||
$("[href='ayu-highlight.css']").prop('disabled', true);
|
||||
$("[href='tomorrow-night.css']").prop('disabled', true);
|
||||
$("[href='highlight.css']").prop('disabled', false);
|
||||
|
||||
ace_theme = "ace/theme/dawn";
|
||||
}
|
||||
|
||||
localStorage.setItem('theme', theme);
|
||||
if (window.ace && window.editors) {
|
||||
window.editors.forEach(function(editor) {
|
||||
editor.setTheme(ace_theme);
|
||||
});
|
||||
}
|
||||
|
||||
store.set('mdbook-theme', theme);
|
||||
|
||||
$('body').removeClass().addClass(theme);
|
||||
}
|
||||
@@ -146,10 +166,10 @@ $( document ).ready(function() {
|
||||
for(var n = 0; n < lines.length; n++){
|
||||
if($.trim(lines[n])[0] == hiding_character){
|
||||
if(first_non_hidden_line){
|
||||
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)#/, "$1") + "</span>";
|
||||
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)# ?/, "$1") + "</span>";
|
||||
}
|
||||
else {
|
||||
lines[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)#/, "$1") + "\n" + "</span>";
|
||||
lines[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)# ?/, "$1") + "\n" + "</span>";
|
||||
}
|
||||
lines_hidden = true;
|
||||
}
|
||||
@@ -166,21 +186,22 @@ $( document ).ready(function() {
|
||||
if(!lines_hidden) { return; }
|
||||
|
||||
// add expand button
|
||||
pre_block.prepend("<div class=\"buttons\"><i class=\"fa fa-expand\"></i></div>");
|
||||
pre_block.prepend("<div class=\"buttons\"><i class=\"fa fa-expand\" title=\"Show hidden lines\"></i></div>");
|
||||
|
||||
pre_block.find("i").click(function(e){
|
||||
if( $(this).hasClass("fa-expand") ) {
|
||||
$(this).removeClass("fa-expand").addClass("fa-compress");
|
||||
$(this).attr("title", "Hide lines");
|
||||
pre_block.find("span.hidden").removeClass("hidden").addClass("unhidden");
|
||||
}
|
||||
else {
|
||||
$(this).removeClass("fa-compress").addClass("fa-expand");
|
||||
$(this).attr("title", "Show hidden lines");
|
||||
pre_block.find("span.unhidden").removeClass("unhidden").addClass("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Process playpen code blocks
|
||||
$(".playpen").each(function(block){
|
||||
var pre_block = $(this);
|
||||
@@ -190,16 +211,147 @@ $( document ).ready(function() {
|
||||
pre_block.prepend("<div class=\"buttons\"></div>");
|
||||
buttons = pre_block.find(".buttons");
|
||||
}
|
||||
buttons.prepend("<i class=\"fa fa-play play-button\"></i>");
|
||||
buttons.prepend("<i class=\"fa fa-play play-button hidden\" title=\"Run this code\"></i>");
|
||||
buttons.prepend("<i class=\"fa fa-copy clip-button\" title=\"Copy to clipboard\"><i class=\"tooltiptext\"></i></i>");
|
||||
|
||||
let code_block = pre_block.find("code").first();
|
||||
if (window.ace && code_block.hasClass("editable")) {
|
||||
buttons.prepend("<i class=\"fa fa-history reset-button\" title=\"Undo changes\"></i>");
|
||||
}
|
||||
|
||||
buttons.find(".play-button").click(function(e){
|
||||
run_rust_code(pre_block);
|
||||
});
|
||||
buttons.find(".clip-button").mouseout(function(e){
|
||||
hideTooltip(e.currentTarget);
|
||||
});
|
||||
buttons.find(".reset-button").click(function() {
|
||||
if (!window.ace) { return; }
|
||||
let editor = window.ace.edit(code_block.get(0));
|
||||
editor.setValue(editor.originalCode);
|
||||
editor.clearSelection();
|
||||
});
|
||||
});
|
||||
|
||||
var clipboardSnippets = new Clipboard('.clip-button', {
|
||||
text: function(trigger) {
|
||||
hideTooltip(trigger);
|
||||
let playpen = $(trigger).parents(".playpen");
|
||||
return playpen_text(playpen);
|
||||
}
|
||||
});
|
||||
clipboardSnippets.on('success', function(e) {
|
||||
e.clearSelection();
|
||||
showTooltip(e.trigger, "Copied!");
|
||||
});
|
||||
clipboardSnippets.on('error', function(e) {
|
||||
showTooltip(e.trigger, "Clipboard error!");
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: "https://play.rust-lang.org/meta/crates",
|
||||
method: "POST",
|
||||
crossDomain: true,
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
success: function(response){
|
||||
// get list of crates available in the rust playground
|
||||
let playground_crates = response.crates.map(function(item) {return item["id"];} );
|
||||
$(".playpen").each(function(block) {
|
||||
handle_crate_list_update($(this), playground_crates);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function playpen_text(playpen) {
|
||||
let code_block = playpen.find("code").first();
|
||||
|
||||
if (window.ace && code_block.hasClass("editable")) {
|
||||
let editor = window.ace.edit(code_block.get(0));
|
||||
return editor.getValue();
|
||||
} else {
|
||||
return code_block.get(0).textContent;
|
||||
}
|
||||
}
|
||||
|
||||
function handle_crate_list_update(playpen_block, playground_crates) {
|
||||
// update the play buttons after receiving the response
|
||||
update_play_button(playpen_block, playground_crates);
|
||||
|
||||
// and install on change listener to dynamically update ACE editors
|
||||
if (window.ace) {
|
||||
let code_block = playpen_block.find("code").first();
|
||||
if (code_block.hasClass("editable")) {
|
||||
let editor = window.ace.edit(code_block.get(0));
|
||||
editor.on("change", function(e){
|
||||
update_play_button(playpen_block, playground_crates);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updates the visibility of play button based on `no_run` class and
|
||||
// used crates vs ones available on http://play.rust-lang.org
|
||||
function update_play_button(pre_block, playground_crates) {
|
||||
var play_button = pre_block.find(".play-button");
|
||||
|
||||
var classes = pre_block.find("code").attr("class").split(" ");
|
||||
// skip if code is `no_run`
|
||||
if (classes.indexOf("no_run") > -1) {
|
||||
play_button.addClass("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
// get list of `extern crate`'s from snippet
|
||||
var txt = playpen_text(pre_block);
|
||||
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
|
||||
var snippet_crates = [];
|
||||
while (item = re.exec(txt)) {
|
||||
snippet_crates.push(item[1]);
|
||||
}
|
||||
|
||||
// check if all used crates are available on play.rust-lang.org
|
||||
var all_available = snippet_crates.every(function(elem) {
|
||||
return playground_crates.indexOf(elem) > -1;
|
||||
});
|
||||
|
||||
if (all_available) {
|
||||
play_button.removeClass("hidden");
|
||||
} else {
|
||||
play_button.addClass("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function hideTooltip(elem) {
|
||||
elem.firstChild.innerText="";
|
||||
elem.setAttribute('class', 'fa fa-copy clip-button');
|
||||
}
|
||||
|
||||
function showTooltip(elem, msg) {
|
||||
elem.firstChild.innerText=msg;
|
||||
elem.setAttribute('class', 'fa fa-copy tooltipped');
|
||||
}
|
||||
|
||||
function sidebarToggle() {
|
||||
var html = $("html");
|
||||
if ( html.hasClass("sidebar-hidden") ) {
|
||||
html.removeClass("sidebar-hidden").addClass("sidebar-visible");
|
||||
store.set('mdbook-sidebar', 'visible');
|
||||
} else if ( html.hasClass("sidebar-visible") ) {
|
||||
html.removeClass("sidebar-visible").addClass("sidebar-hidden");
|
||||
store.set('mdbook-sidebar', 'hidden');
|
||||
} else {
|
||||
if($("#sidebar").position().left === 0){
|
||||
html.addClass("sidebar-hidden");
|
||||
store.set('mdbook-sidebar', 'hidden');
|
||||
} else {
|
||||
html.addClass("sidebar-visible");
|
||||
store.set('mdbook-sidebar', 'visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function run_rust_code(code_block) {
|
||||
var result_block = code_block.find(".result");
|
||||
@@ -208,29 +360,35 @@ function run_rust_code(code_block) {
|
||||
result_block = code_block.find(".result");
|
||||
}
|
||||
|
||||
let text = code_block.find(".language-rust").text();
|
||||
let text = playpen_text(code_block);
|
||||
|
||||
var params = {
|
||||
channel: "stable",
|
||||
mode: "debug",
|
||||
crateType: "bin",
|
||||
tests: false,
|
||||
code: text,
|
||||
}
|
||||
|
||||
let params = {
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
code: text,
|
||||
};
|
||||
|
||||
if(text.includes("#![feature")) {
|
||||
params.version = "nightly";
|
||||
if(text.indexOf("#![feature") !== -1) {
|
||||
params.channel = "nightly";
|
||||
}
|
||||
|
||||
result_block.text("Running...");
|
||||
|
||||
$.ajax({
|
||||
url: "https://play.rust-lang.org/evaluate.json",
|
||||
url: "https://play.rust-lang.org/execute",
|
||||
method: "POST",
|
||||
crossDomain: true,
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(params),
|
||||
timeout: 15000,
|
||||
success: function(response){
|
||||
result_block.text(response.result);
|
||||
}
|
||||
result_block.text(response.success ? response.stdout : response.stderr);
|
||||
},
|
||||
error: function(qXHR, textStatus, errorThrown){
|
||||
result_block.text("Playground communication " + textStatus);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
7
src/theme/clipboard.min.js
vendored
Normal file
7
src/theme/clipboard.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/theme/header.hbs
Normal file
1
src/theme/header.hbs
Normal file
@@ -0,0 +1 @@
|
||||
{{!-- Put your header HTML text here --}}
|
||||
@@ -1,15 +1,6 @@
|
||||
/* Modified Base16 Atelier Dune Light - Theme
|
||||
/* Original by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
background: #f1f1f1;
|
||||
color: #6e6b5e;
|
||||
padding: 0.5em;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
/* Base16 Atelier Dune Light - Theme */
|
||||
/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */
|
||||
/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */
|
||||
|
||||
/* Atelier-Dune Comment */
|
||||
.hljs-comment,
|
||||
@@ -61,6 +52,14 @@
|
||||
color: #b854d4;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
background: #f1f1f1;
|
||||
color: #6e6b5e;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
<html lang="{{ language }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ chapter_title }} - {{ title }}</title>
|
||||
<title>{{ title }}</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="description" content="{{ description }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -20,9 +20,25 @@
|
||||
|
||||
<link rel="stylesheet" href="highlight.css">
|
||||
<link rel="stylesheet" href="tomorrow-night.css">
|
||||
<link rel="stylesheet" href="ayu-highlight.css">
|
||||
|
||||
<!-- Custom theme -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{this}}">
|
||||
{{/each}}
|
||||
|
||||
{{#if mathjax_support}}
|
||||
<!-- MathJax -->
|
||||
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
<script 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>
|
||||
|
||||
<!-- Fetch JQuery from CDN but have a local fallback -->
|
||||
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
@@ -31,18 +47,22 @@
|
||||
document.write(unescape("%3Cscript src='jquery.js'%3E%3C/script%3E"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Fetch store.js from local - TODO add CDN when 2.x.x is available on cdnjs -->
|
||||
<script src="store.js"></script>
|
||||
|
||||
</head>
|
||||
<body class="light">
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
<script type="text/javascript">
|
||||
var theme = localStorage.getItem('theme');
|
||||
if (theme == null) { theme = 'light'; }
|
||||
var theme = store.get('mdbook-theme');
|
||||
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||
$('body').removeClass().addClass(theme);
|
||||
</script>
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script type="text/javascript">
|
||||
var sidebar = localStorage.getItem('sidebar');
|
||||
var sidebar = store.get('mdbook-sidebar');
|
||||
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
|
||||
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
|
||||
</script>
|
||||
@@ -53,17 +73,20 @@
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page">
|
||||
<div class="page" tabindex="-1">
|
||||
{{> header}}
|
||||
<div id="menu-bar" class="menu-bar">
|
||||
<div class="left-buttons">
|
||||
<i id="sidebar-toggle" class="fa fa-bars"></i>
|
||||
<i id="theme-toggle" class="fa fa-paint-brush"></i>
|
||||
<i id="sidebar-toggle" class="fa fa-bars" title="Toggle sidebar"></i>
|
||||
<i id="theme-toggle" class="fa fa-paint-brush" title="Change theme"></i>
|
||||
</div>
|
||||
|
||||
<h1 class="menu-title">{{ title }}</h1>
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
|
||||
<div class="right-buttons">
|
||||
<i id="print-button" class="fa fa-print" title="Print this book"></i>
|
||||
<a href="print.html">
|
||||
<i id="print-button" class="fa fa-print" title="Print this book"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,13 +96,13 @@
|
||||
|
||||
<!-- Mobile navigation buttons -->
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="mobile-nav-chapters previous">
|
||||
<a rel="prev" href="{{link}}" class="mobile-nav-chapters previous" title="Previous chapter">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
|
||||
{{#next}}
|
||||
<a href="{{link}}" class="mobile-nav-chapters next">
|
||||
<a rel="next" href="{{link}}" class="mobile-nav-chapters next" title="Next chapter">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
{{/next}}
|
||||
@@ -111,7 +134,48 @@
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
{{{livereload}}}
|
||||
|
||||
{{#if google_analytics}}
|
||||
<!-- Google Analytics Tag -->
|
||||
<script>
|
||||
var localAddrs = ["localhost", "127.0.0.1", ""];
|
||||
|
||||
// make sure we don't activate google analytics if the developer is
|
||||
// inspecting the book locally...
|
||||
if (localAddrs.indexOf(document.location.hostname) === -1) {
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', '{{google_analytics}}', 'auto');
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playpens_editable}}
|
||||
<script src="{{ ace_js }}" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ editor_js }}" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ mode_rust_js }}" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ theme_dawn_js }}" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ theme_tomorrow_night_js }}" type="text/javascript" charset="utf-8"></script>
|
||||
{{/if}}
|
||||
|
||||
{{#if is_print}}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
window.print();
|
||||
})
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
<script src="highlight.js"></script>
|
||||
<script src="book.js"></script>
|
||||
|
||||
<!-- Custom JS script -->
|
||||
{{#each additional_js}}
|
||||
<script type="text/javascript" src="{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
217
src/theme/mod.rs
217
src/theme/mod.rs
@@ -1,105 +1,190 @@
|
||||
pub mod playpen_editor;
|
||||
|
||||
use std::path::Path;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
use errors::*;
|
||||
|
||||
pub static INDEX: &'static [u8] = include_bytes!("index.hbs");
|
||||
pub static HEADER: &'static [u8] = include_bytes!("header.hbs");
|
||||
pub static CSS: &'static [u8] = include_bytes!("book.css");
|
||||
pub static FAVICON: &'static [u8] = include_bytes!("favicon.png");
|
||||
pub static JS: &'static [u8] = include_bytes!("book.js");
|
||||
pub static HIGHLIGHT_JS: &'static [u8] = include_bytes!("highlight.js");
|
||||
pub static TOMORROW_NIGHT_CSS: &'static [u8] = include_bytes!("tomorrow-night.css");
|
||||
pub static HIGHLIGHT_CSS: &'static [u8] = include_bytes!("highlight.css");
|
||||
pub static JQUERY: &'static [u8] = include_bytes!("jquery-2.1.4.min.js");
|
||||
pub static AYU_HIGHLIGHT_CSS: &'static [u8] = include_bytes!("ayu-highlight.css");
|
||||
pub static JQUERY: &'static [u8] = include_bytes!("jquery.js");
|
||||
pub static CLIPBOARD_JS: &'static [u8] = include_bytes!("clipboard.min.js");
|
||||
pub static STORE_JS: &'static [u8] = include_bytes!("store.js");
|
||||
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");
|
||||
pub static FONT_AWESOME_SVG: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &'static [u8] = 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_EOT: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.eot");
|
||||
pub static FONT_AWESOME_SVG: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &'static [u8] =
|
||||
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");
|
||||
|
||||
/// 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 source folder and use the users theme instead
|
||||
/// of the default.
|
||||
|
||||
/// 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
|
||||
/// source folder and use the users theme instead of the default.
|
||||
///
|
||||
/// You should exceptionnaly use the static variables only if you need the default theme even if the
|
||||
/// user has specified another theme.
|
||||
/// You should only ever use the static variables directly if you want to
|
||||
/// override the user's theme with the defaults.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Theme {
|
||||
pub index: Vec<u8>,
|
||||
pub header: Vec<u8>,
|
||||
pub css: Vec<u8>,
|
||||
pub favicon: Vec<u8>,
|
||||
pub js: Vec<u8>,
|
||||
pub highlight_css: Vec<u8>,
|
||||
pub tomorrow_night_css: Vec<u8>,
|
||||
pub ayu_highlight_css: Vec<u8>,
|
||||
pub highlight_js: Vec<u8>,
|
||||
pub clipboard_js: Vec<u8>,
|
||||
pub store_js: Vec<u8>,
|
||||
pub jquery: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn new(src: &Path) -> Self {
|
||||
pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
|
||||
let theme_dir = theme_dir.as_ref();
|
||||
let mut theme = Theme::default();
|
||||
|
||||
// Default theme
|
||||
let mut theme = Theme {
|
||||
index: INDEX.to_owned(),
|
||||
css: CSS.to_owned(),
|
||||
favicon: FAVICON.to_owned(),
|
||||
js: JS.to_owned(),
|
||||
highlight_css: HIGHLIGHT_CSS.to_owned(),
|
||||
tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
|
||||
highlight_js: HIGHLIGHT_JS.to_owned(),
|
||||
jquery: JQUERY.to_owned(),
|
||||
};
|
||||
|
||||
// Check if the given path exists
|
||||
if !src.exists() || !src.is_dir() {
|
||||
// If the theme directory doesn't exist there's no point continuing...
|
||||
if !theme_dir.exists() || !theme_dir.is_dir() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
// Check for individual files if they exist
|
||||
// Check for individual files, if they exist copy them across
|
||||
{
|
||||
let files = vec![
|
||||
(theme_dir.join("index.hbs"), &mut theme.index),
|
||||
(theme_dir.join("header.hbs"), &mut theme.header),
|
||||
(theme_dir.join("book.js"), &mut theme.js),
|
||||
(theme_dir.join("book.css"), &mut theme.css),
|
||||
(theme_dir.join("favicon.png"), &mut theme.favicon),
|
||||
(theme_dir.join("highlight.js"), &mut theme.highlight_js),
|
||||
(theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
|
||||
(theme_dir.join("store.js"), &mut theme.store_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("jquery.js"), &mut theme.jquery),
|
||||
];
|
||||
|
||||
// index.hbs
|
||||
if let Ok(mut f) = File::open(&src.join("index.hbs")) {
|
||||
theme.index.clear(); // Reset the value, because read_to_string appends...
|
||||
let _ = f.read_to_end(&mut theme.index);
|
||||
}
|
||||
for (filename, dest) in files {
|
||||
if !filename.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// book.js
|
||||
if let Ok(mut f) = File::open(&src.join("book.js")) {
|
||||
theme.js.clear();
|
||||
let _ = f.read_to_end(&mut theme.js);
|
||||
}
|
||||
|
||||
// book.css
|
||||
if let Ok(mut f) = File::open(&src.join("book.css")) {
|
||||
theme.css.clear();
|
||||
let _ = f.read_to_end(&mut theme.css);
|
||||
}
|
||||
|
||||
// favicon.png
|
||||
if let Ok(mut f) = File::open(&src.join("favicon.png")) {
|
||||
theme.favicon.clear();
|
||||
let _ = f.read_to_end(&mut theme.favicon);
|
||||
}
|
||||
|
||||
// highlight.js
|
||||
if let Ok(mut f) = File::open(&src.join("highlight.js")) {
|
||||
theme.highlight_js.clear();
|
||||
let _ = f.read_to_end(&mut theme.highlight_js);
|
||||
}
|
||||
|
||||
// highlight.css
|
||||
if let Ok(mut f) = File::open(&src.join("highlight.css")) {
|
||||
theme.highlight_css.clear();
|
||||
let _ = f.read_to_end(&mut theme.highlight_css);
|
||||
}
|
||||
|
||||
// tomorrow-night.css
|
||||
if let Ok(mut f) = File::open(&src.join("tomorrow-night.css")) {
|
||||
theme.tomorrow_night_css.clear();
|
||||
let _ = f.read_to_end(&mut theme.tomorrow_night_css);
|
||||
if let Err(e) = load_file_contents(&filename, dest) {
|
||||
warn!("Couldn't load custom file, {}: {}", filename.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
theme
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
fn default() -> Theme {
|
||||
Theme {
|
||||
index: INDEX.to_owned(),
|
||||
header: HEADER.to_owned(),
|
||||
css: CSS.to_owned(),
|
||||
favicon: FAVICON.to_owned(),
|
||||
js: JS.to_owned(),
|
||||
highlight_css: HIGHLIGHT_CSS.to_owned(),
|
||||
tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
|
||||
ayu_highlight_css: AYU_HIGHLIGHT_CSS.to_owned(),
|
||||
highlight_js: HIGHLIGHT_JS.to_owned(),
|
||||
clipboard_js: CLIPBOARD_JS.to_owned(),
|
||||
store_js: STORE_JS.to_owned(),
|
||||
jquery: JQUERY.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a file exists, if so, the destination buffer will be filled with
|
||||
/// its contents.
|
||||
fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result<()> {
|
||||
let filename = filename.as_ref();
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
File::open(filename)?.read_to_end(&mut buffer)?;
|
||||
|
||||
// We needed the buffer so we'd only overwrite the existing content if we
|
||||
// could successfully load the file into memory.
|
||||
dest.clear();
|
||||
dest.append(&mut buffer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempdir::TempDir;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn theme_uses_defaults_with_nonexistent_src_dir() {
|
||||
let non_existent = PathBuf::from("/non/existent/directory/");
|
||||
assert!(!non_existent.exists());
|
||||
|
||||
let should_be = Theme::default();
|
||||
let got = Theme::new(&non_existent);
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_dir_overrides_defaults() {
|
||||
// Get all the non-Rust files in the theme directory
|
||||
let special_files = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src/theme")
|
||||
.read_dir()
|
||||
.unwrap()
|
||||
.filter_map(|f| f.ok())
|
||||
.map(|f| f.path())
|
||||
.filter(|p| p.is_file() && !p.ends_with(".rs"));
|
||||
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
|
||||
// "touch" all of the special files so we have empty copies
|
||||
for special_file in special_files {
|
||||
let filename = temp.path().join(special_file.file_name().unwrap());
|
||||
let _ = File::create(&filename);
|
||||
}
|
||||
|
||||
let got = Theme::new(temp.path());
|
||||
|
||||
let empty = Theme {
|
||||
index: Vec::new(),
|
||||
header: Vec::new(),
|
||||
css: Vec::new(),
|
||||
favicon: Vec::new(),
|
||||
js: Vec::new(),
|
||||
highlight_css: Vec::new(),
|
||||
tomorrow_night_css: Vec::new(),
|
||||
ayu_highlight_css: Vec::new(),
|
||||
highlight_js: Vec::new(),
|
||||
clipboard_js: Vec::new(),
|
||||
store_js: Vec::new(),
|
||||
jquery: Vec::new(),
|
||||
};
|
||||
|
||||
assert_eq!(got, empty);
|
||||
}
|
||||
}
|
||||
|
||||
41
src/theme/playpen_editor/ace.js
Normal file
41
src/theme/playpen_editor/ace.js
Normal file
File diff suppressed because one or more lines are too long
25
src/theme/playpen_editor/editor.js
Normal file
25
src/theme/playpen_editor/editor.js
Normal file
@@ -0,0 +1,25 @@
|
||||
window.editors = [];
|
||||
(function(editors) {
|
||||
if (typeof(ace) === 'undefined' || !ace) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(".editable").each(function() {
|
||||
let editor = ace.edit(this);
|
||||
editor.setOptions({
|
||||
highlightActiveLine: false,
|
||||
showPrintMargin: false,
|
||||
showLineNumbers: false,
|
||||
showGutter: false,
|
||||
maxLines: Infinity
|
||||
});
|
||||
|
||||
editor.$blockScrolling = Infinity;
|
||||
|
||||
editor.getSession().setMode("ace/mode/rust");
|
||||
|
||||
editor.originalCode = editor.getValue();
|
||||
|
||||
editors.push(editor);
|
||||
});
|
||||
})(window.editors);
|
||||
70
src/theme/playpen_editor/mod.rs
Normal file
70
src/theme/playpen_editor/mod.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::path::Path;
|
||||
|
||||
use theme::load_file_contents;
|
||||
|
||||
pub static JS: &'static [u8] = include_bytes!("editor.js");
|
||||
pub static ACE_JS: &'static [u8] = include_bytes!("ace.js");
|
||||
pub static MODE_RUST_JS: &'static [u8] = include_bytes!("mode-rust.js");
|
||||
pub static THEME_DAWN_JS: &'static [u8] = include_bytes!("theme-dawn.js");
|
||||
pub static THEME_TOMORROW_NIGHT_JS: &'static [u8] = include_bytes!("theme-tomorrow_night.js");
|
||||
|
||||
/// Integration of a JavaScript editor for playpens.
|
||||
/// Uses the Ace editor: https://ace.c9.io/.
|
||||
/// The Ace editor itself, the mode, and the theme files are the
|
||||
/// generated minified no conflict versions.
|
||||
///
|
||||
/// The `PlaypenEditor` struct should be used instead of the static variables because
|
||||
/// the `new()` method
|
||||
/// will look if the user has an editor directory in his source folder and use
|
||||
/// the users editor instead
|
||||
/// of the default.
|
||||
///
|
||||
/// You should exceptionnaly use the static variables only if you need the
|
||||
/// default editor even if the
|
||||
/// user has specified another editor.
|
||||
pub struct PlaypenEditor {
|
||||
pub js: Vec<u8>,
|
||||
pub ace_js: Vec<u8>,
|
||||
pub mode_rust_js: Vec<u8>,
|
||||
pub theme_dawn_js: Vec<u8>,
|
||||
pub theme_tomorrow_night_js: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PlaypenEditor {
|
||||
pub fn new(src: &Path) -> Self {
|
||||
let mut editor = PlaypenEditor {
|
||||
js: JS.to_owned(),
|
||||
ace_js: ACE_JS.to_owned(),
|
||||
mode_rust_js: MODE_RUST_JS.to_owned(),
|
||||
theme_dawn_js: THEME_DAWN_JS.to_owned(),
|
||||
theme_tomorrow_night_js: THEME_TOMORROW_NIGHT_JS.to_owned(),
|
||||
};
|
||||
|
||||
// Check if the given path exists
|
||||
if !src.exists() || !src.is_dir() {
|
||||
return editor;
|
||||
}
|
||||
|
||||
// Check for individual files if they exist
|
||||
{
|
||||
let files = vec![(src.join("editor.js"), &mut editor.js),
|
||||
(src.join("ace.js"), &mut editor.ace_js),
|
||||
(src.join("mode-rust.js"), &mut editor.mode_rust_js),
|
||||
(src.join("theme-dawn.js"), &mut editor.theme_dawn_js),
|
||||
(src.join("theme-tomorrow_night.js"),
|
||||
&mut editor.theme_tomorrow_night_js)];
|
||||
|
||||
for (filename, dest) in files {
|
||||
if !filename.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = load_file_contents(&filename, dest) {
|
||||
warn!("Couldn't load custom file, {}: {}", filename.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor
|
||||
}
|
||||
}
|
||||
1
src/theme/playpen_editor/mode-rust.js
Normal file
1
src/theme/playpen_editor/mode-rust.js
Normal file
File diff suppressed because one or more lines are too long
1
src/theme/playpen_editor/theme-dawn.js
Normal file
1
src/theme/playpen_editor/theme-dawn.js
Normal file
@@ -0,0 +1 @@
|
||||
ace.define("ace/theme/dawn",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-dawn",t.cssText=".ace-dawn .ace_gutter {background: #ebebeb;color: #333}.ace-dawn .ace_print-margin {width: 1px;background: #e8e8e8}.ace-dawn {background-color: #F9F9F9;color: #080808}.ace-dawn .ace_cursor {color: #000000}.ace-dawn .ace_marker-layer .ace_selection {background: rgba(39, 95, 255, 0.30)}.ace-dawn.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #F9F9F9;}.ace-dawn .ace_marker-layer .ace_step {background: rgb(255, 255, 0)}.ace-dawn .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(75, 75, 126, 0.50)}.ace-dawn .ace_marker-layer .ace_active-line {background: rgba(36, 99, 180, 0.12)}.ace-dawn .ace_gutter-active-line {background-color : #dcdcdc}.ace-dawn .ace_marker-layer .ace_selected-word {border: 1px solid rgba(39, 95, 255, 0.30)}.ace-dawn .ace_invisible {color: rgba(75, 75, 126, 0.50)}.ace-dawn .ace_keyword,.ace-dawn .ace_meta {color: #794938}.ace-dawn .ace_constant,.ace-dawn .ace_constant.ace_character,.ace-dawn .ace_constant.ace_character.ace_escape,.ace-dawn .ace_constant.ace_other {color: #811F24}.ace-dawn .ace_invalid.ace_illegal {text-decoration: underline;font-style: italic;color: #F8F8F8;background-color: #B52A1D}.ace-dawn .ace_invalid.ace_deprecated {text-decoration: underline;font-style: italic;color: #B52A1D}.ace-dawn .ace_support {color: #691C97}.ace-dawn .ace_support.ace_constant {color: #B4371F}.ace-dawn .ace_fold {background-color: #794938;border-color: #080808}.ace-dawn .ace_list,.ace-dawn .ace_markup.ace_list,.ace-dawn .ace_support.ace_function {color: #693A17}.ace-dawn .ace_storage {font-style: italic;color: #A71D5D}.ace-dawn .ace_string {color: #0B6125}.ace-dawn .ace_string.ace_regexp {color: #CF5628}.ace-dawn .ace_comment {font-style: italic;color: #5A525F}.ace-dawn .ace_heading,.ace-dawn .ace_markup.ace_heading {color: #19356D}.ace-dawn .ace_variable {color: #234A97}.ace-dawn .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYLh/5+x/AAizA4hxNNsZAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)})
|
||||
1
src/theme/playpen_editor/theme-tomorrow_night.js
Normal file
1
src/theme/playpen_editor/theme-tomorrow_night.js
Normal file
@@ -0,0 +1 @@
|
||||
ace.define("ace/theme/tomorrow_night",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-tomorrow-night",t.cssText=".ace-tomorrow-night .ace_gutter {background: #25282c;color: #C5C8C6}.ace-tomorrow-night .ace_print-margin {width: 1px;background: #25282c}.ace-tomorrow-night {background-color: #1D1F21;color: #C5C8C6}.ace-tomorrow-night .ace_cursor {color: #AEAFAD}.ace-tomorrow-night .ace_marker-layer .ace_selection {background: #373B41}.ace-tomorrow-night.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #1D1F21;}.ace-tomorrow-night .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-tomorrow-night .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #4B4E55}.ace-tomorrow-night .ace_marker-layer .ace_active-line {background: #282A2E}.ace-tomorrow-night .ace_gutter-active-line {background-color: #282A2E}.ace-tomorrow-night .ace_marker-layer .ace_selected-word {border: 1px solid #373B41}.ace-tomorrow-night .ace_invisible {color: #4B4E55}.ace-tomorrow-night .ace_keyword,.ace-tomorrow-night .ace_meta,.ace-tomorrow-night .ace_storage,.ace-tomorrow-night .ace_storage.ace_type,.ace-tomorrow-night .ace_support.ace_type {color: #B294BB}.ace-tomorrow-night .ace_keyword.ace_operator {color: #8ABEB7}.ace-tomorrow-night .ace_constant.ace_character,.ace-tomorrow-night .ace_constant.ace_language,.ace-tomorrow-night .ace_constant.ace_numeric,.ace-tomorrow-night .ace_keyword.ace_other.ace_unit,.ace-tomorrow-night .ace_support.ace_constant,.ace-tomorrow-night .ace_variable.ace_parameter {color: #DE935F}.ace-tomorrow-night .ace_constant.ace_other {color: #CED1CF}.ace-tomorrow-night .ace_invalid {color: #CED2CF;background-color: #DF5F5F}.ace-tomorrow-night .ace_invalid.ace_deprecated {color: #CED2CF;background-color: #B798BF}.ace-tomorrow-night .ace_fold {background-color: #81A2BE;border-color: #C5C8C6}.ace-tomorrow-night .ace_entity.ace_name.ace_function,.ace-tomorrow-night .ace_support.ace_function,.ace-tomorrow-night .ace_variable {color: #81A2BE}.ace-tomorrow-night .ace_support.ace_class,.ace-tomorrow-night .ace_support.ace_type {color: #F0C674}.ace-tomorrow-night .ace_heading,.ace-tomorrow-night .ace_markup.ace_heading,.ace-tomorrow-night .ace_string {color: #B5BD68}.ace-tomorrow-night .ace_entity.ace_name.ace_tag,.ace-tomorrow-night .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow-night .ace_meta.ace_tag,.ace-tomorrow-night .ace_string.ace_regexp,.ace-tomorrow-night .ace_variable {color: #CC6666}.ace-tomorrow-night .ace_comment {color: #969896}.ace-tomorrow-night .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHB3d/8PAAOIAdULw8qMAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)})
|
||||
2
src/theme/store.js
Normal file
2
src/theme/store.js
Normal file
File diff suppressed because one or more lines are too long
@@ -8,3 +8,4 @@
|
||||
@import 'theme-popup'
|
||||
@import 'themes'
|
||||
@import 'print'
|
||||
@import 'tooltip'
|
||||
|
||||
@@ -3,8 +3,13 @@ html, body {
|
||||
color: #333
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Source Code Pro", "Menlo", "DejaVu Sans Mono", monospace;
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
@@ -20,6 +25,10 @@ code {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.play-button.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h2, h3 { margin-top: 2.5em }
|
||||
h4, h5 { margin-top: 2em }
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
text-align: center
|
||||
text-decoration: none
|
||||
|
||||
position: absolute
|
||||
position: fixed
|
||||
top: 50px /* Height of menu-bar */
|
||||
bottom: 0
|
||||
margin: 0
|
||||
@@ -19,5 +19,20 @@
|
||||
|
||||
.mobile-nav-chapters { display: none }
|
||||
.nav-chapters:hover { text-decoration: none }
|
||||
.previous { left: 0 }
|
||||
.next { right: 15px }
|
||||
.previous {
|
||||
left: $sidebar-width + $page-padding
|
||||
transition: left 0.5s
|
||||
|
||||
@media only screen and (max-width: $max-page-width-with-hidden-sidebar) {
|
||||
left: $page-padding
|
||||
}
|
||||
}
|
||||
.next { right: $page-padding }
|
||||
|
||||
.sidebar-hidden .previous {
|
||||
left: $page-padding
|
||||
}
|
||||
|
||||
.sidebar-visible .previous {
|
||||
left: $sidebar-width + $page-padding
|
||||
}
|
||||
|
||||
@@ -1,43 +1,30 @@
|
||||
@require 'variables'
|
||||
|
||||
.page-wrapper {
|
||||
position: absolute
|
||||
overflow-y: auto
|
||||
left: $sidebar-width + 15px
|
||||
right: 0
|
||||
top: 0
|
||||
bottom: 0
|
||||
padding-left: $sidebar-width
|
||||
box-sizing: border-box
|
||||
-webkit-overflow-scrolling: touch
|
||||
|
||||
min-height: 100%
|
||||
|
||||
// Animation: slide away
|
||||
transition: left 0.5s
|
||||
transition: padding-left 0.5s
|
||||
|
||||
@media only screen and (max-width: 1060px) {
|
||||
left: 15px;
|
||||
padding-right: 15px;
|
||||
@media only screen and (max-width: $max-page-width-with-hidden-sidebar) {
|
||||
padding-left: 0
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-hidden .page-wrapper {
|
||||
left: 15px
|
||||
padding-left: 0
|
||||
}
|
||||
|
||||
.sidebar-visible .page-wrapper {
|
||||
left: $sidebar-width + 15px
|
||||
padding-left: $sidebar-width
|
||||
}
|
||||
|
||||
.page {
|
||||
position: absolute
|
||||
top: 0
|
||||
right: 0
|
||||
left: 0
|
||||
bottom: 0
|
||||
|
||||
padding-right: 15px
|
||||
overflow-y: auto
|
||||
outline: 0
|
||||
padding: 0 $page-padding
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
#page-wrapper.page-wrapper {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
#content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
@@ -30,6 +34,10 @@
|
||||
-webkit-print-color-adjust: exact
|
||||
}
|
||||
|
||||
pre > .buttons {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
a, a:visited, a:active, a:hover {
|
||||
color: #4183c4
|
||||
text-decoration: none
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@require 'variables'
|
||||
|
||||
.sidebar {
|
||||
position: absolute
|
||||
position: fixed
|
||||
left: 0
|
||||
top: 0
|
||||
bottom: 0
|
||||
@@ -15,7 +15,7 @@
|
||||
// Animation: slide away
|
||||
transition: left 0.5s
|
||||
|
||||
@media only screen and (max-width: 1060px) {
|
||||
@media only screen and (max-width: $max-page-width-with-hidden-sidebar) {
|
||||
left: - $sidebar-width
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.theme-popup {
|
||||
position: relative
|
||||
position: absolute
|
||||
left: 10px
|
||||
|
||||
z-index: 1000;
|
||||
@@ -12,6 +12,7 @@
|
||||
padding: 2px 10px
|
||||
line-height: 25px
|
||||
white-space: nowrap
|
||||
cursor: pointer
|
||||
|
||||
&:hover:first-child,
|
||||
&:hover:last-child {
|
||||
|
||||
30
src/theme/stylus/themes/ayu.styl
Normal file
30
src/theme/stylus/themes/ayu.styl
Normal file
@@ -0,0 +1,30 @@
|
||||
$theme-name = 'ayu'
|
||||
|
||||
$bg = #0f1419
|
||||
$fg = #c5c5c5
|
||||
|
||||
$sidebar-bg = #14191f
|
||||
$sidebar-fg = #c8c9db
|
||||
$sidebar-non-existant = #5c6773
|
||||
$sidebar-active = #ffb454
|
||||
$sidebar-spacer = #2d334f
|
||||
|
||||
$icons = #737480
|
||||
$icons-hover = #b7b9cc
|
||||
|
||||
$links = #0096cf
|
||||
|
||||
$inline-code-color = #ffb454
|
||||
|
||||
$theme-popup-bg = #14191f
|
||||
$theme-popup-border = #5c6773
|
||||
$theme-hover = #191f26
|
||||
|
||||
$quote-bg = #262933
|
||||
$quote-border = lighten($quote-bg, 5%)
|
||||
|
||||
$table-border-color = lighten($bg, 5%)
|
||||
$table-header-bg = lighten($bg, 20%)
|
||||
$table-alternate-bg = lighten($bg, 3%)
|
||||
|
||||
@import 'base'
|
||||
@@ -38,7 +38,8 @@
|
||||
.nav-chapters,
|
||||
.nav-chapters:visited,
|
||||
.mobile-nav-chapters,
|
||||
.mobile-nav-chapters:visited {
|
||||
.mobile-nav-chapters:visited,
|
||||
.menu-bar a i {
|
||||
color: $icons
|
||||
}
|
||||
|
||||
@@ -56,7 +57,7 @@
|
||||
background-color: $sidebar-bg
|
||||
}
|
||||
|
||||
.content a:link, a:visited {
|
||||
.content a:link, a:visited, a > .hljs {
|
||||
color: $links
|
||||
}
|
||||
|
||||
@@ -105,6 +106,11 @@
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
color: $inline-code-color;
|
||||
}
|
||||
|
||||
a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
pre {
|
||||
@@ -112,6 +118,7 @@
|
||||
|
||||
& > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ $icons-hover = #b3c0cc
|
||||
|
||||
$links = #2b79a2
|
||||
|
||||
$inline-code-color = #c5c8c6;
|
||||
|
||||
$theme-popup-bg = #141617
|
||||
$theme-popup-border = #43484d
|
||||
$theme-hover = #1f2124
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
@import 'coal'
|
||||
@import 'navy'
|
||||
@import 'rust'
|
||||
@import 'ayu'
|
||||
|
||||
@@ -14,6 +14,8 @@ $icons-hover = #333333
|
||||
|
||||
$links = #4183c4
|
||||
|
||||
$inline-code-color = #6e6b5e;
|
||||
|
||||
$theme-popup-bg = #fafafa
|
||||
$theme-popup-border = #cccccc
|
||||
$theme-hover = #e6e6e6
|
||||
|
||||
@@ -14,6 +14,8 @@ $icons-hover = #b7b9cc
|
||||
|
||||
$links = #2b79a2
|
||||
|
||||
$inline-code-color = #c5c8c6;
|
||||
|
||||
$theme-popup-bg = #161923
|
||||
$theme-popup-border = #737480
|
||||
$theme-hover = #282e40
|
||||
|
||||
@@ -14,6 +14,8 @@ $icons-hover = #262625
|
||||
|
||||
$links = #2b79a2
|
||||
|
||||
$inline-code-color = #6e6b5e;
|
||||
|
||||
$theme-popup-bg = #e1e1db
|
||||
$theme-popup-border = #b38f6b
|
||||
$theme-hover = #99908a
|
||||
|
||||
18
src/theme/stylus/tooltip.styl
Normal file
18
src/theme/stylus/tooltip.styl
Normal file
@@ -0,0 +1,18 @@
|
||||
.tooltiptext {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
|
||||
left: -8px; /* Half of the width of the icon */
|
||||
top: -35px;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px 8px;
|
||||
margin: 5px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.tooltipped .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
$sidebar-width = 300px
|
||||
$page-padding = 15px
|
||||
$max-page-width-with-hidden-sidebar = 1060px
|
||||
|
||||
131
src/utils/fs.rs
131
src/utils/fs.rs
@@ -1,98 +1,101 @@
|
||||
use std::path::{Path, Component};
|
||||
use std::error::Error;
|
||||
use std::io::{self, Read};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use errors::*;
|
||||
use std::io::Read;
|
||||
use std::fs::{self, File};
|
||||
|
||||
/// Takes a path to a file and try to read the file into a String
|
||||
|
||||
pub fn file_to_string(path: &Path) -> Result<String, Box<Error>> {
|
||||
pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let path = path.as_ref();
|
||||
let mut file = match File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
debug!("[*]: Failed to open {:?}", path);
|
||||
return Err(Box::new(e));
|
||||
},
|
||||
bail!(e);
|
||||
}
|
||||
};
|
||||
|
||||
let mut content = String::new();
|
||||
|
||||
if let Err(e) = file.read_to_string(&mut content) {
|
||||
debug!("[*]: Failed to read {:?}", path);
|
||||
return Err(Box::new(e));
|
||||
bail!(e);
|
||||
}
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Takes a path and returns a path containing just enough `../` to point to the root of the given path.
|
||||
/// Takes a path and returns a path containing just enough `../` to point to
|
||||
/// the root of the given path.
|
||||
///
|
||||
/// This is mostly interesting for a relative path to point back to the directory from where the
|
||||
/// path starts.
|
||||
/// This is mostly interesting for a relative path to point back to the
|
||||
/// directory from where the path starts.
|
||||
///
|
||||
/// ```ignore
|
||||
/// let mut path = Path::new("some/relative/path");
|
||||
///
|
||||
/// println!("{}", path_to_root(&path));
|
||||
/// ```rust
|
||||
/// # extern crate mdbook;
|
||||
/// #
|
||||
/// # use std::path::Path;
|
||||
/// # use mdbook::utils::fs::path_to_root;
|
||||
/// #
|
||||
/// # fn main() {
|
||||
/// let path = Path::new("some/relative/path");
|
||||
/// assert_eq!(path_to_root(path), "../../");
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// **Outputs**
|
||||
///
|
||||
/// ```text
|
||||
/// "../../"
|
||||
/// ```
|
||||
///
|
||||
/// **note:** it's not very fool-proof, if you find a situation where it doesn't return the correct
|
||||
/// path. Consider [submitting a new issue](https://github.com/azerupi/mdBook/issues) or a
|
||||
/// [pull-request](https://github.com/azerupi/mdBook/pulls) to improve it.
|
||||
/// **note:** it's not very fool-proof, if you find a situation where
|
||||
/// it doesn't return the correct path.
|
||||
/// Consider [submitting a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
|
||||
/// or a [pull-request](https://github.com/rust-lang-nursery/mdBook/pulls) to improve it.
|
||||
|
||||
pub fn path_to_root(path: &Path) -> String {
|
||||
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||
debug!("[fn]: path_to_root");
|
||||
// Remove filename and add "../" for every directory
|
||||
|
||||
path.to_path_buf()
|
||||
path.into()
|
||||
.parent()
|
||||
.expect("")
|
||||
.components()
|
||||
.fold(String::new(), |mut s, c| {
|
||||
match c {
|
||||
Component::Normal(_) => s.push_str("../"),
|
||||
_ => {
|
||||
debug!("[*]: Other path component... {:?}", c);
|
||||
},
|
||||
match c {
|
||||
Component::Normal(_) => s.push_str("../"),
|
||||
_ => {
|
||||
debug!("[*]: Other path component... {:?}", c);
|
||||
}
|
||||
s
|
||||
})
|
||||
}
|
||||
s
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// This function creates a file and returns it. But before creating the file it checks every
|
||||
/// directory in the path to see if it exists, and if it does not it will be created.
|
||||
/// This function creates a file and returns it. But before creating the file
|
||||
/// it checks every directory in the path to see if it exists,
|
||||
/// and if it does not it will be created.
|
||||
|
||||
pub fn create_file(path: &Path) -> io::Result<File> {
|
||||
pub fn create_file(path: &Path) -> Result<File> {
|
||||
debug!("[fn]: create_file");
|
||||
|
||||
// Construct path
|
||||
if let Some(p) = path.parent() {
|
||||
debug!("Parent directory is: {:?}", p);
|
||||
|
||||
try!(fs::create_dir_all(p));
|
||||
fs::create_dir_all(p)?;
|
||||
}
|
||||
|
||||
debug!("[*]: Create file: {:?}", path);
|
||||
File::create(path)
|
||||
File::create(path).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Removes all the content of a directory but not the directory itself
|
||||
|
||||
pub fn remove_dir_content(dir: &Path) -> Result<(), Box<Error>> {
|
||||
for item in try!(fs::read_dir(dir)) {
|
||||
pub fn remove_dir_content(dir: &Path) -> Result<()> {
|
||||
for item in fs::read_dir(dir)? {
|
||||
if let Ok(item) = item {
|
||||
let item = item.path();
|
||||
if item.is_dir() {
|
||||
try!(fs::remove_dir_all(item));
|
||||
fs::remove_dir_all(item)?;
|
||||
} else {
|
||||
try!(fs::remove_file(item));
|
||||
fs::remove_file(item)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,20 +104,24 @@ pub fn remove_dir_content(dir: &Path) -> Result<(), Box<Error>> {
|
||||
|
||||
///
|
||||
///
|
||||
/// Copies all files of a directory to another one except the files with the extensions given in the
|
||||
/// `ext_blacklist` array
|
||||
/// Copies all files of a directory to another one except the files
|
||||
/// with the extensions given in the `ext_blacklist` array
|
||||
|
||||
pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blacklist: &[&str]) -> Result<(), Box<Error>> {
|
||||
pub fn copy_files_except_ext(from: &Path,
|
||||
to: &Path,
|
||||
recursive: bool,
|
||||
ext_blacklist: &[&str])
|
||||
-> Result<()> {
|
||||
debug!("[fn] copy_files_except_ext");
|
||||
// Check that from and to are different
|
||||
if from == to {
|
||||
return Ok(());
|
||||
}
|
||||
debug!("[*] Loop");
|
||||
for entry in try!(fs::read_dir(from)) {
|
||||
let entry = try!(entry);
|
||||
for entry in fs::read_dir(from)? {
|
||||
let entry = entry?;
|
||||
debug!("[*] {:?}", entry.path());
|
||||
let metadata = try!(entry.metadata());
|
||||
let metadata = entry.metadata()?;
|
||||
|
||||
// If the entry is a dir and the recursive option is enabled, call itself
|
||||
if metadata.is_dir() && recursive {
|
||||
@@ -125,15 +132,14 @@ pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blackl
|
||||
|
||||
// check if output dir already exists
|
||||
if !to.join(entry.file_name()).exists() {
|
||||
try!(fs::create_dir(&to.join(entry.file_name())));
|
||||
fs::create_dir(&to.join(entry.file_name()))?;
|
||||
}
|
||||
|
||||
try!(copy_files_except_ext(&from.join(entry.file_name()),
|
||||
&to.join(entry.file_name()),
|
||||
true,
|
||||
ext_blacklist));
|
||||
copy_files_except_ext(&from.join(entry.file_name()),
|
||||
&to.join(entry.file_name()),
|
||||
true,
|
||||
ext_blacklist)?;
|
||||
} else if metadata.is_file() {
|
||||
|
||||
// Check if it is in the blacklist
|
||||
if let Some(ext) = entry.path().extension() {
|
||||
if ext_blacklist.contains(&ext.to_str().unwrap()) {
|
||||
@@ -141,13 +147,19 @@ pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blackl
|
||||
}
|
||||
}
|
||||
debug!("[*] creating path for file: {:?}",
|
||||
&to.join(entry.path().file_name().expect("a file should have a file name...")));
|
||||
&to.join(entry.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")));
|
||||
|
||||
info!("[*] Copying file: {:?}\n to {:?}",
|
||||
entry.path(),
|
||||
&to.join(entry.path().file_name().expect("a file should have a file name...")));
|
||||
try!(fs::copy(entry.path(),
|
||||
&to.join(entry.path().file_name().expect("a file should have a file name..."))));
|
||||
&to.join(entry.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")));
|
||||
fs::copy(entry.path(),
|
||||
&to.join(entry.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -206,7 +218,7 @@ mod tests {
|
||||
|
||||
match copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"]) {
|
||||
Err(e) => panic!("Error while executing the function:\n{:?}", e),
|
||||
Ok(_) => {},
|
||||
Ok(_) => {}
|
||||
}
|
||||
|
||||
// Check if the correct files where created
|
||||
@@ -225,6 +237,5 @@ mod tests {
|
||||
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
210
src/utils/mod.rs
210
src/utils/mod.rs
@@ -1,13 +1,15 @@
|
||||
pub mod fs;
|
||||
|
||||
use pulldown_cmark::{Parser, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
|
||||
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
|
||||
OPTION_ENABLE_TABLES};
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
||||
///
|
||||
///
|
||||
/// Wrapper around the pulldown-cmark parser and renderer to render markdown
|
||||
|
||||
pub fn render_markdown(text: &str) -> String {
|
||||
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
||||
let mut s = String::with_capacity(text.len() * 3 / 2);
|
||||
|
||||
let mut opts = Options::empty();
|
||||
@@ -15,6 +17,208 @@ pub fn render_markdown(text: &str) -> String {
|
||||
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
||||
|
||||
let p = Parser::new_ext(text, opts);
|
||||
html::push_html(&mut s, p);
|
||||
let mut converter = EventQuoteConverter::new(curly_quotes);
|
||||
let events = p.map(clean_codeblock_headers)
|
||||
.map(|event| converter.convert(event));
|
||||
|
||||
html::push_html(&mut s, events);
|
||||
s
|
||||
}
|
||||
|
||||
struct EventQuoteConverter {
|
||||
enabled: bool,
|
||||
convert_text: bool,
|
||||
}
|
||||
|
||||
impl EventQuoteConverter {
|
||||
fn new(enabled: bool) -> Self {
|
||||
EventQuoteConverter {
|
||||
enabled: enabled,
|
||||
convert_text: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert<'a>(&mut self, event: Event<'a>) -> Event<'a> {
|
||||
if !self.enabled {
|
||||
return event;
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
|
||||
self.convert_text = false;
|
||||
event
|
||||
}
|
||||
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
|
||||
self.convert_text = true;
|
||||
event
|
||||
}
|
||||
Event::Text(ref text) if self.convert_text => {
|
||||
Event::Text(Cow::from(convert_quotes_to_curly(text)))
|
||||
}
|
||||
_ => event,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clean_codeblock_headers(event: Event) -> Event {
|
||||
match event {
|
||||
Event::Start(Tag::CodeBlock(ref info)) => {
|
||||
let info: String = info.chars().filter(|ch| !ch.is_whitespace()).collect();
|
||||
|
||||
Event::Start(Tag::CodeBlock(Cow::from(info)))
|
||||
}
|
||||
_ => 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 {
|
||||
'’'
|
||||
}
|
||||
}
|
||||
'"' => {
|
||||
if preceded_by_whitespace {
|
||||
'“'
|
||||
} else {
|
||||
'”'
|
||||
}
|
||||
}
|
||||
_ => original_char,
|
||||
};
|
||||
|
||||
preceded_by_whitespace = original_char.is_whitespace();
|
||||
|
||||
converted_char
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod render_markdown {
|
||||
use super::super::render_markdown;
|
||||
|
||||
#[test]
|
||||
fn it_can_keep_quotes_straight() {
|
||||
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_make_quotes_curly_except_when_they_are_in_code() {
|
||||
let input = r#"
|
||||
'one'
|
||||
```
|
||||
'two'
|
||||
```
|
||||
`'three'` 'four'"#;
|
||||
let expected = r#"<p>‘one’</p>
|
||||
<pre><code>'two'
|
||||
</code></pre>
|
||||
<p><code>'three'</code> ‘four’</p>
|
||||
"#;
|
||||
assert_eq!(render_markdown(input, true), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_outside_of_codeblock_header_is_preserved() {
|
||||
let input = r#"
|
||||
some text with spaces
|
||||
```rust
|
||||
fn main() {
|
||||
// code inside is unchanged
|
||||
}
|
||||
```
|
||||
more text with spaces
|
||||
"#;
|
||||
|
||||
let expected = r#"<p>some text with spaces</p>
|
||||
<pre><code class="language-rust">fn main() {
|
||||
// code inside is unchanged
|
||||
}
|
||||
</code></pre>
|
||||
<p>more text with spaces</p>
|
||||
"#;
|
||||
assert_eq!(render_markdown(input, false), expected);
|
||||
assert_eq!(render_markdown(input, true), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_code_block_properties_are_passed_as_space_delimited_class() {
|
||||
let input = r#"
|
||||
```rust,no_run,should_panic,property_3
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected =
|
||||
r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre>
|
||||
"#;
|
||||
assert_eq!(render_markdown(input, false), expected);
|
||||
assert_eq!(render_markdown(input, true), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_code_block_properties_with_whitespace_are_passed_as_space_delimited_class() {
|
||||
let input = r#"
|
||||
```rust, no_run,,,should_panic , ,property_3
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected =
|
||||
r#"<pre><code class="language-rust,no_run,,,should_panic,,property_3"></code></pre>
|
||||
"#;
|
||||
assert_eq!(render_markdown(input, false), expected);
|
||||
assert_eq!(render_markdown(input, true), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_code_block_without_properties_has_proper_html_class() {
|
||||
let input = r#"
|
||||
```rust
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r#"<pre><code class="language-rust"></code></pre>
|
||||
"#;
|
||||
assert_eq!(render_markdown(input, false), expected);
|
||||
assert_eq!(render_markdown(input, true), expected);
|
||||
|
||||
let input = r#"
|
||||
```rust
|
||||
```
|
||||
"#;
|
||||
assert_eq!(render_markdown(input, false), expected);
|
||||
assert_eq!(render_markdown(input, true), expected);
|
||||
}
|
||||
}
|
||||
|
||||
mod convert_quotes_to_curly {
|
||||
use super::super::convert_quotes_to_curly;
|
||||
|
||||
#[test]
|
||||
fn it_converts_single_quotes() {
|
||||
assert_eq!(convert_quotes_to_curly("'one', 'two'"),
|
||||
"‘one’, ‘two’");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_converts_double_quotes() {
|
||||
assert_eq!(convert_quotes_to_curly(r#""one", "two""#),
|
||||
"“one”, “two”");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_treats_tab_as_whitespace() {
|
||||
assert_eq!(convert_quotes_to_curly("\t'one'"), "\t‘one’");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
116
tests/dummy_book/mod.rs
Normal file
116
tests/dummy_book/mod.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
//! This will create an entire book in a temporary directory using some
|
||||
//! dummy contents from the `tests/dummy-book/` directory.
|
||||
|
||||
// Not all features are used in all test crates, so...
|
||||
#![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
|
||||
extern crate mdbook;
|
||||
extern crate tempdir;
|
||||
extern crate 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;
|
||||
|
||||
// The funny `self::` here is because we've got an `extern crate ...` and are
|
||||
// in a submodule
|
||||
use self::tempdir::TempDir;
|
||||
use self::mdbook::MDBook;
|
||||
use self::walkdir::WalkDir;
|
||||
|
||||
|
||||
/// Create a dummy book in a temporary directory, using the contents of
|
||||
/// `SUMMARY_MD` as a guide.
|
||||
///
|
||||
/// The "Nested Chapter" file contains a code block with a single
|
||||
/// `assert!($TEST_STATUS)`. If you want to check MDBook's testing
|
||||
/// functionality, `$TEST_STATUS` can be substitute for either `true` or
|
||||
/// `false`. This is done using the `passing_test` parameter.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DummyBook {
|
||||
passing_test: bool,
|
||||
}
|
||||
|
||||
impl DummyBook {
|
||||
/// Create a new `DummyBook` with all the defaults.
|
||||
pub fn new() -> DummyBook {
|
||||
DummyBook { passing_test: true }
|
||||
}
|
||||
|
||||
/// Whether the doc-test included in the "Nested Chapter" should pass or
|
||||
/// fail (it passes by default).
|
||||
pub fn with_passing_test(&mut self, test_passes: bool) -> &mut DummyBook {
|
||||
self.passing_test = test_passes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Write a book to a temporary directory using the provided settings.
|
||||
pub fn build(&self) -> Result<TempDir> {
|
||||
let temp = TempDir::new("dummy_book").chain_err(|| "Unable to create temp directory")?;
|
||||
|
||||
let 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"
|
||||
})?;
|
||||
|
||||
let sub_pattern = if self.passing_test { "true" } else { "false" };
|
||||
let file_containing_test = temp.path().join("src/first/nested.md");
|
||||
replace_pattern_in_file(&file_containing_test, "$TEST_STATUS", sub_pattern)?;
|
||||
|
||||
Ok(temp)
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()> {
|
||||
let contents = file_to_string(filename)?;
|
||||
File::create(filename)?.write_all(contents.replace(from, to).as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read the contents of the provided file into memory and then iterate through
|
||||
/// the list of strings asserting that the file contains all of them.
|
||||
pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
|
||||
let filename = filename.as_ref();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 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();
|
||||
let to = to.as_ref();
|
||||
|
||||
for entry in WalkDir::new(&from) {
|
||||
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 new_location = to.join(relative);
|
||||
|
||||
if original_location.is_file() {
|
||||
if let Some(parent) = new_location.parent() {
|
||||
fs::create_dir_all(parent).chain_err(|| "Couldn't create directory")?;
|
||||
}
|
||||
|
||||
fs::copy(&original_location, &new_location).chain_err(|| {
|
||||
"Unable to copy file contents"
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
10
tests/dummy_book/src/SUMMARY.md
Normal file
10
tests/dummy_book/src/SUMMARY.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Summary
|
||||
|
||||
[Introduction](intro.md)
|
||||
|
||||
- [First Chapter](./first/index.md)
|
||||
- [Nested Chapter](./first/nested.md)
|
||||
---
|
||||
- [Second Chapter](./second.md)
|
||||
|
||||
[Conclusion](./conclusion.md)
|
||||
1
tests/dummy_book/src/conclusion.md
Normal file
1
tests/dummy_book/src/conclusion.md
Normal file
@@ -0,0 +1 @@
|
||||
# Conclusion
|
||||
5
tests/dummy_book/src/first/index.md
Normal file
5
tests/dummy_book/src/first/index.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# First Chapter
|
||||
|
||||
more text.
|
||||
|
||||
## Some Section
|
||||
9
tests/dummy_book/src/first/nested.md
Normal file
9
tests/dummy_book/src/first/nested.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Nested Chapter
|
||||
|
||||
This file has some testable code.
|
||||
|
||||
```rust
|
||||
assert!($TEST_STATUS);
|
||||
```
|
||||
|
||||
## Some Section
|
||||
3
tests/dummy_book/src/intro.md
Normal file
3
tests/dummy_book/src/intro.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Introduction
|
||||
|
||||
Here's some interesting text...
|
||||
1
tests/dummy_book/src/second.md
Normal file
1
tests/dummy_book/src/second.md
Normal file
@@ -0,0 +1 @@
|
||||
# Second Chapter
|
||||
64
tests/init.rs
Normal file
64
tests/init.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
extern crate mdbook;
|
||||
extern crate tempdir;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use mdbook::MDBook;
|
||||
use tempdir::TempDir;
|
||||
|
||||
|
||||
/// Run `mdbook init` in an empty directory and make sure the default files
|
||||
/// are created.
|
||||
#[test]
|
||||
fn base_mdbook_init_should_create_default_content() {
|
||||
let created_files = vec!["book", "src", "src/SUMMARY.md", "src/chapter_1.md"];
|
||||
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
for file in &created_files {
|
||||
assert!(!temp.path().join(file).exists());
|
||||
}
|
||||
|
||||
let mut md = MDBook::new(temp.path());
|
||||
md.init().unwrap();
|
||||
|
||||
for file in &created_files {
|
||||
let target = temp.path().join(file);
|
||||
println!("{}", target.display());
|
||||
assert!(target.exists(), "{} doesn't exist", file);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set some custom arguments for where to place the source and destination
|
||||
/// files, then call `mdbook init`.
|
||||
#[test]
|
||||
fn run_mdbook_init_with_custom_book_and_src_locations() {
|
||||
let created_files = vec!["out", "in", "in/SUMMARY.md", "in/chapter_1.md"];
|
||||
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
for file in &created_files {
|
||||
assert!(!temp.path().join(file).exists(),
|
||||
"{} shouldn't exist yet!",
|
||||
file);
|
||||
}
|
||||
|
||||
let mut md = MDBook::new(temp.path());
|
||||
md.config.book.src = PathBuf::from("in");
|
||||
md.config.build.build_dir = PathBuf::from("out");
|
||||
|
||||
md.init().unwrap();
|
||||
|
||||
for file in &created_files {
|
||||
let target = temp.path().join(file);
|
||||
assert!(target.exists(), "{} should have been created by `mdbook init`", file);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn book_toml_isnt_required() {
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
md.init().unwrap();
|
||||
|
||||
assert!(!temp.path().join("book.toml").exists());
|
||||
|
||||
md.read_config().unwrap().build().unwrap();
|
||||
}
|
||||
280
tests/rendered_output.rs
Normal file
280
tests/rendered_output.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
extern crate mdbook;
|
||||
#[macro_use]
|
||||
extern crate pretty_assertions;
|
||||
extern crate select;
|
||||
extern crate tempdir;
|
||||
extern crate walkdir;
|
||||
|
||||
mod dummy_book;
|
||||
|
||||
use dummy_book::{assert_contains_strings, DummyBook};
|
||||
|
||||
use std::fs::{File, remove_file};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::ffi::OsStr;
|
||||
use walkdir::{DirEntry, WalkDir, WalkDirIterator};
|
||||
use select::document::Document;
|
||||
use select::predicate::{Class, Name, Predicate};
|
||||
use tempdir::TempDir;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils::fs::file_to_string;
|
||||
use mdbook::MDBook;
|
||||
|
||||
|
||||
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",
|
||||
"Introduction"];
|
||||
const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter"];
|
||||
|
||||
/// Make sure you can load the dummy book and build it without panicking.
|
||||
#[test]
|
||||
fn build_the_dummy_book() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
|
||||
assert!(!temp.path().join("book").exists());
|
||||
md.build().unwrap();
|
||||
|
||||
assert!(temp.path().join("book").exists());
|
||||
assert!(temp.path().join("book").join("index.html").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_sure_bottom_level_files_contain_links_to_chapters() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
md.build().unwrap();
|
||||
|
||||
let dest = temp.path().join("book");
|
||||
let links = vec![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_bottom_dir = vec!["index.html", "intro.html", "second.html", "conclusion.html"];
|
||||
|
||||
for filename in files_in_bottom_dir {
|
||||
assert_contains_strings(dest.join(filename), &links);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_correct_cross_links_in_nested_dir() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
md.build().unwrap();
|
||||
|
||||
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""#];
|
||||
|
||||
let files_in_nested_dir = vec!["index.html", "nested.html"];
|
||||
|
||||
for filename in files_in_nested_dir {
|
||||
assert_contains_strings(first.join(filename), &links);
|
||||
}
|
||||
|
||||
assert_contains_strings(first.join("index.html"),
|
||||
&[r##"href="./first/index.html#some-section" id="some-section""##]);
|
||||
|
||||
assert_contains_strings(first.join("nested.html"),
|
||||
&[r##"href="./first/nested.html#some-section" id="some-section""##]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rendered_code_has_playpen_stuff() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
md.build().unwrap();
|
||||
|
||||
let nested = temp.path().join("book/first/nested.html");
|
||||
let playpen_class = vec![r#"class="playpen""#];
|
||||
|
||||
assert_contains_strings(nested, &playpen_class);
|
||||
|
||||
let book_js = temp.path().join("book/book.js");
|
||||
assert_contains_strings(book_js, &[".playpen"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chapter_content_appears_in_rendered_document() {
|
||||
let content = vec![("index.html", "Here's some interesting text"),
|
||||
("second.html", "Second Chapter"),
|
||||
("first/nested.html", "testable code"),
|
||||
("first/index.html", "more text"),
|
||||
("conclusion.html", "Conclusion")];
|
||||
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
md.build().unwrap();
|
||||
|
||||
let destination = temp.path().join("book");
|
||||
|
||||
for (filename, text) in content {
|
||||
let path = destination.join(filename);
|
||||
assert_contains_strings(path, &[text]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Apply a series of predicates to some root predicate, where each
|
||||
/// successive predicate is the descendant of the last one. Similar to how you
|
||||
/// might do `ul.foo li a` in CSS to access all anchor tags in the `foo` list.
|
||||
macro_rules! descendants {
|
||||
($root:expr, $($child:expr),*) => {
|
||||
$root
|
||||
$(
|
||||
.descendant($child)
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// Make sure that all `*.md` files (excluding `SUMMARY.md`) were rendered
|
||||
/// and placed in the `book` directory with their extensions set to `*.html`.
|
||||
#[test]
|
||||
fn chapter_files_were_rendered_to_html() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let src = Path::new(BOOK_ROOT).join("src");
|
||||
|
||||
let chapter_files = WalkDir::new(&src).into_iter()
|
||||
.filter_entry(|entry| entry_ends_with(entry, ".md"))
|
||||
.filter_map(|entry| entry.ok())
|
||||
.map(|entry| entry.path().to_path_buf())
|
||||
.filter(|path| {
|
||||
path.file_name().and_then(OsStr::to_str)
|
||||
!= Some("SUMMARY.md")
|
||||
});
|
||||
|
||||
for chapter in chapter_files {
|
||||
let rendered_location = temp.path().join(chapter.strip_prefix(&src).unwrap())
|
||||
.with_extension("html");
|
||||
assert!(rendered_location.exists(),
|
||||
"{} doesn't exits",
|
||||
rendered_location.display());
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_ends_with(entry: &DirEntry, ending: &str) -> bool {
|
||||
entry.file_name().to_string_lossy().ends_with(ending)
|
||||
}
|
||||
|
||||
/// Read the main page (`book/index.html`) and expose it as a DOM which we
|
||||
/// can search with the `select` crate
|
||||
fn root_index_html() -> Result<Document> {
|
||||
let temp = DummyBook::new().build()
|
||||
.chain_err(|| "Couldn't create the dummy book")?;
|
||||
MDBook::new(temp.path()).build()
|
||||
.chain_err(|| "Book building failed")?;
|
||||
|
||||
let index_page = temp.path().join("book").join("index.html");
|
||||
let html = file_to_string(&index_page).chain_err(|| "Unable to read index.html")?;
|
||||
|
||||
Ok(Document::from(html.as_str()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_second_toc_level() {
|
||||
let doc = root_index_html().unwrap();
|
||||
let mut should_be = Vec::from(TOC_SECOND_LEVEL);
|
||||
should_be.sort();
|
||||
|
||||
let pred = descendants!(Class("chapter"), Name("li"), Name("li"), Name("a"));
|
||||
|
||||
let mut children_of_children: Vec<_> =
|
||||
doc.find(pred).map(|elem| elem.text().trim().to_string())
|
||||
.collect();
|
||||
children_of_children.sort();
|
||||
|
||||
assert_eq!(children_of_children, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_first_toc_level() {
|
||||
let doc = root_index_html().unwrap();
|
||||
let mut should_be = Vec::from(TOC_TOP_LEVEL);
|
||||
|
||||
should_be.extend(TOC_SECOND_LEVEL);
|
||||
should_be.sort();
|
||||
|
||||
let pred = descendants!(Class("chapter"), Name("li"), Name("a"));
|
||||
|
||||
let mut children: Vec<_> = doc.find(pred).map(|elem| elem.text().trim().to_string())
|
||||
.collect();
|
||||
children.sort();
|
||||
|
||||
assert_eq!(children, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_spacers() {
|
||||
let doc = root_index_html().unwrap();
|
||||
let should_be = 1;
|
||||
|
||||
let num_spacers =
|
||||
doc.find(Class("chapter").descendant(Name("li").and(Class("spacer")))).count();
|
||||
assert_eq!(num_spacers, should_be);
|
||||
}
|
||||
|
||||
/// Ensure building fails if `create-missing` is false and one of the files does
|
||||
/// not exist.
|
||||
#[test]
|
||||
fn failure_on_missing_file() {
|
||||
let (md, _temp) = create_missing_setup(Some(false));
|
||||
|
||||
// On failure, `build()` does not return a specific error, so assume
|
||||
// any error is a failure due to a missing file.
|
||||
assert!(md.read_config().unwrap().build().is_err());
|
||||
}
|
||||
|
||||
/// Ensure a missing file is created if `create-missing` is true.
|
||||
#[test]
|
||||
fn create_missing_file_with_config() {
|
||||
let (md, temp) = create_missing_setup(Some(true));
|
||||
|
||||
md.read_config().unwrap().build().unwrap();
|
||||
assert!(temp.path().join("src").join("intro.md").exists());
|
||||
}
|
||||
|
||||
/// Ensure a missing file is created if `create-missing` is not set (the default
|
||||
/// is true).
|
||||
#[test]
|
||||
fn create_missing_file_without_config() {
|
||||
let (md, temp) = create_missing_setup(None);
|
||||
|
||||
md.read_config().unwrap().build().unwrap();
|
||||
assert!(temp.path().join("src").join("intro.md").exists());
|
||||
}
|
||||
|
||||
fn create_missing_setup(create_missing: Option<bool>) -> (MDBook, TempDir) {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let md = MDBook::new(temp.path());
|
||||
|
||||
let mut file = File::create(temp.path().join("book.toml")).unwrap();
|
||||
match create_missing {
|
||||
Some(true) => file.write_all(b"[build]\ncreate-missing = true\n").unwrap(),
|
||||
Some(false) => file.write_all(b"[build]\ncreate-missing = false\n").unwrap(),
|
||||
None => (),
|
||||
}
|
||||
file.flush().unwrap();
|
||||
|
||||
remove_file(temp.path().join("src").join("intro.md")).unwrap();
|
||||
|
||||
(md, temp)
|
||||
}
|
||||
23
tests/testing.rs
Normal file
23
tests/testing.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
extern crate mdbook;
|
||||
|
||||
mod dummy_book;
|
||||
|
||||
use dummy_book::DummyBook;
|
||||
use mdbook::MDBook;
|
||||
|
||||
|
||||
#[test]
|
||||
fn mdbook_can_correctly_test_a_passing_book() {
|
||||
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
|
||||
assert!(md.test(vec![]).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mdbook_detects_book_with_failing_tests() {
|
||||
let temp = DummyBook::new().with_passing_test(false).build().unwrap();
|
||||
let mut md: MDBook = MDBook::new(temp.path());
|
||||
|
||||
assert!(md.test(vec![]).is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user