mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 13:51:10 -05:00
Compare commits
449 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d94c097495 | ||
|
|
b7372d3bf2 | ||
|
|
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 | ||
|
|
69188445e7 | ||
|
|
9b7a26effd | ||
|
|
4f4120b5a4 | ||
|
|
607bf4426e | ||
|
|
8b84b8fa82 | ||
|
|
a4a708bdda | ||
|
|
a3b925e3ab | ||
|
|
cba988f009 | ||
|
|
4525810737 | ||
|
|
5d72d966ad | ||
|
|
15dcca87d8 | ||
|
|
c6e81337fb | ||
|
|
9602acce80 | ||
|
|
65d7e86024 | ||
|
|
b5ec813d2f | ||
|
|
41735b4579 | ||
|
|
d24ad83a5c | ||
|
|
9cb232058b | ||
|
|
ef402c16e8 | ||
|
|
df5472ab5a | ||
|
|
d768963c30 | ||
|
|
8e7ec6e1fd | ||
|
|
80f01d70c6 | ||
|
|
40f275bf21 | ||
|
|
af8300c0b4 | ||
|
|
793a88260c | ||
|
|
1ec776244d | ||
|
|
4af107b0ca | ||
|
|
35e2807138 | ||
|
|
1632d2e339 | ||
|
|
7c3932cef9 | ||
|
|
ed1a216121 | ||
|
|
f814e96459 | ||
|
|
980ea5796e | ||
|
|
8500d1c8a7 | ||
|
|
a7272e0ff5 | ||
|
|
1cf4774737 | ||
|
|
c6a5d12002 | ||
|
|
b120ce7397 | ||
|
|
c7916c4818 | ||
|
|
56f597b90c | ||
|
|
c5f9625feb | ||
|
|
79f00eeea3 | ||
|
|
677fa42458 | ||
|
|
a8bba0b94d | ||
|
|
e5a973a18d | ||
|
|
e218257e42 | ||
|
|
1345c05b18 | ||
|
|
5e3a3f3482 | ||
|
|
7f46071faa | ||
|
|
5674da2afb | ||
|
|
01341a7705 | ||
|
|
0c624d0f74 | ||
|
|
58cfef00f2 | ||
|
|
6af3eea24b | ||
|
|
c88656284c | ||
|
|
14a28080c1 | ||
|
|
7fa36f82b0 | ||
|
|
3a30e65eef | ||
|
|
fab24f5224 | ||
|
|
cfa4295d79 | ||
|
|
d7f38d08fd | ||
|
|
864be6cf42 | ||
|
|
ec42e2f771 | ||
|
|
aba153a271 | ||
|
|
280dabecd7 | ||
|
|
38b3516b60 | ||
|
|
d609988264 | ||
|
|
95fd292b4f | ||
|
|
f3fb1f1e16 | ||
|
|
152ebba762 | ||
|
|
23d25c853e | ||
|
|
b97a8205f6 | ||
|
|
82faec6b5a | ||
|
|
32814f6f71 | ||
|
|
ac6f15cb27 | ||
|
|
0d6185ac96 | ||
|
|
4b31ae6789 | ||
|
|
1afa2debc1 | ||
|
|
3a71371946 | ||
|
|
9a318adc03 | ||
|
|
c7b4147ba7 | ||
|
|
1ac2602360 | ||
|
|
09729aaca5 | ||
|
|
3ffd24df63 | ||
|
|
fe8d46b8e6 | ||
|
|
21bc3d47c8 | ||
|
|
f2b87f7944 | ||
|
|
894a03655e | ||
|
|
6b2572e78d | ||
|
|
fe287a1eca | ||
|
|
375502a6fa | ||
|
|
a6e1844aad | ||
|
|
d92852867b | ||
|
|
0f0750df52 | ||
|
|
712adcf737 | ||
|
|
3a0cfc87df | ||
|
|
b1e384b03b | ||
|
|
6410e792d7 | ||
|
|
b75243f1f5 | ||
|
|
f1df53a4bb | ||
|
|
8a178e311d | ||
|
|
53ec61ac70 | ||
|
|
97d46e79b7 | ||
|
|
552e39c897 | ||
|
|
791487bc84 | ||
|
|
f67ae7c71a | ||
|
|
e53dcdcf4d | ||
|
|
85d8e2ebd3 | ||
|
|
a9e5dc63f1 | ||
|
|
cf35e08abc | ||
|
|
c986b3afc4 | ||
|
|
08b5d14f7e | ||
|
|
f9101ca62c | ||
|
|
f0c0d71326 | ||
|
|
d2f3eb5007 | ||
|
|
67aee5c192 | ||
|
|
30eb85711e | ||
|
|
b0d33e76ec | ||
|
|
eb65f3fd1e | ||
|
|
2600c62cf9 | ||
|
|
ecae442d25 | ||
|
|
f26f41fde3 | ||
|
|
b91f817bfd | ||
|
|
528945d67d | ||
|
|
4852e9e65a | ||
|
|
e54b6643e1 | ||
|
|
c7a95ccb8b | ||
|
|
81a8f946b7 | ||
|
|
04a643805a | ||
|
|
49608b560b | ||
|
|
9e634a4e83 | ||
|
|
c2c721025d | ||
|
|
2dfc25fc6e | ||
|
|
8f8893bab2 | ||
|
|
4153db2624 | ||
|
|
db11ff27f4 | ||
|
|
b584f6eb9c | ||
|
|
a7ae0b99c4 | ||
|
|
9732a3bc7d | ||
|
|
f3f9c93765 | ||
|
|
a0d8013242 | ||
|
|
1b9d55bcd5 | ||
|
|
a459a3606e | ||
|
|
1e6bccd924 | ||
|
|
6d77b7fd83 | ||
|
|
f9ea6135c3 | ||
|
|
317023cd0e | ||
|
|
0b88b043d0 | ||
|
|
ac725cb39d | ||
|
|
db0306a6d2 | ||
|
|
02c5c971e7 | ||
|
|
5350d62591 | ||
|
|
9c8a563223 | ||
|
|
b4948b680f | ||
|
|
b6df992420 | ||
|
|
b0e5f375ba | ||
|
|
a4a277cb50 | ||
|
|
b9e22bb8f2 | ||
|
|
ab29e92071 | ||
|
|
03373c6bf2 | ||
|
|
425b583625 | ||
|
|
dfef0d7585 | ||
|
|
9b49acc2c9 | ||
|
|
1ae0d4f637 | ||
|
|
f9aa9a6843 | ||
|
|
9b1e224680 | ||
|
|
cfcf6d952f | ||
|
|
e3f398cff2 | ||
|
|
6bc088db6e | ||
|
|
e34bef0e53 | ||
|
|
15d6227a11 | ||
|
|
1b8af2bf57 | ||
|
|
7f34512751 | ||
|
|
876ea7895a | ||
|
|
bea80f6266 | ||
|
|
334540835c | ||
|
|
10e7a41d92 | ||
|
|
2ec5648587 | ||
|
|
6aa6546ce4 | ||
|
|
c071406fef | ||
|
|
c8051294b0 | ||
|
|
807a2f116e | ||
|
|
2f43167b75 | ||
|
|
e861880f95 | ||
|
|
c3564f1699 | ||
|
|
15d26befcc | ||
|
|
925939e267 | ||
|
|
ceb139a848 | ||
|
|
d0e39f469a | ||
|
|
c5752620d7 | ||
|
|
0c93599242 | ||
|
|
7f3a6c8130 | ||
|
|
b30a8bdc81 | ||
|
|
74fff81e4b | ||
|
|
ad0794a0bd | ||
|
|
6bac41caa8 | ||
|
|
b094268b68 | ||
|
|
02a37e0ee9 | ||
|
|
9e34eccb3e | ||
|
|
79fb92ed7c | ||
|
|
5e78697ab1 | ||
|
|
469cb10d4a | ||
|
|
0f9caf4410 | ||
|
|
f23a5f2729 | ||
|
|
bc41efe414 | ||
|
|
5316089e61 | ||
|
|
73ce3f814a | ||
|
|
075da959c9 | ||
|
|
80deac90d9 | ||
|
|
625f5081fa | ||
|
|
1eb59428e6 | ||
|
|
3e8151e8e3 | ||
|
|
3c10a85735 | ||
|
|
330b1ad55d | ||
|
|
596455f28c | ||
|
|
f24eb59753 | ||
|
|
01c5085725 | ||
|
|
9f17be2c32 | ||
|
|
88fabd76f0 | ||
|
|
f508db6113 | ||
|
|
1083d1822d | ||
|
|
fc86b963bb | ||
|
|
f2b913c9dd | ||
|
|
dd0cfc14d4 | ||
|
|
5891e4b5db | ||
|
|
394023f617 | ||
|
|
39a6fe4b3c | ||
|
|
75b98d7019 | ||
|
|
814b21ad94 | ||
|
|
7364d41f0c | ||
|
|
f6be4a7d7e | ||
|
|
0b00c270d5 | ||
|
|
8d2ca521c0 | ||
|
|
276cd8d490 | ||
|
|
e958fc8605 | ||
|
|
e286b208da | ||
|
|
3fd1d4606c | ||
|
|
78b6148463 | ||
|
|
78e1897b47 | ||
|
|
d000fc8bac | ||
|
|
5170e6b675 | ||
|
|
a7f329d337 | ||
|
|
bb0c878e06 | ||
|
|
2a7463c45b | ||
|
|
db7424e947 | ||
|
|
0ac0301d72 | ||
|
|
38b2dee17e | ||
|
|
0cb234de5d | ||
|
|
ee4a7fb35c | ||
|
|
9c5c8a6804 |
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[attr]rust text eol=lf whitespace=tab-in-indent,trailing-space,tabwidth=4
|
||||
|
||||
* text=auto eol=lf
|
||||
*.rs rust
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,8 @@
|
||||
Cargo.lock
|
||||
target
|
||||
|
||||
# MacOS temp file
|
||||
.DS_Store
|
||||
|
||||
book-test
|
||||
book-example/book
|
||||
|
||||
124
.travis.yml
124
.travis.yml
@@ -1,20 +1,112 @@
|
||||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
matrix:
|
||||
allow_failure:
|
||||
- rust: nightly
|
||||
sudo: false
|
||||
|
||||
language: generic
|
||||
|
||||
env:
|
||||
global:
|
||||
secure: l3/qEC4krRerllLQzni8j5AjngFi6pluWvBWj//1mJLoIEYwxlQ9mYxEdd9BqccWWFn3K0bVYCVC/64+tP6sRfLkZCe2gPUtwe7ITwCDbapUxmkiRObVJCs5yMQZt6idyhHUDKAXKgNCrusfI2BM3tKGBfRK7Cnn/R/7p/U9+q7D1sgJtUKp6ypVzK6A3jLNp3dFLFI19a5KmbZMVsaa7tOhtdDJjjr7ebsc9z7HMW5/OItiWU3FSauVQQlUMaCiEgFuIG7H7OnBAYWB/gNEtLuwfLqU9UjtWk/njNNRnmJ7m3y5HbQhv5H5F5mJUOq9XFlPLwPwyTeVztSGdQm6k8Pp2pgKBUjY27afBl9BWU+msmN6k0oXfhvIebiBPe/x2udiKeFik1xqOOEU1q9dF0sZiuPxCSM1n7tgWklJ8epgaRQaMPPQw9pO/2H5/ynHCJqBlw6WcdiqWtwAyyr/GEx62u/cg5IVkqb7KLmYsWzjS8wYG4CYs1eIxCw2xPZxP0FGuUXvxTBUPipFze6Z7FqxVauXtVe2D7c1P4738HZP660rmR0GYtHtKLny1QxCCK9sxd9JmcezFCSz4YeQ1od9xc0OzGJ2ullKNGizmGfYmgL6X8faNylLIEdaiHAcY16xV3L0g3fXL1Qg360UHQyj7GIv+0nqQnf+H9xRTTU=
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- nodejs
|
||||
- npm
|
||||
- PROJECT_NAME=mdBook
|
||||
|
||||
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
|
||||
dist: trusty
|
||||
addons:
|
||||
apt:
|
||||
packages: &musl_packages
|
||||
- musl
|
||||
- musl-dev
|
||||
- musl-tools
|
||||
# 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
|
||||
dist: trusty
|
||||
addons:
|
||||
apt:
|
||||
packages: &musl_packages
|
||||
- musl
|
||||
- musl-dev
|
||||
- musl-tools
|
||||
# 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
|
||||
dist: trusty
|
||||
addons:
|
||||
apt:
|
||||
packages: &musl_packages
|
||||
- musl
|
||||
- musl-dev
|
||||
- musl-tools
|
||||
|
||||
install:
|
||||
- npm install stylus nib
|
||||
- export PATH="$PATH:$HOME/.cargo/bin"
|
||||
- bash ci/install.sh
|
||||
|
||||
script:
|
||||
- bash ci/script.sh
|
||||
|
||||
after_success:
|
||||
- test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && bash deploy.sh
|
||||
- test "$TRAVIS_PULL_REQUEST" == "false" &&
|
||||
test "$TRAVIS_BRANCH" == "master" &&
|
||||
test "$TARGET" == "x86_64-unknown-linux-gnu" &&
|
||||
test "$CHANNEL" = "stable" &&
|
||||
npm install stylus nib &&
|
||||
bash deploy.sh
|
||||
|
||||
before_deploy:
|
||||
- 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
|
||||
# don't delete the artifacts from previous phases
|
||||
skip_cleanup: true
|
||||
# deploy when a new tag is pushed
|
||||
on:
|
||||
condition: $CHANNEL = stable
|
||||
tags: true
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
|
||||
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/azerupi/mdBook/issues), no question is too simple.
|
||||
If we don't respond in a couple of days, ping us @azerupi, @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/azerupi/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/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||
[A-Style](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
|
||||
[A-HTML](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
||||
[A-Mobile](https://github.com/azerupi/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/azerupi/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/azerupi/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:
|
||||
47
Cargo.toml
47
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.0.5"
|
||||
version = "0.0.23"
|
||||
authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
|
||||
description = "create books from markdown files (like Gitbook)"
|
||||
documentation = "http://azerupi.github.io/mdBook/index.html"
|
||||
@@ -15,36 +15,41 @@ exclude = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
clap = "~1.5.3"
|
||||
handlebars = "~0.12.0"
|
||||
rustc-serialize = "~0.3.16"
|
||||
pulldown-cmark = "~0.0.3"
|
||||
|
||||
clap = "2.24"
|
||||
handlebars = "0.27"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
error-chain = "0.10.0"
|
||||
serde_json = "1.0"
|
||||
pulldown-cmark = "0.0.14"
|
||||
lazy_static = "0.2"
|
||||
log = "0.3"
|
||||
env_logger = "0.4.0"
|
||||
toml = "0.4"
|
||||
open = "1.1"
|
||||
regex = "0.2.1"
|
||||
tempdir = "0.3.4"
|
||||
|
||||
# Watch feature
|
||||
[dependencies.notify]
|
||||
notify = "^2.4.1"
|
||||
optional = true
|
||||
notify = { version = "4.0", optional = true }
|
||||
time = { version = "0.1.34", optional = true }
|
||||
crossbeam = { version = "0.2.8", optional = true }
|
||||
|
||||
[dependencies.time]
|
||||
time = "^0.1.33"
|
||||
optional = true
|
||||
|
||||
[dependencies.crossbeam]
|
||||
time = "^0.2.0"
|
||||
optional = true
|
||||
|
||||
# Tests
|
||||
[dev-dependencies]
|
||||
tempdir = "~0.3.4"
|
||||
# Serve feature
|
||||
iron = { version = "0.5", optional = true }
|
||||
staticfile = { version = "0.4", optional = true }
|
||||
ws = { version = "0.7", optional = true}
|
||||
|
||||
[build-dependencies]
|
||||
error-chain = "0.10"
|
||||
|
||||
[features]
|
||||
default = ["output", "watch"]
|
||||
default = ["output", "watch", "serve"]
|
||||
debug = []
|
||||
output = []
|
||||
regenerate-css = []
|
||||
watch = ["notify", "time", "crossbeam"]
|
||||
serve = ["iron", "staticfile", "ws"]
|
||||
|
||||
[[bin]]
|
||||
doc = false
|
||||
|
||||
141
README.md
141
README.md
@@ -1,80 +1,129 @@
|
||||
# mdBook [](https://travis-ci.org/azerupi/mdBook) [](https://crates.io/crates/mdbook) [](LICENSE)
|
||||
# mdBook
|
||||
|
||||
Personal implementation of Gitbook in Rust
|
||||
<table>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Windows</strong></td>
|
||||
<td>
|
||||
<a href="https://ci.appveyor.com/project/azerupi/mdbook/"><img src="https://ci.appveyor.com/api/projects/status/o38racsnbcospyc8/branch/master?svg=true"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
**This project is still in it's early days.**
|
||||
For more information about what is left on my to-do list, check the issue tracker
|
||||
mdBook is a utility to create modern online books from Markdown files.
|
||||
|
||||
|
||||
## Example
|
||||
## What does it look like?
|
||||
|
||||
To have an idea of what a rendered book looks like,take a look at the [**Documentation**](http://azerupi.github.io/mdBook/). It is rendered by the latest version of mdBook.
|
||||
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.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
cargo install mdbook
|
||||
```
|
||||
There are multiple ways to install mdBook.
|
||||
|
||||
If you want to regenerate the css (stylesheet), clone the git repo locally and make sure that you installed `stylus` and `nib` from `npm` because it is used to compile the stylesheets
|
||||
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`.
|
||||
|
||||
Install [node.js](https://nodejs.org/en/)
|
||||
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:
|
||||
```
|
||||
cargo install mdbook
|
||||
```
|
||||
|
||||
```
|
||||
npm install -g stylus nib
|
||||
```
|
||||
This will download and compile mdBook for you, the only thing left to do is to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
Then build with the `regenerate-css` feature:
|
||||
**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 build --release --features="regenerate-css"
|
||||
```
|
||||
```
|
||||
cargo install mdbook --vers "^0.1.0"
|
||||
```
|
||||
|
||||
## Structure
|
||||
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***!
|
||||
|
||||
There are two main parts of this project:
|
||||
```
|
||||
cargo install --git https://github.com/azerupi/mdBook.git
|
||||
```
|
||||
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
- **The library:** The crate is structured so that all the code that actually does something is part of the library. You could therefore easily hook mdbook into your existing project, extend it's functionality by wrapping it in some other code, etc.
|
||||
- **The binary:** Is just a wrapper around the library functionality providing a nice and easy command line interface.
|
||||
4. **For Contributions**
|
||||
If you want to contribute to mdBook you will have to clone the repository on your local machine:
|
||||
|
||||
### Command line interface
|
||||
```
|
||||
git clone https://github.com/azerupi/mdBook.git
|
||||
```
|
||||
`cd` into `mdBook/` and run
|
||||
|
||||
#### init
|
||||
```
|
||||
cargo build
|
||||
```
|
||||
|
||||
If you run `mdbook init` in a directory, it will create a couple of folders and files you can start with.
|
||||
This is the strucutre it creates at the moment:
|
||||
```
|
||||
book-test/
|
||||
├── book
|
||||
└── src
|
||||
├── chapter_1.md
|
||||
└── SUMMARY.md
|
||||
```
|
||||
`book` and `src` are both directories. `src` contains the markdown files that will be used to render the ouput to the `book` directory.
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
|
||||
|
||||
Please, take a look at the [**Documentation**](http://azerupi.github.io/mdBook/cli/init.html) for more information.
|
||||
|
||||
#### build
|
||||
|
||||
Use `mdbook build` in the directory to render the book. You can find more information in the [**Documentation**](http://azerupi.github.io/mdBook/cli/build.html)
|
||||
## Usage
|
||||
|
||||
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/).
|
||||
|
||||
- `mdbook init`
|
||||
|
||||
The init command will create a directory with the minimal boilerplate to start with.
|
||||
|
||||
```
|
||||
book-test/
|
||||
├── book
|
||||
└── src
|
||||
├── chapter_1.md
|
||||
└── SUMMARY.md
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
- `mdbook build`
|
||||
|
||||
This is the command you will run to render your book, it reads the `SUMMARY.md` file to understand the structure of your book, takes the markdown files in the source directory as input and outputs static html pages that you can upload to a server.
|
||||
|
||||
- `mdbook watch`
|
||||
|
||||
When you run this command, mdbook will watch your markdown files to rebuild the book on every change. This avoids having to come back to the terminal to type `mdbook build` over and over again.
|
||||
|
||||
- `mdbook serve`
|
||||
|
||||
Does the same thing as `mdbook watch` but additionally serves the book at `http://localhost:3000` (port is changeable) and reloads the browser when a change occurs.
|
||||
|
||||
### As a library
|
||||
|
||||
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 webapp 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 and easy to use API and more!
|
||||
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 [Documentation](http://azerupi.github.io/mdBook/lib/lib.html) and the [API docs](http://azerupi.github.io/mdBook/mdbook/index.html) for more information.
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are highly apreciated. Here are some ideas:
|
||||
Contributions are highly appreciated and encouraged! Don't hesitate to participate to discussions in the issues, propose new features and ask for help.
|
||||
|
||||
- **Create new renderers**, at the moment I have only created a renderer that uses [handlebars](https://github.com/sunng87/handlebars-rust), [pulldown-cmark](https://github.com/google/pulldown-cmark) and renders to html. But you could create a renderer that uses another template engine, markdown parser or even outputs to another format like pdf.
|
||||
- **Add tests** I have not much experience in writing tests, all help to write meaningful tests is thus very welcome
|
||||
- **write documentation** documentation can always be improved
|
||||
- **Smaller tasks** I try to add a lot of the remaining tasks on the issue tracker with the label: [`Enhancement`](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AEnhancement). Just pick one that looks interesting. The majority of the tasks are small enough to be tackled by people who are unfamiliar with the project.
|
||||
If you are just starting out with Rust, there are a series of issus that are tagged [E-Easy](https://github.com/azerupi/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.
|
||||
|
||||
For more info about contributing, check out our [contribution guide](CONTRIBUTING.md) who helps you go through the build and contribution process!
|
||||
|
||||
If you have an idea for improvement, create a new issue. Or a pull request if you can :)
|
||||
|
||||
## 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.
|
||||
|
||||
63
appveyor.yml
Normal file
63
appveyor.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
environment:
|
||||
global:
|
||||
PROJECT_NAME: mdBook
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: stable
|
||||
# Beta channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: beta
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: beta
|
||||
# Nightly channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: nightly
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: nightly
|
||||
|
||||
# Install Rust and Cargo
|
||||
install:
|
||||
- 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
|
||||
|
||||
build: false
|
||||
|
||||
# Equivalent to Travis' `script` phase
|
||||
test_script:
|
||||
- cargo build --verbose
|
||||
- cargo test --verbose
|
||||
|
||||
before_deploy:
|
||||
# Generate artifacts for release
|
||||
- cargo build --release
|
||||
- mkdir staging
|
||||
- copy target\release\mdbook.exe staging
|
||||
- cd staging
|
||||
- 7z a ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip *
|
||||
- appveyor PushArtifact ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip
|
||||
|
||||
deploy:
|
||||
description: 'Windows release'
|
||||
artifact: /.*\.zip/
|
||||
auth_token:
|
||||
secure: QQhjKVyz7mpjlyGhlXytbFQQfKFQWTahHkD+B0NzIUoEVqO7ZLWjnoWasvLqW4nE
|
||||
provider: GitHub
|
||||
on:
|
||||
RUST_CHANNEL: stable
|
||||
appveyor_repo_tag: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"title": "mdBook Documentation",
|
||||
"author": "Mathieu David"
|
||||
}
|
||||
6
book-example/book.toml
Normal file
6
book-example/book.toml
Normal file
@@ -0,0 +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,7 +2,7 @@
|
||||
|
||||
**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).
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
- [init](cli/init.md)
|
||||
- [build](cli/build.md)
|
||||
- [watch](cli/watch.md)
|
||||
- [serve](cli/serve.md)
|
||||
- [test](cli/test.md)
|
||||
- [Format](format/format.md)
|
||||
- [SUMMARY.md](format/summary.md)
|
||||
@@ -13,6 +14,7 @@
|
||||
- [index.hbs](format/theme/index-hbs.md)
|
||||
- [Syntax highlighting](format/theme/syntax-highlighting.md)
|
||||
- [MathJax Support](format/mathjax.md)
|
||||
- [Rust code specific features](format/rust.md)
|
||||
- [Rust Library](lib/lib.md)
|
||||
-----------
|
||||
[Contributors](misc/contributors.md)
|
||||
|
||||
@@ -21,6 +21,15 @@ current working directory.
|
||||
mdbook build path/to/book
|
||||
```
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) option, mdbook will open the rendered book in
|
||||
your default web browser after building it.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
|
||||
|
||||
-------------------
|
||||
|
||||
***note:*** *make sure to run the build command in the root directory and not in the source directory*
|
||||
|
||||
@@ -22,7 +22,7 @@ configuration files, etc.
|
||||
- The `book` directory is where your book is rendered. All the output is ready to be uploaded
|
||||
to a server to be seen by your audience.
|
||||
|
||||
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is discussed in more detail in another [chapter](../format/summary.html).
|
||||
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is discussed in more detail in another [chapter](format/summary.html).
|
||||
|
||||
#### Tip & Trick: Hidden Feature
|
||||
When a `SUMMARY.md` file already exists, the `init` command will first parse it and generate the missing files according to the paths used in the `SUMMARY.md`. This allows you to think and create the whole structure of your book and then let mdBook generate it for you.
|
||||
|
||||
40
book-example/src/cli/serve.md
Normal file
40
book-example/src/cli/serve.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# The serve command
|
||||
|
||||
The `serve` command is useful when you want to preview your book. It also does hot reloading of the webpage whenever a file changes.
|
||||
It achieves this by serving the books content over `localhost:3000` (unless otherwise configured, see below) and runs a websocket server on `localhost:3001` which triggers the reloads.
|
||||
This preferred by many for writing books with mdbook because it allows for you to see the result of your work instantly after every file change.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
Like `watch`, `serve` can take a directory as argument to use instead of the
|
||||
current working directory.
|
||||
|
||||
```bash
|
||||
mdbook serve path/to/book
|
||||
```
|
||||
|
||||
|
||||
#### Server options
|
||||
|
||||
`serve` has four options: the http port, the websocket port, the interface to serve on, and the public address of the server so that the browser may reach the websocket server.
|
||||
|
||||
For example: suppose you had an nginx server for SSL termination which has a public address of 192.168.1.100 on port 80 and proxied that to 127.0.0.1 on port 8000. To run use the nginx proxy do:
|
||||
|
||||
```bash
|
||||
mdbook server path/to/book -p 8000 -i 127.0.0.1 -a 192.168.1.100
|
||||
```
|
||||
|
||||
If you were to want live reloading for this you would need to proxy the websocket calls through nginx as well from `192.168.1.100:<WS_PORT>` to `127.0.0.1:<WS_PORT>`. The `-w` flag allows for the websocket port to be configured.
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) option, mdbook will open the book in your
|
||||
your default web browser after starting the server.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
|
||||
|
||||
-----
|
||||
|
||||
***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)*
|
||||
@@ -10,7 +10,7 @@ mdBook supports a `test` command that will run all available tests in mdBook. At
|
||||
- checking for unused files
|
||||
- ...
|
||||
|
||||
In the future I would like the user to be able to enable / disable test from the `book.json` configuration file and support custom tests.
|
||||
In the future I would like the user to be able to enable / disable test from the `book.toml` configuration file and support custom tests.
|
||||
|
||||
**How to use it:**
|
||||
```bash
|
||||
|
||||
@@ -12,6 +12,14 @@ current working directory.
|
||||
mdbook watch path/to/book
|
||||
```
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) option, mdbook will open the rendered book in
|
||||
your default web browser.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -1,21 +1,89 @@
|
||||
# Configuration
|
||||
|
||||
You can configure the parameters for your book in the ***book.json*** file.
|
||||
You can configure the parameters for your book in the ***book.toml*** file.
|
||||
|
||||
Here is an example of what a ***book.json*** file might look like:
|
||||
>**Note:**
|
||||
JSON configuration files were previously supported but have been deprecated in favor of
|
||||
the TOML configuration file. If you are still using JSON we strongly encourage you to migrate to
|
||||
the TOML configuration because JSON support will be removed in the future.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Example book",
|
||||
"author": "Name",
|
||||
"dest": "output/my-book"
|
||||
}
|
||||
Here is an example of what a ***book.toml*** file might look like:
|
||||
|
||||
```toml
|
||||
title = "Example book"
|
||||
author = "John Doe"
|
||||
description = "The example book covers examples."
|
||||
|
||||
[output.html]
|
||||
destination = "my-example-book"
|
||||
additional-css = ["custom.css"]
|
||||
```
|
||||
|
||||
#### Supported variables
|
||||
## Supported configuration options
|
||||
|
||||
- **title:** title of the book
|
||||
- **author:** author of the book
|
||||
- **dest:** path to the directory where you want your book to be rendered. If a relative path is given it will be relative to 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.
|
||||
|
||||
### General metadata
|
||||
|
||||
- **title:** The title of the book
|
||||
- **author:** The author of the book
|
||||
- **description:** A description for the book, which is added as meta information in the html `<head>` of each page
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
title = "Example book"
|
||||
author = "John Doe"
|
||||
description = "The example book covers examples."
|
||||
```
|
||||
|
||||
Some books may have multiple authors, there is an alternative key called `authors` plural that lets you specify an array
|
||||
of authors.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
```
|
||||
|
||||
### Source directory
|
||||
By default, the source directory is found in the directory named `src` directly under the root folder. But this is configurable
|
||||
with the `source` key in the configuration file.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
source = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
|
||||
```
|
||||
|
||||
### HTML renderer options
|
||||
The HTML renderer has a couple of options aswell. All the options for the renderer need to be specified under the TOML table `[output.html]`.
|
||||
The following configuration options are available:
|
||||
|
||||
- **`destination`:** By default, the HTML book will be rendered in the `root/book` directory, but this option lets you specify another
|
||||
destination fodler.
|
||||
- **`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.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
[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"]
|
||||
```
|
||||
|
||||
***note:*** *the supported configurable parameters are scarce at the moment, but more will be added in the future*
|
||||
|
||||
6
book-example/src/format/example.rs
Normal file
6
book-example/src/format/example.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
fn main() {
|
||||
println!("Hello World!");
|
||||
#
|
||||
# // You can even hide lines! :D
|
||||
# println!("I am hidden! Expand the code snippet to see me");
|
||||
}
|
||||
@@ -4,5 +4,5 @@ In this section you will learn how to:
|
||||
|
||||
- Structure your book correctly
|
||||
- Format your `SUMMARY.md` file
|
||||
- Configure your book using `book.json`
|
||||
- Configure your book using `book.toml`
|
||||
- Customize your theme
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
# 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 \\]
|
||||
```
|
||||
|
||||
42
book-example/src/format/rust.md
Normal file
42
book-example/src/format/rust.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Rust code specific features
|
||||
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that let's you hide code lines by prepending them with a `#`.
|
||||
|
||||
```bash
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 6;
|
||||
|
||||
println!("{}", x + y);
|
||||
# }
|
||||
```
|
||||
|
||||
Will render as
|
||||
|
||||
```rust
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 7;
|
||||
|
||||
println!("{}", x + y);
|
||||
# }
|
||||
```
|
||||
|
||||
|
||||
## Inserting runnable Rust files
|
||||
|
||||
With the following syntax, you can insert runnable Rust files into your book:
|
||||
|
||||
```hbs
|
||||
\{{#playpen file.rs}}
|
||||
```
|
||||
|
||||
The path to the Rust file has to be relative from the current source file.
|
||||
|
||||
When play is clicked, the code snippet will be send to the [Rust Playpen]() to be compiled and run. The result is send back and displayed directly underneath the code.
|
||||
|
||||
Here is what a rendered code snippet looks like:
|
||||
|
||||
{{#playpen example.rs}}
|
||||
@@ -27,6 +27,6 @@ allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
|
||||
```
|
||||
You can either use `-` or `*` to indicate a numbered chapter.
|
||||
|
||||
4. ***Sufix Chapter*** After the numbered chapters you can add a couple of non-numbered chapters. They are the same as prefix chapters but come after the numbered chapters instead of before.
|
||||
4. ***Suffix Chapter*** After the numbered chapters you can add a couple of non-numbered chapters. They are the same as prefix chapters but come after the numbered chapters instead of before.
|
||||
|
||||
All other elements are unsupported and will be ignored at best or result in an error.
|
||||
|
||||
@@ -19,7 +19,8 @@ Here is a list of the properties that are exposed:
|
||||
|
||||
- ***language*** Language of the book in the form `en`. To use in <code class="language-html">\<html lang="{{ language }}"></code> for example.
|
||||
At the moment it is hardcoded.
|
||||
- ***title*** Title of the book, as specified in `book.json`
|
||||
- ***title*** Title of the book, as specified in `book.toml`
|
||||
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
||||
|
||||
- ***path*** Relative path to the original markdown file from the source directory
|
||||
- ***content*** This is the rendered markdown.
|
||||
|
||||
@@ -28,26 +28,26 @@ There is a feature in mdBook that let's you hide code lines by prepending them w
|
||||
|
||||
|
||||
```bash
|
||||
#fn main() {
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 6;
|
||||
|
||||
println!("{}", x + y);
|
||||
#}
|
||||
# }
|
||||
```
|
||||
|
||||
Will render as
|
||||
|
||||
```rust
|
||||
#fn main() {
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 7;
|
||||
|
||||
println!("{}", x + y);
|
||||
#}
|
||||
# }
|
||||
```
|
||||
|
||||
**At the moment, this only works for code examples that are annotated with `rust`. Because it would collide with semantics of some programming languages. In the future, we want to make this configurable through the `book.json` so that everyone can benefit from it.**
|
||||
**At the moment, this only works for code examples that are annotated with `rust`. Because it would collide with semantics of some programming languages. In the future, we want to make this configurable through the `book.toml` so that everyone can benefit from it.**
|
||||
|
||||
|
||||
## Improve default theme
|
||||
|
||||
@@ -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:
|
||||
@@ -14,6 +14,7 @@ Here are the files you can override:
|
||||
- ***book.js*** is mostly used to add client side functionality, like hiding / un-hiding the sidebar, changing the theme, ...
|
||||
- ***highlight.js*** is the JavaScript that is used to highlight code snippets, you should not need to modify this.
|
||||
- ***highlight.css*** is the theme used for the code highlighting
|
||||
- ***favicon.png*** the favicon that will be used
|
||||
|
||||
Generally, when you want to tweak the theme, you don't need to override all the files. If you only need changes in the stylesheet,
|
||||
there is no point in overriding all the other files. Because custom files take precedence over built-in ones, they will not get updated with new fixes / features.
|
||||
|
||||
@@ -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.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.
|
||||
|
||||
3
book-example/src/misc/introduction.md
Normal file
3
book-example/src/misc/introduction.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Introduction
|
||||
|
||||
A frontmatter chapter.
|
||||
84
build.rs
84
build.rs
@@ -3,27 +3,93 @@
|
||||
use std::process::Command;
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
|
||||
fn main() {
|
||||
#[cfg(windows)]
|
||||
mod execs {
|
||||
pub const NPM: &'static str = "npm.cmd";
|
||||
pub const STYLUS: &'static str = "stylus.cmd";
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
mod execs {
|
||||
pub const NPM: &'static str = "npm";
|
||||
pub const STYLUS: &'static str = "stylus";
|
||||
}
|
||||
|
||||
|
||||
error_chain!{
|
||||
foreign_links {
|
||||
Io(std::io::Error);
|
||||
}
|
||||
}
|
||||
|
||||
fn program_exists(program: &str) -> Result<()> {
|
||||
Command::new(program)
|
||||
.arg("-v")
|
||||
.output()
|
||||
.chain_err(|| format!("Please install '{}'!", program))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn npm_package_exists(package: &str) -> Result<()> {
|
||||
let status = Command::new(execs::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::{Program, Package};
|
||||
|
||||
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(execs::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()))
|
||||
if !Command::new(execs::STYLUS)
|
||||
.arg(stylus_dir)
|
||||
.arg("--out")
|
||||
.arg(format!("{}", theme_dir.to_str().unwrap()))
|
||||
.arg(theme_dir)
|
||||
.arg("--use")
|
||||
.arg("nib")
|
||||
.status().unwrap()
|
||||
.status()?
|
||||
.success() {
|
||||
panic!("Stylus encoutered an error");
|
||||
bail!("Stylus encoutered an error");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
quick_main!(run);
|
||||
|
||||
32
ci/before_deploy.sh
Normal file
32
ci/before_deploy.sh
Normal file
@@ -0,0 +1,32 @@
|
||||
# `before_deploy` phase: here we package the build artifacts
|
||||
|
||||
set -ex
|
||||
|
||||
mktempd() {
|
||||
echo $(mktemp -d 2>/dev/null || mktemp -d -t tmp)
|
||||
}
|
||||
|
||||
mk_artifacts() {
|
||||
cargo build --target $TARGET --release
|
||||
}
|
||||
|
||||
mk_tarball() {
|
||||
local td=$(mktempd)
|
||||
local out_dir=$(pwd)
|
||||
|
||||
cp target/$TARGET/release/mdbook $td
|
||||
|
||||
pushd $td
|
||||
|
||||
tar czf $out_dir/${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}.tar.gz *
|
||||
|
||||
popd $td
|
||||
rm -r $td
|
||||
}
|
||||
|
||||
main() {
|
||||
mk_artifacts
|
||||
mk_tarball
|
||||
}
|
||||
|
||||
main
|
||||
59
ci/install.sh
Normal file
59
ci/install.sh
Normal file
@@ -0,0 +1,59 @@
|
||||
# `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 self update
|
||||
rustup install "$CHANNEL"
|
||||
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
Normal file
45
ci/script.sh
Normal file
@@ -0,0 +1,45 @@
|
||||
# `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
|
||||
@@ -15,7 +15,7 @@ cargo doc
|
||||
|
||||
echo -e "${CYAN}Running mdbook build${NC}"
|
||||
# Run mdbook to generate the book
|
||||
target/debug/mdbook build book-example/
|
||||
target/"$TARGET"/debug/mdbook build book-example/
|
||||
|
||||
echo -e "${CYAN}Copying book to target/doc${NC}"
|
||||
# Copy files from rendered book to doc root
|
||||
|
||||
16
rustfmt.toml
Normal file
16
rustfmt.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
write_mode = "Overwrite"
|
||||
|
||||
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
|
||||
use_try_shorthand = true
|
||||
|
||||
report_todo = "Always"
|
||||
report_fixme = "Always"
|
||||
42
src/bin/build.rs
Normal file
42
src/bin/build.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
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'")
|
||||
.arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'")
|
||||
.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 book = MDBook::new(&book_dir).read_config()?;
|
||||
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.with_destination(dest_dir),
|
||||
None => book,
|
||||
};
|
||||
|
||||
if args.is_present("no-create") {
|
||||
book.create_missing = false;
|
||||
}
|
||||
|
||||
if args.is_present("curly-quotes") {
|
||||
book = book.with_curly_quotes(true);
|
||||
}
|
||||
|
||||
book.build()?;
|
||||
|
||||
if args.is_present("open") {
|
||||
open(book.get_destination().join("index.html"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
77
src/bin/init.rs
Normal file
77
src/bin/init.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use clap::{ArgMatches, SubCommand, App};
|
||||
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.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(())
|
||||
}
|
||||
|
||||
// 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,228 +1,98 @@
|
||||
#[macro_use]
|
||||
extern crate mdbook;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate crossbeam;
|
||||
|
||||
// Dependencies for the Watch feature
|
||||
#[cfg(feature = "watch")]
|
||||
extern crate notify;
|
||||
#[cfg(feature = "watch")]
|
||||
extern crate time;
|
||||
|
||||
extern crate log;
|
||||
extern crate env_logger;
|
||||
extern crate open;
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use clap::{App, ArgMatches, AppSettings};
|
||||
use log::{LogRecord, LogLevelFilter};
|
||||
use env_logger::LogBuilder;
|
||||
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
|
||||
// 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::sync::mpsc::channel;
|
||||
|
||||
|
||||
use mdbook::MDBook;
|
||||
pub mod watch;
|
||||
|
||||
const NAME: &'static str = "mdbook";
|
||||
|
||||
fn main() {
|
||||
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!()))
|
||||
.subcommand_required(true)
|
||||
.after_help("For more information about a specific command, try `mdbook <command> --help`")
|
||||
.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 ommitted)'")
|
||||
.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("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
|
||||
.subcommand(SubCommand::with_name("watch")
|
||||
.about("Watch the files for changes")
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
|
||||
.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`\nSource code for mdbook available at: https://github.com/azerupi/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),
|
||||
("test", Some(sub_matches)) => test(sub_matches),
|
||||
(_, _) => unreachable!()
|
||||
("watch", Some(sub_matches)) => watch::execute(sub_matches),
|
||||
#[cfg(feature = "serve")]
|
||||
("serve", Some(sub_matches)) => serve::execute(sub_matches),
|
||||
("test", Some(sub_matches)) => test::execute(sub_matches),
|
||||
(_, _) => unreachable!(),
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
writeln!(&mut io::stderr(), "An error occured:\n{}", e).ok();
|
||||
::std::process::exit(101);
|
||||
}
|
||||
}
|
||||
|
||||
fn init_logger() {
|
||||
let format = |record: &LogRecord| {
|
||||
let module_path = record.location().module_path();
|
||||
format!("{}:{}: {}", record.level(), module_path, record.args())
|
||||
};
|
||||
|
||||
// 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 id `--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.");
|
||||
let mut builder = LogBuilder::new();
|
||||
builder.format(format).filter(None, LogLevelFilter::Info);
|
||||
|
||||
if let Ok(var) = env::var("RUST_LOG") {
|
||||
builder.parse(&var);
|
||||
}
|
||||
|
||||
println!("\nAll done, no errors...");
|
||||
|
||||
Ok(())
|
||||
builder.init().unwrap();
|
||||
}
|
||||
|
||||
|
||||
// Build command implementation
|
||||
fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir).read_config();
|
||||
|
||||
try!(book.build());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// 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();
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);
|
||||
|
||||
match w {
|
||||
Ok(mut watcher) => {
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.get_src()) {
|
||||
println!("Error while watching {:?}:\n {:?}", book.get_src(), e);
|
||||
::std::process::exit(0);
|
||||
};
|
||||
|
||||
// Add the book.json file to the watcher if it exists, because it's not
|
||||
// located in the source directory
|
||||
if let Err(_) = watcher.watch(book_dir.join("book.json")) {
|
||||
// do nothing if book.json is not found
|
||||
}
|
||||
|
||||
let previous_time = time::get_time().sec;
|
||||
|
||||
crossbeam::scope(|scope| {
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(event) => {
|
||||
|
||||
// Skip the event if an event has already been issued in the last second
|
||||
if time::get_time().sec - previous_time < 1 { continue }
|
||||
|
||||
if let Some(path) = event.path {
|
||||
// Trigger the build process in a new thread (to keep receiving events)
|
||||
scope.spawn(move || {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
match build(args) {
|
||||
Err(e) => println!("Error while building: {:?}", e),
|
||||
_ => {}
|
||||
}
|
||||
println!("");
|
||||
});
|
||||
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("An error occured: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
::std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
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...
|
||||
let p = Path::new(dir);
|
||||
if p.is_relative() {
|
||||
env::current_dir().unwrap().join(dir)
|
||||
env::current_dir().unwrap().join(dir)
|
||||
} else {
|
||||
p.to_path_buf()
|
||||
p.to_path_buf()
|
||||
}
|
||||
} else {
|
||||
env::current_dir().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn open<P: AsRef<OsStr>>(path: P) {
|
||||
if let Err(e) = open::that(path) {
|
||||
println!("Error opening web browser: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
115
src/bin/serve.rs
Normal file
115
src/bin/serve.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
extern crate iron;
|
||||
extern crate staticfile;
|
||||
extern crate ws;
|
||||
|
||||
use std;
|
||||
use std::path::Path;
|
||||
use self::iron::{Iron, AfterMiddleware, IronResult, IronError, Request, Response, status, Set, Chain};
|
||||
use clap::{ArgMatches, SubCommand, App};
|
||||
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("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'")
|
||||
.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 book = MDBook::new(&book_dir).read_config()?;
|
||||
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.with_destination(Path::new(dest_dir)),
|
||||
None => book,
|
||||
};
|
||||
|
||||
if args.is_present("curly-quotes") {
|
||||
book = book.with_curly_quotes(true);
|
||||
}
|
||||
|
||||
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.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));
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/bin/test.rs
Normal file
22
src/bin/test.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use clap::{ArgMatches, SubCommand, App};
|
||||
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(())
|
||||
}
|
||||
117
src/bin/watch.rs
Normal file
117
src/bin/watch.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
extern crate notify;
|
||||
extern crate time;
|
||||
extern crate crossbeam;
|
||||
|
||||
use std::path::Path;
|
||||
use self::notify::Watcher;
|
||||
use std::time::Duration;
|
||||
use std::sync::mpsc::channel;
|
||||
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("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("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'")
|
||||
.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 book = MDBook::new(&book_dir).read_config()?;
|
||||
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.with_destination(dest_dir),
|
||||
None => book,
|
||||
};
|
||||
|
||||
if args.is_present("curly-quotes") {
|
||||
book = book.with_curly_quotes(true);
|
||||
}
|
||||
|
||||
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.get_theme_path(), 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.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);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
extern crate rustc_serialize;
|
||||
use self::rustc_serialize::json::Json;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BookConfig {
|
||||
pub title: String,
|
||||
pub author: String,
|
||||
root: PathBuf,
|
||||
dest: PathBuf,
|
||||
src: PathBuf,
|
||||
pub indent_spaces: i32,
|
||||
multilingual: bool,
|
||||
}
|
||||
|
||||
|
||||
impl BookConfig {
|
||||
pub fn new(root: &Path) -> Self {
|
||||
BookConfig {
|
||||
title: String::new(),
|
||||
author: String::new(),
|
||||
root: root.to_owned(),
|
||||
dest: PathBuf::from("book"),
|
||||
src: PathBuf::from("src"),
|
||||
indent_spaces: 4, // indentation used for SUMMARY.md
|
||||
multilingual: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_config(&mut self, root: &Path) -> &mut Self {
|
||||
|
||||
debug!("[fn]: read_config");
|
||||
|
||||
// If the file does not exist, return early
|
||||
let mut config_file = match File::open(root.join("book.json")) {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
debug!("[*]: Failed to open {:?}", root.join("book.json"));
|
||||
return self
|
||||
},
|
||||
};
|
||||
|
||||
debug!("[*]: Reading config");
|
||||
let mut data = String::new();
|
||||
|
||||
// Just return if an error occured.
|
||||
// I would like to propagate the error, but I have to return `&self`
|
||||
if let Err(_) = config_file.read_to_string(&mut data) { return self }
|
||||
|
||||
// Convert to JSON
|
||||
if let Ok(config) = Json::from_str(&data) {
|
||||
// Extract data
|
||||
|
||||
debug!("[*]: Extracting data from config");
|
||||
// Title & author
|
||||
if let Some(a) = config.find_path(&["title"]) { self.title = a.to_string().replace("\"", "") }
|
||||
if let Some(a) = config.find_path(&["author"]) { self.author = a.to_string().replace("\"", "") }
|
||||
|
||||
// Destination
|
||||
if let Some(a) = config.find_path(&["dest"]) {
|
||||
let dest = PathBuf::from(&a.to_string().replace("\"", ""));
|
||||
|
||||
// If path is relative make it absolute from the parent directory of src
|
||||
match dest.is_relative() {
|
||||
true => {
|
||||
let dest = self.get_root().join(&dest).to_owned();
|
||||
self.set_dest(&dest);
|
||||
},
|
||||
false => { self.set_dest(&dest); },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use self::rustc_serialize::json::{Json, ToJson};
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde::ser::SerializeStruct;
|
||||
use std::path::PathBuf;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BookItem {
|
||||
@@ -27,7 +26,6 @@ pub struct BookItems<'a> {
|
||||
|
||||
|
||||
impl Chapter {
|
||||
|
||||
pub fn new(name: String, path: PathBuf) -> Self {
|
||||
|
||||
Chapter {
|
||||
@@ -39,15 +37,14 @@ impl Chapter {
|
||||
}
|
||||
|
||||
|
||||
impl ToJson for Chapter {
|
||||
|
||||
fn to_json(&self) -> Json {
|
||||
let mut m: BTreeMap<String, Json> = BTreeMap::new();
|
||||
m.insert("name".to_owned(), self.name.to_json());
|
||||
m.insert("path".to_owned(),self.path.to_str()
|
||||
.expect("Json conversion failed for path").to_json()
|
||||
);
|
||||
m.to_json()
|
||||
impl Serialize for Chapter {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,23 +63,24 @@ 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.get(self.current_index).unwrap();
|
||||
let cur = &self.items[self.current_index];
|
||||
|
||||
match *cur {
|
||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
|
||||
BookItem::Chapter(_, ref ch) |
|
||||
BookItem::Affix(ref ch) => {
|
||||
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)
|
||||
return Some(cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,357 +0,0 @@
|
||||
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 {BookConfig, BookItem, theme, parse, utils};
|
||||
use book::BookItems;
|
||||
use renderer::{Renderer, HtmlHandlebars};
|
||||
use utils::{PathExt, create_path};
|
||||
|
||||
pub struct MDBook {
|
||||
config: BookConfig,
|
||||
pub content: Vec<BookItem>,
|
||||
renderer: Box<Renderer>,
|
||||
}
|
||||
|
||||
impl MDBook {
|
||||
|
||||
/// Create a new `MDBook` struct with root directory `root`
|
||||
///
|
||||
/// - The default source directory is set to `root/src`
|
||||
/// - The default output directory is set to `root/book`
|
||||
///
|
||||
/// They can both be changed by using [`set_src()`](#method.set_src) and [`set_dest()`](#method.set_dest)
|
||||
|
||||
pub fn new(root: &Path) -> MDBook {
|
||||
|
||||
if !root.exists() || !root.is_dir() {
|
||||
output!("{:?} No directory with that name", root);
|
||||
}
|
||||
|
||||
MDBook {
|
||||
content: vec![],
|
||||
config: BookConfig::new(root)
|
||||
.set_src(&root.join("src"))
|
||||
.set_dest(&root.join("book"))
|
||||
.to_owned(),
|
||||
renderer: Box::new(HtmlHandlebars::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
/// # fn main() {
|
||||
/// # let mut book = MDBook::new(Path::new("mybook"));
|
||||
/// for item in book.iter() {
|
||||
/// match item {
|
||||
/// &BookItem::Chapter(ref section, ref chapter) => {},
|
||||
/// &BookItem::Affix(ref chapter) => {},
|
||||
/// &BookItem::Spacer => {},
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // would print something like this:
|
||||
/// // 1. Chapter 1
|
||||
/// // 1.1 Sub Chapter
|
||||
/// // 1.2 Sub Chapter
|
||||
/// // 2. Chapter 2
|
||||
/// //
|
||||
/// // etc.
|
||||
/// # }
|
||||
/// ```
|
||||
|
||||
pub fn iter(&self) -> BookItems {
|
||||
BookItems {
|
||||
items: &self.content[..],
|
||||
current_index: 0,
|
||||
stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `init()` creates some boilerplate files and directories to get you started with your book.
|
||||
///
|
||||
/// ```text
|
||||
/// book-test/
|
||||
/// ├── book
|
||||
/// └── src
|
||||
/// ├── chapter_1.md
|
||||
/// └── SUMMARY.md
|
||||
/// ```
|
||||
///
|
||||
/// 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>> {
|
||||
|
||||
debug!("[fn]: init");
|
||||
|
||||
if !self.config.get_root().exists() {
|
||||
create_path(self.config.get_root()).unwrap();
|
||||
output!("{:?} created", self.config.get_root());
|
||||
}
|
||||
|
||||
{
|
||||
let dest = self.config.get_dest();
|
||||
let src = self.config.get_src();
|
||||
|
||||
if !dest.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", dest);
|
||||
try!(fs::create_dir(&dest));
|
||||
}
|
||||
|
||||
if !src.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", src);
|
||||
try!(fs::create_dir(&src));
|
||||
}
|
||||
|
||||
let summary = src.join("SUMMARY.md");
|
||||
|
||||
if !summary.exists() {
|
||||
|
||||
// Summary does not exist, create it
|
||||
|
||||
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", src.join("SUMMARY.md"));
|
||||
let mut f = try!(File::create(&src.join("SUMMARY.md")));
|
||||
|
||||
debug!("[*]: Writing to SUMMARY.md");
|
||||
|
||||
try!(writeln!(f, "# Summary"));
|
||||
try!(writeln!(f, ""));
|
||||
try!(writeln!(f, "- [Chapter 1](./chapter_1.md)"));
|
||||
}
|
||||
}
|
||||
|
||||
// parse SUMMARY.md, and create the missing item related file
|
||||
try!(self.parse_summary());
|
||||
|
||||
debug!("[*]: constructing paths for missing files");
|
||||
for item in self.iter() {
|
||||
debug!("[*]: item: {:?}", item);
|
||||
match *item {
|
||||
BookItem::Spacer => continue,
|
||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
|
||||
if ch.path != PathBuf::new() {
|
||||
let path = self.config.get_src().join(&ch.path);
|
||||
|
||||
if !path.exists() {
|
||||
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));
|
||||
|
||||
//debug!("[*]: Writing to {:?}", path);
|
||||
try!(writeln!(f, "# {}", ch.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("[*]: init done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
debug!("[fn]: build");
|
||||
|
||||
try!(self.init());
|
||||
|
||||
// Clean output directory
|
||||
try!(utils::remove_dir_content(&self.config.get_dest()));
|
||||
|
||||
try!(self.renderer.render(&self));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn copy_theme(&self) -> Result<(), Box<Error>> {
|
||||
debug!("[fn]: copy_theme");
|
||||
|
||||
let theme_dir = self.config.get_src().join("theme");
|
||||
|
||||
if !theme_dir.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", theme_dir);
|
||||
try!(fs::create_dir(&theme_dir));
|
||||
}
|
||||
|
||||
// index.hbs
|
||||
let mut index = try!(File::create(&theme_dir.join("index.hbs")));
|
||||
try!(index.write_all(theme::INDEX));
|
||||
|
||||
// book.css
|
||||
let mut css = try!(File::create(&theme_dir.join("book.css")));
|
||||
try!(css.write_all(theme::CSS));
|
||||
|
||||
// book.js
|
||||
let mut js = try!(File::create(&theme_dir.join("book.js")));
|
||||
try!(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));
|
||||
|
||||
// highlight.js
|
||||
let mut highlight_js = try!(File::create(&theme_dir.join("highlight.js")));
|
||||
try!(highlight_js.write_all(theme::HIGHLIGHT_JS));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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`
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate mdbook;
|
||||
/// # use mdbook::MDBook;
|
||||
/// # use std::path::Path;
|
||||
/// # fn main() {
|
||||
/// let mut book = MDBook::new(Path::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.
|
||||
|
||||
pub fn read_config(mut self) -> Self {
|
||||
let root = self.config.get_root().to_owned();
|
||||
self.config.read_config(&root);
|
||||
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)
|
||||
///
|
||||
/// ```no_run
|
||||
/// extern crate mdbook;
|
||||
/// use mdbook::MDBook;
|
||||
/// use mdbook::renderer::HtmlHandlebars;
|
||||
/// # use std::path::Path;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let mut book = MDBook::new(Path::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
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// **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>> {
|
||||
// read in the chapters
|
||||
try!(self.parse_summary());
|
||||
for item in self.iter() {
|
||||
|
||||
match *item {
|
||||
BookItem::Chapter(_, ref ch) => {
|
||||
if ch.path != PathBuf::new() {
|
||||
|
||||
let path = self.get_src().join(&ch.path);
|
||||
|
||||
println!("[*]: Testing file: {:?}", path);
|
||||
|
||||
let output_result = Command::new("rustdoc")
|
||||
.arg(&path)
|
||||
.arg("--test")
|
||||
.output();
|
||||
let output = try!(output_result);
|
||||
|
||||
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>);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_dest(mut self, dest: &Path) -> Self {
|
||||
|
||||
// Handle absolute and relative paths
|
||||
match dest.is_absolute() {
|
||||
true => { self.config.set_dest(dest); },
|
||||
false => {
|
||||
let dest = self.config.get_root().join(dest).to_owned();
|
||||
self.config.set_dest(&dest);
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_dest(&self) -> &Path {
|
||||
self.config.get_dest()
|
||||
}
|
||||
|
||||
pub fn set_src(mut self, src: &Path) -> Self {
|
||||
|
||||
// Handle absolute and relative paths
|
||||
match src.is_absolute() {
|
||||
true => { self.config.set_src(src); },
|
||||
false => {
|
||||
let src = self.config.get_root().join(src).to_owned();
|
||||
self.config.set_src(&src);
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_src(&self) -> &Path {
|
||||
self.config.get_src()
|
||||
}
|
||||
|
||||
pub fn set_title(mut self, title: &str) -> Self {
|
||||
self.config.title = title.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_title(&self) -> &str {
|
||||
&self.config.title
|
||||
}
|
||||
|
||||
pub fn set_author(mut self, author: &str) -> Self {
|
||||
self.config.author = author.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_author(&self) -> &str {
|
||||
&self.config.author
|
||||
}
|
||||
|
||||
// Construct book
|
||||
fn parse_summary(&mut self) -> Result<(), Box<Error>> {
|
||||
// When append becomes stable, use self.content.append() ...
|
||||
self.content = try!(parse::construct_bookitems(&self.config.get_src().join("SUMMARY.md")));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
523
src/book/mod.rs
523
src/book/mod.rs
@@ -1,7 +1,522 @@
|
||||
pub mod mdbook;
|
||||
pub mod bookitem;
|
||||
pub mod bookconfig;
|
||||
|
||||
pub use self::bookitem::{BookItem, BookItems};
|
||||
pub use self::bookconfig::BookConfig;
|
||||
pub use self::mdbook::MDBook;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::process::Command;
|
||||
use tempdir::TempDir;
|
||||
|
||||
use {theme, parse, utils};
|
||||
use renderer::{Renderer, HtmlHandlebars};
|
||||
use preprocess;
|
||||
use errors::*;
|
||||
|
||||
use config::BookConfig;
|
||||
use config::tomlconfig::TomlConfig;
|
||||
use config::htmlconfig::HtmlConfig;
|
||||
use config::jsonconfig::JsonConfig;
|
||||
|
||||
pub struct MDBook {
|
||||
config: BookConfig,
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
impl MDBook {
|
||||
/// Create a new `MDBook` struct with root directory `root`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate mdbook;
|
||||
/// # use mdbook::MDBook;
|
||||
/// # #[allow(unused_variables)]
|
||||
/// # fn main() {
|
||||
/// 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.
|
||||
///
|
||||
/// Default directory paths:
|
||||
///
|
||||
/// - source: `root/src`
|
||||
/// - 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<P: Into<PathBuf>>(root: P) -> MDBook {
|
||||
|
||||
let root = root.into();
|
||||
if !root.exists() || !root.is_dir() {
|
||||
warn!("{:?} No directory with that name", root);
|
||||
}
|
||||
|
||||
MDBook {
|
||||
config: BookConfig::new(root),
|
||||
|
||||
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):
|
||||
/// `(section: String, bookitem: &BookItem)`
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate mdbook;
|
||||
/// # use mdbook::MDBook;
|
||||
/// # use mdbook::BookItem;
|
||||
/// # #[allow(unused_variables)]
|
||||
/// # fn main() {
|
||||
/// # let book = MDBook::new("mybook");
|
||||
/// for item in book.iter() {
|
||||
/// match item {
|
||||
/// &BookItem::Chapter(ref section, ref chapter) => {},
|
||||
/// &BookItem::Affix(ref chapter) => {},
|
||||
/// &BookItem::Spacer => {},
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // would print something like this:
|
||||
/// // 1. Chapter 1
|
||||
/// // 1.1 Sub Chapter
|
||||
/// // 1.2 Sub Chapter
|
||||
/// // 2. Chapter 2
|
||||
/// //
|
||||
/// // etc.
|
||||
/// # }
|
||||
/// ```
|
||||
|
||||
pub fn iter(&self) -> BookItems {
|
||||
BookItems {
|
||||
items: &self.content[..],
|
||||
current_index: 0,
|
||||
stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `init()` creates some boilerplate files and directories
|
||||
/// to get you started with your book.
|
||||
///
|
||||
/// ```text
|
||||
/// book-test/
|
||||
/// ├── book
|
||||
/// └── src
|
||||
/// ├── chapter_1.md
|
||||
/// └── SUMMARY.md
|
||||
/// ```
|
||||
///
|
||||
/// 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<()> {
|
||||
|
||||
debug!("[fn]: init");
|
||||
|
||||
if !self.config.get_root().exists() {
|
||||
fs::create_dir_all(&self.config.get_root()).unwrap();
|
||||
info!("{:?} created", &self.config.get_root());
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
if !self.get_destination().exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", self.get_destination());
|
||||
fs::create_dir_all(self.get_destination())?;
|
||||
}
|
||||
|
||||
|
||||
if !self.config.get_source().exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", self.config.get_source());
|
||||
fs::create_dir_all(self.config.get_source())?;
|
||||
}
|
||||
|
||||
let summary = self.config.get_source().join("SUMMARY.md");
|
||||
|
||||
if !summary.exists() {
|
||||
|
||||
// Summary does not exist, create it
|
||||
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", &summary);
|
||||
let mut f = File::create(&summary)?;
|
||||
|
||||
debug!("[*]: Writing to SUMMARY.md");
|
||||
|
||||
writeln!(f, "# Summary")?;
|
||||
writeln!(f, "")?;
|
||||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||
}
|
||||
}
|
||||
|
||||
// parse SUMMARY.md, and create the missing item related file
|
||||
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,
|
||||
};
|
||||
if !ch.path.as_os_str().is_empty() {
|
||||
let path = self.config.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());
|
||||
}
|
||||
debug!("[*]: {:?} does not exist, trying to create file", path);
|
||||
::std::fs::create_dir_all(path.parent().unwrap())?;
|
||||
let mut f = File::create(path)?;
|
||||
|
||||
// debug!("[*]: Writing to {:?}", path);
|
||||
writeln!(f, "# {}", ch.name)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("[*]: init done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_gitignore(&self) {
|
||||
let gitignore = self.get_gitignore();
|
||||
|
||||
let destination = self.config.get_html_config()
|
||||
.get_destination();
|
||||
|
||||
// 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.config.get_root()) {
|
||||
|
||||
let relative = destination
|
||||
.strip_prefix(self.config.get_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);
|
||||
|
||||
let mut f = File::create(&gitignore).expect("Could not create file.");
|
||||
|
||||
debug!("[*]: Writing to .gitignore");
|
||||
|
||||
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()`
|
||||
/// method of the current renderer.
|
||||
///
|
||||
/// It is the renderer who generates all the output files.
|
||||
pub fn build(&mut self) -> Result<()> {
|
||||
debug!("[fn]: build");
|
||||
|
||||
self.init()?;
|
||||
|
||||
// Clean output directory
|
||||
utils::fs::remove_dir_content(self.config.get_html_config().get_destination())?;
|
||||
|
||||
self.renderer.render(self)
|
||||
}
|
||||
|
||||
|
||||
pub fn get_gitignore(&self) -> PathBuf {
|
||||
self.config.get_root().join(".gitignore")
|
||||
}
|
||||
|
||||
pub fn copy_theme(&self) -> Result<()> {
|
||||
debug!("[fn]: copy_theme");
|
||||
|
||||
let themedir = self.config.get_html_config().get_theme();
|
||||
if !themedir.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", themedir);
|
||||
fs::create_dir(&themedir)?;
|
||||
}
|
||||
|
||||
// index.hbs
|
||||
let mut index = File::create(&themedir.join("index.hbs"))?;
|
||||
index.write_all(theme::INDEX)?;
|
||||
|
||||
// book.css
|
||||
let mut css = File::create(&themedir.join("book.css"))?;
|
||||
css.write_all(theme::CSS)?;
|
||||
|
||||
// favicon.png
|
||||
let mut favicon = File::create(&themedir.join("favicon.png"))?;
|
||||
favicon.write_all(theme::FAVICON)?;
|
||||
|
||||
// book.js
|
||||
let mut js = File::create(&themedir.join("book.js"))?;
|
||||
js.write_all(theme::JS)?;
|
||||
|
||||
// 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 = 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<()> {
|
||||
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.
|
||||
/// 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) -> Result<Self> {
|
||||
|
||||
let toml = self.get_root().join("book.toml");
|
||||
let json = self.get_root().join("book.json");
|
||||
|
||||
if toml.exists() {
|
||||
let mut file = File::open(toml)?;
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content)?;
|
||||
|
||||
let parsed_config = TomlConfig::from_toml(&content)?;
|
||||
self.config.fill_from_tomlconfig(parsed_config);
|
||||
} else if json.exists() {
|
||||
warn!("The JSON configuration file is deprecated, please use the TOML configuration.");
|
||||
let mut file = File::open(json)?;
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content)?;
|
||||
|
||||
let parsed_config = JsonConfig::from_json(&content)?;
|
||||
self.config.fill_from_jsonconfig(parsed_config);
|
||||
}
|
||||
|
||||
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)
|
||||
///
|
||||
/// ```no_run
|
||||
/// extern crate mdbook;
|
||||
/// use mdbook::MDBook;
|
||||
/// use mdbook::renderer::HtmlHandlebars;
|
||||
///
|
||||
/// # #[allow(unused_variables)]
|
||||
/// fn main() {
|
||||
/// 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
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// **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, library_paths: Vec<&str>) -> Result<()> {
|
||||
// read in the chapters
|
||||
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.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);
|
||||
|
||||
//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() {
|
||||
bail!(ErrorKind::Subprocess("Rustdoc returned an error".to_string(), output));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_root(&self) -> &Path {
|
||||
self.config.get_root()
|
||||
}
|
||||
|
||||
|
||||
pub fn with_destination<T: Into<PathBuf>>(mut self, destination: T) -> Self {
|
||||
let root = self.config.get_root().to_owned();
|
||||
self.config.get_mut_html_config()
|
||||
.set_destination(&root, &destination.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn get_destination(&self) -> &Path {
|
||||
self.config.get_html_config()
|
||||
.get_destination()
|
||||
}
|
||||
|
||||
pub fn with_source<T: Into<PathBuf>>(mut self, source: T) -> Self {
|
||||
self.config.set_source(source);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_source(&self) -> &Path {
|
||||
self.config.get_source()
|
||||
}
|
||||
|
||||
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
|
||||
self.config.set_title(title);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_title(&self) -> &str {
|
||||
self.config.get_title()
|
||||
}
|
||||
|
||||
pub fn with_description<T: Into<String>>(mut self, description: T) -> Self {
|
||||
self.config.set_description(description);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_description(&self) -> &str {
|
||||
self.config.get_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 with_theme_path<T: Into<PathBuf>>(mut self, theme_path: T) -> Self {
|
||||
let root = self.config.get_root().to_owned();
|
||||
self.config.get_mut_html_config()
|
||||
.set_theme(&root, &theme_path.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_theme_path(&self) -> &Path {
|
||||
self.config.get_html_config()
|
||||
.get_theme()
|
||||
}
|
||||
|
||||
pub fn with_curly_quotes(mut self, curly_quotes: bool) -> Self {
|
||||
self.config.get_mut_html_config()
|
||||
.set_curly_quotes(curly_quotes);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_curly_quotes(&self) -> bool {
|
||||
self.config.get_html_config()
|
||||
.get_curly_quotes()
|
||||
}
|
||||
|
||||
pub fn with_mathjax_support(mut self, mathjax_support: bool) -> Self {
|
||||
self.config.get_mut_html_config()
|
||||
.set_mathjax_support(mathjax_support);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_mathjax_support(&self) -> bool {
|
||||
self.config.get_html_config()
|
||||
.get_mathjax_support()
|
||||
}
|
||||
|
||||
pub fn get_google_analytics_id(&self) -> Option<String> {
|
||||
self.config.get_html_config()
|
||||
.get_google_analytics_id()
|
||||
}
|
||||
|
||||
pub fn has_additional_js(&self) -> bool {
|
||||
self.config.get_html_config()
|
||||
.has_additional_js()
|
||||
}
|
||||
|
||||
pub fn get_additional_js(&self) -> &[PathBuf] {
|
||||
self.config.get_html_config()
|
||||
.get_additional_js()
|
||||
}
|
||||
|
||||
pub fn has_additional_css(&self) -> bool {
|
||||
self.config.get_html_config()
|
||||
.has_additional_css()
|
||||
}
|
||||
|
||||
pub fn get_additional_css(&self) -> &[PathBuf] {
|
||||
self.config.get_html_config()
|
||||
.get_additional_css()
|
||||
}
|
||||
|
||||
pub fn get_html_config(&self) -> &HtmlConfig {
|
||||
self.config.get_html_config()
|
||||
}
|
||||
|
||||
// Construct book
|
||||
fn parse_summary(&mut self) -> Result<()> {
|
||||
// When append becomes stable, use self.content.append() ...
|
||||
self.content = parse::construct_bookitems(&self.get_source().join("SUMMARY.md"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
229
src/config/bookconfig.rs
Normal file
229
src/config/bookconfig.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
use std::path::{PathBuf, Path};
|
||||
|
||||
use super::HtmlConfig;
|
||||
use super::tomlconfig::TomlConfig;
|
||||
use super::jsonconfig::JsonConfig;
|
||||
|
||||
/// Configuration struct containing all the configuration options available in mdBook.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BookConfig {
|
||||
root: PathBuf,
|
||||
source: PathBuf,
|
||||
|
||||
title: String,
|
||||
authors: Vec<String>,
|
||||
description: String,
|
||||
|
||||
multilingual: bool,
|
||||
indent_spaces: i32,
|
||||
|
||||
html_config: HtmlConfig,
|
||||
}
|
||||
|
||||
impl BookConfig {
|
||||
/// Creates a new `BookConfig` struct with as root path the path given as parameter.
|
||||
/// The source directory is `root/src` and the destination for the rendered book is `root/book`.
|
||||
///
|
||||
/// ```
|
||||
/// # use std::path::PathBuf;
|
||||
/// # use mdbook::config::{BookConfig, HtmlConfig};
|
||||
/// #
|
||||
/// let root = PathBuf::from("directory/to/my/book");
|
||||
/// let config = BookConfig::new(&root);
|
||||
///
|
||||
/// assert_eq!(config.get_root(), &root);
|
||||
/// assert_eq!(config.get_source(), PathBuf::from("directory/to/my/book/src"));
|
||||
/// assert_eq!(config.get_html_config(), &HtmlConfig::new(PathBuf::from("directory/to/my/book")));
|
||||
/// ```
|
||||
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
|
||||
let root: PathBuf = root.into();
|
||||
let htmlconfig = HtmlConfig::new(&root);
|
||||
|
||||
BookConfig {
|
||||
root: root.clone(),
|
||||
source: root.join("src"),
|
||||
|
||||
title: String::new(),
|
||||
authors: Vec::new(),
|
||||
description: String::new(),
|
||||
|
||||
multilingual: false,
|
||||
indent_spaces: 4,
|
||||
|
||||
html_config: htmlconfig,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder method to set the source directory
|
||||
pub fn with_source<T: Into<PathBuf>>(mut self, source: T) -> Self {
|
||||
self.source = source.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder method to set the book's title
|
||||
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
|
||||
self.title = title.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder method to set the book's description
|
||||
pub fn with_description<T: Into<String>>(mut self, description: T) -> Self {
|
||||
self.description = description.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder method to set the book's authors
|
||||
pub fn with_authors<T: Into<Vec<String>>>(mut self, authors: T) -> Self {
|
||||
self.authors = authors.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn from_tomlconfig<T: Into<PathBuf>>(root: T, tomlconfig: TomlConfig) -> Self {
|
||||
let root = root.into();
|
||||
let mut config = BookConfig::new(&root);
|
||||
config.fill_from_tomlconfig(tomlconfig);
|
||||
config
|
||||
}
|
||||
|
||||
pub fn fill_from_tomlconfig(&mut self, tomlconfig: TomlConfig) -> &mut Self {
|
||||
|
||||
if let Some(s) = tomlconfig.source {
|
||||
self.set_source(s);
|
||||
}
|
||||
|
||||
if let Some(t) = tomlconfig.title {
|
||||
self.set_title(t);
|
||||
}
|
||||
|
||||
if let Some(d) = tomlconfig.description {
|
||||
self.set_description(d);
|
||||
}
|
||||
|
||||
if let Some(a) = tomlconfig.authors {
|
||||
self.set_authors(a);
|
||||
}
|
||||
|
||||
if let Some(a) = tomlconfig.author {
|
||||
self.set_authors(vec![a]);
|
||||
}
|
||||
|
||||
if let Some(tomlhtmlconfig) = tomlconfig.output.and_then(|o| o.html) {
|
||||
let root = self.root.clone();
|
||||
self.get_mut_html_config()
|
||||
.fill_from_tomlconfig(root, tomlhtmlconfig);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// The JSON configuration file is **deprecated** and should not be used anymore.
|
||||
/// Please, migrate to the TOML configuration file.
|
||||
pub fn from_jsonconfig<T: Into<PathBuf>>(root: T, jsonconfig: JsonConfig) -> Self {
|
||||
let root = root.into();
|
||||
let mut config = BookConfig::new(&root);
|
||||
config.fill_from_jsonconfig(jsonconfig);
|
||||
config
|
||||
}
|
||||
|
||||
/// The JSON configuration file is **deprecated** and should not be used anymore.
|
||||
/// Please, migrate to the TOML configuration file.
|
||||
pub fn fill_from_jsonconfig(&mut self, jsonconfig: JsonConfig) -> &mut Self {
|
||||
|
||||
if let Some(s) = jsonconfig.src {
|
||||
self.set_source(s);
|
||||
}
|
||||
|
||||
if let Some(t) = jsonconfig.title {
|
||||
self.set_title(t);
|
||||
}
|
||||
|
||||
if let Some(d) = jsonconfig.description {
|
||||
self.set_description(d);
|
||||
}
|
||||
|
||||
if let Some(a) = jsonconfig.author {
|
||||
self.set_authors(vec![a]);
|
||||
}
|
||||
|
||||
if let Some(d) = jsonconfig.dest {
|
||||
let root = self.get_root().to_owned();
|
||||
self.get_mut_html_config()
|
||||
.set_destination(&root, &d);
|
||||
}
|
||||
|
||||
if let Some(d) = jsonconfig.theme_path {
|
||||
let root = self.get_root().to_owned();
|
||||
self.get_mut_html_config()
|
||||
.set_theme(&root, &d);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_root<T: Into<PathBuf>>(&mut self, root: T) -> &mut Self {
|
||||
self.root = root.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
pub fn set_source<T: Into<PathBuf>>(&mut self, source: T) -> &mut Self {
|
||||
let mut source = source.into();
|
||||
|
||||
// If the source path is relative, start with the root path
|
||||
if source.is_relative() {
|
||||
source = self.root.join(source);
|
||||
}
|
||||
|
||||
self.source = source;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_source(&self) -> &Path {
|
||||
&self.source
|
||||
}
|
||||
|
||||
pub fn set_title<T: Into<String>>(&mut self, title: T) -> &mut Self {
|
||||
self.title = title.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
pub fn set_description<T: Into<String>>(&mut self, description: T) -> &mut Self {
|
||||
self.description = description.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_description(&self) -> &str {
|
||||
&self.description
|
||||
}
|
||||
|
||||
pub fn set_authors<T: Into<Vec<String>>>(&mut self, authors: T) -> &mut Self {
|
||||
self.authors = authors.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the authors of the book as specified in the configuration file
|
||||
pub fn get_authors(&self) -> &[String] {
|
||||
self.authors.as_slice()
|
||||
}
|
||||
|
||||
pub fn set_html_config(&mut self, htmlconfig: HtmlConfig) -> &mut Self {
|
||||
self.html_config = htmlconfig;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the configuration for the HTML renderer or None of there isn't any
|
||||
pub fn get_html_config(&self) -> &HtmlConfig {
|
||||
&self.html_config
|
||||
}
|
||||
|
||||
pub fn get_mut_html_config(&mut self) -> &mut HtmlConfig {
|
||||
&mut self.html_config
|
||||
}
|
||||
}
|
||||
173
src/config/htmlconfig.rs
Normal file
173
src/config/htmlconfig.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use std::path::{PathBuf, Path};
|
||||
|
||||
use super::tomlconfig::TomlHtmlConfig;
|
||||
use super::playpenconfig::PlaypenConfig;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct HtmlConfig {
|
||||
destination: PathBuf,
|
||||
theme: PathBuf,
|
||||
curly_quotes: bool,
|
||||
mathjax_support: bool,
|
||||
google_analytics: Option<String>,
|
||||
additional_css: Vec<PathBuf>,
|
||||
additional_js: Vec<PathBuf>,
|
||||
playpen: PlaypenConfig,
|
||||
}
|
||||
|
||||
impl HtmlConfig {
|
||||
/// Creates a new `HtmlConfig` struct containing the configuration parameters for the HTML renderer.
|
||||
///
|
||||
/// ```
|
||||
/// # use std::path::PathBuf;
|
||||
/// # use mdbook::config::HtmlConfig;
|
||||
/// #
|
||||
/// let output = PathBuf::from("root/book");
|
||||
/// let config = HtmlConfig::new(PathBuf::from("root"));
|
||||
///
|
||||
/// assert_eq!(config.get_destination(), &output);
|
||||
/// ```
|
||||
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
|
||||
let root = root.into();
|
||||
let theme = root.join("theme");
|
||||
HtmlConfig {
|
||||
destination: root.clone().join("book"),
|
||||
theme: theme.clone(),
|
||||
curly_quotes: false,
|
||||
mathjax_support: false,
|
||||
google_analytics: None,
|
||||
additional_css: Vec::new(),
|
||||
additional_js: Vec::new(),
|
||||
playpen: PlaypenConfig::new(theme),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_from_tomlconfig<T: Into<PathBuf>>(&mut self, root: T, tomlconfig: TomlHtmlConfig) -> &mut Self {
|
||||
let root = root.into();
|
||||
|
||||
if let Some(d) = tomlconfig.destination {
|
||||
self.set_destination(&root, &d);
|
||||
}
|
||||
|
||||
if let Some(t) = tomlconfig.theme {
|
||||
self.set_theme(&root, &t);
|
||||
}
|
||||
|
||||
if let Some(curly_quotes) = tomlconfig.curly_quotes {
|
||||
self.curly_quotes = curly_quotes;
|
||||
}
|
||||
|
||||
if let Some(mathjax_support) = tomlconfig.mathjax_support {
|
||||
self.mathjax_support = mathjax_support;
|
||||
}
|
||||
|
||||
if tomlconfig.google_analytics.is_some() {
|
||||
self.google_analytics = tomlconfig.google_analytics;
|
||||
}
|
||||
|
||||
if let Some(stylepaths) = tomlconfig.additional_css {
|
||||
for path in stylepaths {
|
||||
if path.is_relative() {
|
||||
self.additional_css.push(root.join(path));
|
||||
} else {
|
||||
self.additional_css.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(scriptpaths) = tomlconfig.additional_js {
|
||||
for path in scriptpaths {
|
||||
if path.is_relative() {
|
||||
self.additional_js.push(root.join(path));
|
||||
} else {
|
||||
self.additional_js.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(playpen) = tomlconfig.playpen {
|
||||
self.playpen.fill_from_tomlconfig(&self.theme, playpen);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_destination<T: Into<PathBuf>>(&mut self, root: T, destination: T) -> &mut Self {
|
||||
let d = destination.into();
|
||||
if d.is_relative() {
|
||||
self.destination = root.into().join(d);
|
||||
} else {
|
||||
self.destination = d;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_destination(&self) -> &Path {
|
||||
&self.destination
|
||||
}
|
||||
|
||||
pub fn get_theme(&self) -> &Path {
|
||||
&self.theme
|
||||
}
|
||||
|
||||
pub fn set_theme<T: Into<PathBuf>>(&mut self, root: T, theme: T) -> &mut Self {
|
||||
let d = theme.into();
|
||||
if d.is_relative() {
|
||||
self.theme = root.into().join(d);
|
||||
} else {
|
||||
self.theme = d;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_curly_quotes(&self) -> bool {
|
||||
self.curly_quotes
|
||||
}
|
||||
|
||||
pub fn set_curly_quotes(&mut self, curly_quotes: bool) {
|
||||
self.curly_quotes = curly_quotes;
|
||||
}
|
||||
|
||||
pub fn get_mathjax_support(&self) -> bool {
|
||||
self.mathjax_support
|
||||
}
|
||||
|
||||
pub fn set_mathjax_support(&mut self, mathjax_support: bool) {
|
||||
self.mathjax_support = mathjax_support;
|
||||
}
|
||||
|
||||
pub fn get_google_analytics_id(&self) -> Option<String> {
|
||||
self.google_analytics.clone()
|
||||
}
|
||||
|
||||
pub fn set_google_analytics_id(&mut self, id: Option<String>) -> &mut Self {
|
||||
self.google_analytics = id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_additional_css(&self) -> bool {
|
||||
!self.additional_css.is_empty()
|
||||
}
|
||||
|
||||
pub fn get_additional_css(&self) -> &[PathBuf] {
|
||||
&self.additional_css
|
||||
}
|
||||
|
||||
pub fn has_additional_js(&self) -> bool {
|
||||
!self.additional_js.is_empty()
|
||||
}
|
||||
|
||||
pub fn get_additional_js(&self) -> &[PathBuf] {
|
||||
&self.additional_js
|
||||
}
|
||||
|
||||
pub fn get_playpen_config(&self) -> &PlaypenConfig {
|
||||
&self.playpen
|
||||
}
|
||||
|
||||
pub fn get_mut_playpen_config(&mut self) -> &mut PlaypenConfig {
|
||||
&mut self.playpen
|
||||
}
|
||||
}
|
||||
42
src/config/jsonconfig.rs
Normal file
42
src/config/jsonconfig.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
extern crate serde_json;
|
||||
use std::path::PathBuf;
|
||||
use errors::*;
|
||||
|
||||
/// The JSON configuration is **deprecated** and will be removed in the near future.
|
||||
/// Please migrate to the TOML configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct JsonConfig {
|
||||
pub src: Option<PathBuf>,
|
||||
pub dest: Option<PathBuf>,
|
||||
|
||||
pub title: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub description: Option<String>,
|
||||
|
||||
pub theme_path: Option<PathBuf>,
|
||||
pub google_analytics: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
/// Returns a `JsonConfig` from a JSON string
|
||||
///
|
||||
/// ```
|
||||
/// # use mdbook::config::jsonconfig::JsonConfig;
|
||||
/// # use std::path::PathBuf;
|
||||
/// let json = r#"{
|
||||
/// "title": "Some title",
|
||||
/// "dest": "htmlbook"
|
||||
/// }"#;
|
||||
///
|
||||
/// let config = JsonConfig::from_json(&json).expect("Should parse correctly");
|
||||
/// assert_eq!(config.title, Some(String::from("Some title")));
|
||||
/// assert_eq!(config.dest, Some(PathBuf::from("htmlbook")));
|
||||
/// ```
|
||||
impl JsonConfig {
|
||||
pub fn from_json(input: &str) -> Result<Self> {
|
||||
let config: JsonConfig = serde_json::from_str(input)
|
||||
.chain_err(|| "Could not parse JSON")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
11
src/config/mod.rs
Normal file
11
src/config/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub mod bookconfig;
|
||||
pub mod htmlconfig;
|
||||
pub mod playpenconfig;
|
||||
pub mod tomlconfig;
|
||||
pub mod jsonconfig;
|
||||
|
||||
// Re-export the config structs
|
||||
pub use self::bookconfig::BookConfig;
|
||||
pub use self::htmlconfig::HtmlConfig;
|
||||
pub use self::playpenconfig::PlaypenConfig;
|
||||
pub use self::tomlconfig::TomlConfig;
|
||||
68
src/config/playpenconfig.rs
Normal file
68
src/config/playpenconfig.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use std::path::{PathBuf, Path};
|
||||
|
||||
use super::tomlconfig::TomlPlaypenConfig;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PlaypenConfig {
|
||||
editor: PathBuf,
|
||||
editable: bool,
|
||||
}
|
||||
|
||||
impl PlaypenConfig {
|
||||
/// Creates a new `PlaypenConfig` for playpen configuration.
|
||||
///
|
||||
/// ```
|
||||
/// # use std::path::PathBuf;
|
||||
/// # use mdbook::config::PlaypenConfig;
|
||||
/// #
|
||||
/// let editor = PathBuf::from("root/editor");
|
||||
/// let config = PlaypenConfig::new(PathBuf::from("root"));
|
||||
///
|
||||
/// assert_eq!(config.get_editor(), &editor);
|
||||
/// assert_eq!(config.is_editable(), false);
|
||||
/// ```
|
||||
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
|
||||
PlaypenConfig {
|
||||
editor: root.into().join("editor"),
|
||||
editable: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_from_tomlconfig<T: Into<PathBuf>>(&mut self, root: T, tomlplaypenconfig: TomlPlaypenConfig) -> &mut Self {
|
||||
let root = root.into();
|
||||
|
||||
if let Some(editor) = tomlplaypenconfig.editor {
|
||||
if editor.is_relative() {
|
||||
self.editor = root.join(editor);
|
||||
} else {
|
||||
self.editor = editor;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(editable) = tomlplaypenconfig.editable {
|
||||
self.editable = editable;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_editable(&self) -> bool {
|
||||
self.editable
|
||||
}
|
||||
|
||||
pub fn get_editor(&self) -> &Path {
|
||||
&self.editor
|
||||
}
|
||||
|
||||
pub fn set_editor<T: Into<PathBuf>>(&mut self, root: T, editor: T) -> &mut Self {
|
||||
let editor = editor.into();
|
||||
|
||||
if editor.is_relative() {
|
||||
self.editor = root.into().join(editor);
|
||||
} else {
|
||||
self.editor = editor;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
61
src/config/tomlconfig.rs
Normal file
61
src/config/tomlconfig.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
extern crate toml;
|
||||
use std::path::PathBuf;
|
||||
use errors::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TomlConfig {
|
||||
pub source: Option<PathBuf>,
|
||||
|
||||
pub title: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub authors: Option<Vec<String>>,
|
||||
pub description: Option<String>,
|
||||
|
||||
pub output: Option<TomlOutputConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TomlOutputConfig {
|
||||
pub html: Option<TomlHtmlConfig>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TomlHtmlConfig {
|
||||
pub destination: Option<PathBuf>,
|
||||
pub theme: Option<PathBuf>,
|
||||
pub google_analytics: Option<String>,
|
||||
pub curly_quotes: Option<bool>,
|
||||
pub mathjax_support: Option<bool>,
|
||||
pub additional_css: Option<Vec<PathBuf>>,
|
||||
pub additional_js: Option<Vec<PathBuf>>,
|
||||
pub playpen: Option<TomlPlaypenConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TomlPlaypenConfig {
|
||||
pub editor: Option<PathBuf>,
|
||||
pub editable: Option<bool>,
|
||||
}
|
||||
|
||||
/// Returns a `TomlConfig` from a TOML string
|
||||
///
|
||||
/// ```
|
||||
/// # use mdbook::config::tomlconfig::TomlConfig;
|
||||
/// # use std::path::PathBuf;
|
||||
/// let toml = r#"title="Some title"
|
||||
/// [output.html]
|
||||
/// destination = "htmlbook" "#;
|
||||
///
|
||||
/// let config = TomlConfig::from_toml(&toml).expect("Should parse correctly");
|
||||
/// assert_eq!(config.title, Some(String::from("Some title")));
|
||||
/// assert_eq!(config.output.unwrap().html.unwrap().destination, Some(PathBuf::from("htmlbook")));
|
||||
/// ```
|
||||
impl TomlConfig {
|
||||
pub fn from_toml(input: &str) -> Result<Self> {
|
||||
let config: TomlConfig = toml::from_str(input)
|
||||
.chain_err(|| "Could not parse TOML")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
68
src/lib.rs
68
src/lib.rs
@@ -21,15 +21,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.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 configuration file
|
||||
//! .expect("I don't handle configuration file errors, but you should!");
|
||||
//!
|
||||
//! book.build().unwrap(); // Render the book
|
||||
//! book.build().unwrap(); // Render the book
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -45,12 +46,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,24 +61,59 @@
|
||||
//!
|
||||
//! ## 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::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.
|
||||
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
pub mod book;
|
||||
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;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
extern crate tempdir;
|
||||
|
||||
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(::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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#[cfg(feature = "debug")]
|
||||
macro_rules! debug {
|
||||
($fmt:expr) => (println!($fmt));
|
||||
($fmt:expr, $($arg:tt)*) => (println!($fmt, $($arg)*));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "debug"))]
|
||||
macro_rules! debug {
|
||||
($fmt:expr) => ();
|
||||
($fmt:expr, $($arg:tt)*) => ();
|
||||
}
|
||||
|
||||
#[cfg(feature = "output")]
|
||||
macro_rules! output {
|
||||
($fmt:expr) => (println!($fmt));
|
||||
($fmt:expr, $($arg:tt)*) => (println!($fmt, $($arg)*));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "output"))]
|
||||
macro_rules! output {
|
||||
($fmt:expr) => ();
|
||||
($fmt:expr, $($arg:tt)*) => ();
|
||||
}
|
||||
@@ -6,10 +6,10 @@ 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)
|
||||
}
|
||||
@@ -22,33 +22,40 @@ 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 }
|
||||
if level < current_level {
|
||||
break;
|
||||
}
|
||||
|
||||
// if level > current_level we call ourselves to go one level deeper
|
||||
if level > current_level {
|
||||
// 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");
|
||||
|
||||
item = if let BookItem::Chapter(ref s, ref ch) = last {
|
||||
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
|
||||
continue;
|
||||
} else {
|
||||
return Err(Error::new( ErrorKind::Other, format!(
|
||||
"Your summary.md is messed up\n\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."
|
||||
)))
|
||||
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 elements can only exist before any chapter and there can be \
|
||||
no chapters after suffix elements."));
|
||||
};
|
||||
|
||||
} else {
|
||||
@@ -58,27 +65,34 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
// 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, format!(
|
||||
"Your summary.md is messed up\n\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."
|
||||
)))
|
||||
BookItem::Affix(_) |
|
||||
BookItem::Spacer if level > 0 => {
|
||||
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 \
|
||||
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, format!(
|
||||
"Your summary.md is messed up\n\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."
|
||||
)))
|
||||
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 \
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
_ => {},
|
||||
}
|
||||
@@ -86,12 +100,14 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
match parsed_item {
|
||||
BookItem::Chapter(_, ch) => {
|
||||
// Increment section
|
||||
let len = section.len() -1;
|
||||
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
|
||||
},
|
||||
_ => parsed_item,
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -131,11 +147,7 @@ fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
|
||||
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)
|
||||
)
|
||||
)
|
||||
return Err(Error::new(ErrorKind::Other, format!("Indentation error on line:\n\n{}", line)));
|
||||
}
|
||||
|
||||
Ok(level)
|
||||
@@ -146,12 +158,12 @@ fn parse_line(l: &str) -> Option<BookItem> {
|
||||
debug!("[fn]: parse_line");
|
||||
|
||||
// Remove leading and trailing spaces or tabs
|
||||
let line = l.trim_matches(|c: char| { c == ' ' || c == '\t' });
|
||||
let line = l.trim_matches(|c: char| c == ' ' || c == '\t');
|
||||
|
||||
// Spacers are "------"
|
||||
if line.starts_with("--") {
|
||||
debug!("[*]: Line is spacer");
|
||||
return Some(BookItem::Spacer)
|
||||
return Some(BookItem::Spacer);
|
||||
}
|
||||
|
||||
if let Some(c) = line.chars().nth(0) {
|
||||
@@ -161,18 +173,22 @@ fn parse_line(l: &str) -> Option<BookItem> {
|
||||
debug!("[*]: Line is list element");
|
||||
|
||||
if let Some((name, path)) = read_link(line) {
|
||||
return Some(BookItem::Chapter("0".to_owned(), Chapter::new(name, path)))
|
||||
} else { return None }
|
||||
return Some(BookItem::Chapter("0".to_owned(), Chapter::new(name, path)));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
// Non-list element
|
||||
'[' => {
|
||||
debug!("[*]: Line is a link element");
|
||||
|
||||
if let Some((name, path)) = read_link(line) {
|
||||
return Some(BookItem::Affix(Chapter::new(name, path)))
|
||||
} else { return None }
|
||||
}
|
||||
_ => {}
|
||||
return Some(BookItem::Affix(Chapter::new(name, path)));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,32 +201,31 @@ fn read_link(line: &str) -> Option<(String, PathBuf)> {
|
||||
|
||||
// In the future, support for list item that is not a link
|
||||
// Not sure if I should error on line I can't parse or just ignore them...
|
||||
if let Some(i) = line.find('[') { start_delimitor = i; }
|
||||
else {
|
||||
if let Some(i) = line.find('[') {
|
||||
start_delimitor = i;
|
||||
} else {
|
||||
debug!("[*]: '[' not found, this line is not a link. Ignoring...");
|
||||
return None
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(i) = line[start_delimitor..].find("](") {
|
||||
end_delimitor = start_delimitor +i;
|
||||
}
|
||||
else {
|
||||
end_delimitor = start_delimitor + i;
|
||||
} else {
|
||||
debug!("[*]: '](' not found, this line is not a link. Ignoring...");
|
||||
return None
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = line[start_delimitor + 1 .. end_delimitor].to_owned();
|
||||
let name = line[start_delimitor + 1..end_delimitor].to_owned();
|
||||
|
||||
start_delimitor = end_delimitor + 1;
|
||||
if let Some(i) = line[start_delimitor..].find(')') {
|
||||
end_delimitor = start_delimitor + i;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
debug!("[*]: ')' not found, this line is not a link. Ignoring...");
|
||||
return None
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = PathBuf::from(line[start_delimitor + 1 .. end_delimitor].to_owned());
|
||||
let path = PathBuf::from(line[start_delimitor + 1..end_delimitor].to_owned());
|
||||
|
||||
Some((name, path))
|
||||
}
|
||||
|
||||
240
src/preprocess/links.rs
Normal file
240
src/preprocess/links.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
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: 90,
|
||||
end_index: 137,
|
||||
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,208 +1,377 @@
|
||||
extern crate handlebars;
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use renderer::html_handlebars::helpers;
|
||||
use preprocess;
|
||||
use renderer::Renderer;
|
||||
use book::MDBook;
|
||||
use book::bookitem::BookItem;
|
||||
use book::bookitem::{BookItem, Chapter};
|
||||
use config::PlaypenConfig;
|
||||
use {utils, theme};
|
||||
use theme::{Theme, playpen_editor};
|
||||
use errors::*;
|
||||
use regex::{Regex, Captures};
|
||||
|
||||
use std::ascii::AsciiExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::File;
|
||||
use std::error::Error;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Read};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use self::handlebars::{Handlebars, JsonRender};
|
||||
use self::rustc_serialize::json::{Json, ToJson};
|
||||
use handlebars::Handlebars;
|
||||
|
||||
use serde_json;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HtmlHandlebars;
|
||||
|
||||
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() => {
|
||||
|
||||
// Load theme
|
||||
let theme = theme::Theme::new(book.get_src());
|
||||
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!"),
|
||||
)?;
|
||||
|
||||
// Register template
|
||||
debug!("[*]: Register handlebars template");
|
||||
try!(handlebars.register_template_string("index", try!(String::from_utf8(theme.index))));
|
||||
// Parse and expand links
|
||||
let content = preprocess::links::replace_all(&content, base)?;
|
||||
let content = utils::render_markdown(&content, ctx.book.get_curly_quotes());
|
||||
print_content.push_str(&content);
|
||||
|
||||
// Register helpers
|
||||
debug!("[*]: Register handlebars helpers");
|
||||
// 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")
|
||||
})?;
|
||||
|
||||
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(
|
||||
"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 filename = Path::new(&ch.path).with_extension("html");
|
||||
let rendered = self.post_process(rendered,
|
||||
filename.file_name().unwrap().to_str().unwrap_or(""),
|
||||
ctx.book.get_html_config().get_playpen_config());
|
||||
|
||||
// Write to file
|
||||
info!("[*] Creating {:?} ✓", filename.display());
|
||||
ctx.book.write_file(filename, &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(())
|
||||
}
|
||||
|
||||
fn post_process(&self, rendered: String, filename: &str, playpen_config: &PlaypenConfig) -> String {
|
||||
let rendered = build_header_links(&rendered, filename);
|
||||
let rendered = fix_anchor_links(&rendered, filename);
|
||||
let rendered = fix_code_blocks(&rendered);
|
||||
let rendered = add_playpen_pre(&rendered, playpen_config);
|
||||
|
||||
rendered
|
||||
}
|
||||
|
||||
fn copy_static_files(&self, book: &MDBook, theme: &Theme) -> 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 = book.get_html_config().get_playpen_config();
|
||||
|
||||
// Ace is a very large dependency, so only load it when requested
|
||||
if playpen_config.is_editable() {
|
||||
// Load the editor
|
||||
let editor = playpen_editor::PlaypenEditor::new(playpen_config.get_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.get_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) {
|
||||
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 custom_files = book.get_additional_css().iter().chain(
|
||||
book.get_additional_js()
|
||||
.iter(),
|
||||
);
|
||||
|
||||
// Print version
|
||||
let mut print_content: String = String::new();
|
||||
|
||||
// Check if dest directory exists
|
||||
debug!("[*]: Check if destination directory exists");
|
||||
if let Err(_) = utils::create_path(book.get_dest()) {
|
||||
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));
|
||||
|
||||
// Render markdown using the pulldown-cmark crate
|
||||
content = utils::render_markdown(&content);
|
||||
print_content.push_str(&content);
|
||||
|
||||
// Remove content from previous file and render content for this one
|
||||
data.remove("path");
|
||||
match ch.path.to_str() {
|
||||
Some(p) => { data.insert("path".to_owned(), p.to_json()); },
|
||||
None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))),
|
||||
}
|
||||
|
||||
|
||||
// Remove content from previous file and render content for this one
|
||||
data.remove("content");
|
||||
data.insert("content".to_owned(), content.to_json());
|
||||
|
||||
// Remove path to root from previous file and render content for this one
|
||||
data.remove("path_to_root");
|
||||
data.insert("path_to_root".to_owned(), utils::path_to_root(&ch.path).to_json());
|
||||
|
||||
// Rendere the handlebars template with the data
|
||||
debug!("[*]: Render template");
|
||||
let rendered = try!(handlebars.render("index", &data));
|
||||
|
||||
debug!("[*]: Create file {:?}", &book.get_dest().join(&ch.path).with_extension("html"));
|
||||
// Write to file
|
||||
let mut file = try!(utils::create_file(&book.get_dest().join(&ch.path).with_extension("html")));
|
||||
output!("[*] Creating {:?} ✓", &book.get_dest().join(&ch.path).with_extension("html"));
|
||||
|
||||
try!(file.write_all(&rendered.into_bytes()));
|
||||
|
||||
// Create an index.html from the first element in SUMMARY.md
|
||||
if index {
|
||||
debug!("[*]: index.html");
|
||||
|
||||
let mut index_file = try!(File::create(book.get_dest().join("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!(index_file.write_all(content.as_bytes()));
|
||||
|
||||
output!(
|
||||
"[*] Creating index.html from {:?} ✓",
|
||||
book.get_dest().join(&ch.path.with_extension("html"))
|
||||
);
|
||||
index = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Print version
|
||||
|
||||
// Remove content from previous file and render content for this one
|
||||
data.remove("path");
|
||||
data.insert("path".to_owned(), "print.md".to_json());
|
||||
|
||||
// Remove content from previous file and render content for this one
|
||||
data.remove("content");
|
||||
data.insert("content".to_owned(), print_content.to_json());
|
||||
|
||||
// Remove path to root from previous file and render content for this one
|
||||
data.remove("path_to_root");
|
||||
data.insert("path_to_root".to_owned(), utils::path_to_root(Path::new("print.md")).to_json());
|
||||
|
||||
// Rendere the handlebars template with the data
|
||||
debug!("[*]: Render template");
|
||||
let rendered = try!(handlebars.render("index", &data));
|
||||
let mut file = try!(utils::create_file(&book.get_dest().join("print").with_extension("html")));
|
||||
try!(file.write_all(&rendered.into_bytes()));
|
||||
output!("[*] Creating print.html ✓");
|
||||
|
||||
// Copy static files (js, css, images, ...)
|
||||
|
||||
debug!("[*] Copy static files");
|
||||
// JavaScript
|
||||
let mut js_file = try!(File::create(book.get_dest().join("book.js")));
|
||||
try!(js_file.write_all(&theme.js));
|
||||
|
||||
// Css
|
||||
let mut css_file = try!(File::create(book.get_dest().join("book.css")));
|
||||
try!(css_file.write_all(&theme.css));
|
||||
|
||||
// JQuery local fallback
|
||||
let mut jquery = try!(File::create(book.get_dest().join("jquery.js")));
|
||||
try!(jquery.write_all(&theme.jquery));
|
||||
|
||||
// Font Awesome local fallback
|
||||
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/css/font-awesome").with_extension("css")));
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME));
|
||||
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.eot")));
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_EOT));
|
||||
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.svg")));
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_SVG));
|
||||
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.ttf")));
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
|
||||
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.woff")));
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF));
|
||||
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.woff2")));
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF2));
|
||||
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/FontAwesome.ttf")));
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
|
||||
|
||||
// syntax highlighting
|
||||
let mut highlight_css = try!(File::create(book.get_dest().join("highlight.css")));
|
||||
try!(highlight_css.write_all(&theme.highlight_css));
|
||||
let mut tomorrow_night_css = try!(File::create(book.get_dest().join("tomorrow-night.css")));
|
||||
try!(tomorrow_night_css.write_all(&theme.tomorrow_night_css));
|
||||
let mut highlight_js = try!(File::create(book.get_dest().join("highlight.js")));
|
||||
try!(highlight_js.write_all(&theme.highlight_js));
|
||||
|
||||
|
||||
// Copy all remaining files
|
||||
try!(utils::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"]));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_data(book: &MDBook) -> Result<BTreeMap<String,Json>, Box<Error>> {
|
||||
|
||||
impl Renderer for HtmlHandlebars {
|
||||
fn render(&self, book: &MDBook) -> Result<()> {
|
||||
debug!("[fn]: render");
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
let theme = theme::Theme::new(book.get_theme_path());
|
||||
|
||||
debug!("[*]: Register handlebars template");
|
||||
handlebars.register_template_string(
|
||||
"index",
|
||||
String::from_utf8(theme.index.clone())?,
|
||||
)?;
|
||||
|
||||
debug!("[*]: Register handlebars helpers");
|
||||
self.register_hbs_helpers(&mut handlebars);
|
||||
|
||||
let mut data = make_data(book)?;
|
||||
|
||||
// Print version
|
||||
let mut print_content = String::new();
|
||||
|
||||
let destination = book.get_destination();
|
||||
|
||||
debug!("[*]: Check if destination directory exists");
|
||||
if fs::create_dir_all(&destination).is_err() {
|
||||
bail!("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,
|
||||
};
|
||||
self.render_item(item, ctx, &mut print_content)?;
|
||||
}
|
||||
|
||||
// Print version
|
||||
self.configure_print_version(&mut data, &print_content);
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("[*]: Render template");
|
||||
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
|
||||
let rendered = self.post_process(rendered, "print.html",
|
||||
book.get_html_config().get_playpen_config());
|
||||
|
||||
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)?;
|
||||
self.copy_additional_css_and_js(book)?;
|
||||
|
||||
// Copy all remaining files
|
||||
utils::fs::copy_files_except_ext(book.get_source(), destination, true, &["md"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
debug!("[fn]: make_data");
|
||||
|
||||
let mut data = BTreeMap::new();
|
||||
data.insert("language".to_owned(), "en".to_json());
|
||||
data.insert("title".to_owned(), book.get_title().to_json());
|
||||
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("favicon".to_owned(), json!("favicon.png"));
|
||||
if let Some(livereload) = book.get_livereload() {
|
||||
data.insert("livereload".to_owned(), json!(livereload));
|
||||
}
|
||||
|
||||
// Add google analytics tag
|
||||
if let Some(ref ga) = book.get_google_analytics_id() {
|
||||
data.insert("google_analytics".to_owned(), json!(ga));
|
||||
}
|
||||
|
||||
if book.get_mathjax_support() {
|
||||
data.insert("mathjax_support".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional style
|
||||
if book.has_additional_css() {
|
||||
let mut css = Vec::new();
|
||||
for style in book.get_additional_css() {
|
||||
match style.strip_prefix(book.get_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 book.has_additional_js() {
|
||||
let mut js = Vec::new();
|
||||
for script in book.get_additional_js() {
|
||||
match script.strip_prefix(book.get_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 book.get_html_config().get_playpen_config().is_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![];
|
||||
|
||||
@@ -212,31 +381,238 @@ fn make_data(book: &MDBook) -> Result<BTreeMap<String,Json>, Box<Error>> {
|
||||
|
||||
match *item {
|
||||
BookItem::Affix(ref ch) => {
|
||||
chapter.insert("name".to_owned(), ch.name.to_json());
|
||||
match ch.path.to_str() {
|
||||
Some(p) => { chapter.insert("path".to_owned(), p.to_json()); },
|
||||
None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))),
|
||||
}
|
||||
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")
|
||||
})?;
|
||||
chapter.insert("path".to_owned(), json!(path));
|
||||
},
|
||||
BookItem::Chapter(ref s, ref ch) => {
|
||||
chapter.insert("section".to_owned(), s.to_json());
|
||||
chapter.insert("name".to_owned(), ch.name.to_json());
|
||||
match ch.path.to_str() {
|
||||
Some(p) => { chapter.insert("path".to_owned(), p.to_json()); },
|
||||
None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))),
|
||||
}
|
||||
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")
|
||||
})?;
|
||||
chapter.insert("path".to_owned(), json!(path));
|
||||
},
|
||||
BookItem::Spacer => {
|
||||
chapter.insert("spacer".to_owned(), "_spacer_".to_json());
|
||||
}
|
||||
chapter.insert("spacer".to_owned(), json!("_spacer_"));
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
chapters.push(chapter);
|
||||
}
|
||||
|
||||
data.insert("chapters".to_owned(), chapters.to_json());
|
||||
data.insert("chapters".to_owned(), json!(chapters));
|
||||
|
||||
debug!("[*]: JSON constructed");
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// 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, filename: &str) -> String {
|
||||
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let level = caps[1].parse().expect(
|
||||
"Regex should ensure we only ever get numbers here",
|
||||
);
|
||||
|
||||
wrap_header_with_link(level, &caps[2], &mut id_counter, filename)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Wraps a single header tag with a link, making sure each tag gets its own
|
||||
/// unique ID by appending an auto-incremented number (if necessary).
|
||||
fn wrap_header_with_link(level: usize, content: &str, id_counter: &mut HashMap<String, usize>, filename: &str)
|
||||
-> String {
|
||||
let raw_id = id_from_content(content);
|
||||
|
||||
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="{filename}#{id}" id="{id}"><h{level}>{text}</h{level}></a>"#,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content,
|
||||
filename = filename
|
||||
)
|
||||
}
|
||||
|
||||
/// 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
|
||||
let repl_sub = vec![
|
||||
"<em>",
|
||||
"</em>",
|
||||
"<code>",
|
||||
"</code>",
|
||||
"<strong>",
|
||||
"</strong>",
|
||||
"<",
|
||||
">",
|
||||
"&",
|
||||
"'",
|
||||
""",
|
||||
];
|
||||
for sub in repl_sub {
|
||||
content = content.replace(sub, "");
|
||||
}
|
||||
|
||||
let mut id = String::new();
|
||||
|
||||
for c in content.chars() {
|
||||
if c.is_alphanumeric() || c == '-' || c == '_' {
|
||||
id.push(c.to_ascii_lowercase());
|
||||
} else if c.is_whitespace() {
|
||||
id.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
// anchors to the same page (href="#anchor") do not work because of
|
||||
// <base href="../"> pointing to the root folder. This function *fixes*
|
||||
// that in a very inelegant way
|
||||
fn fix_anchor_links(html: &str, filename: &str) -> String {
|
||||
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let before = &caps[1];
|
||||
let anchor = &caps[2];
|
||||
let after = &caps[3];
|
||||
|
||||
format!(
|
||||
"<a{before}href=\"{filename}#{anchor}\"{after}>",
|
||||
before = before,
|
||||
filename = filename,
|
||||
anchor = anchor,
|
||||
after = after
|
||||
)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
|
||||
// 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: &str) -> String {
|
||||
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let before = &caps[1];
|
||||
let classes = &caps[2].replace(",", " ");
|
||||
let after = &caps[3];
|
||||
|
||||
format!(r#"<code{before}class="{classes}"{after}>"#, before = before, classes = classes, after = after)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn add_playpen_pre(html: &str, playpen_config: &PlaypenConfig) -> String {
|
||||
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
|
||||
if classes.contains("language-rust") && !classes.contains("ignore") {
|
||||
// wrap the contents in an external pre block
|
||||
if playpen_config.is_editable() &&
|
||||
classes.contains("editable") || text.contains("fn main") || text.contains("quick_main!") {
|
||||
format!("<pre class=\"playpen\">{}</pre>", text)
|
||||
} else {
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
|
||||
format!("<pre class=\"playpen\"><code class=\"{}\">\n# #![allow(unused_variables)]\n\
|
||||
{}#fn main() {{\n\
|
||||
{}\
|
||||
#}}</code></pre>",
|
||||
classes, attrs, code)
|
||||
}
|
||||
} else {
|
||||
// not language-rust, so no-op
|
||||
text.to_owned()
|
||||
}
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn partition_source(s: &str) -> (String, String) {
|
||||
let mut after_header = false;
|
||||
let mut before = String::new();
|
||||
let mut after = String::new();
|
||||
|
||||
for line in s.lines() {
|
||||
let trimline = line.trim();
|
||||
let header = trimline.chars().all(|c| c.is_whitespace()) || trimline.starts_with("#![");
|
||||
if !header || after_header {
|
||||
after_header = true;
|
||||
after.push_str(line);
|
||||
after.push_str("\n");
|
||||
} else {
|
||||
before.push_str(line);
|
||||
before.push_str("\n");
|
||||
}
|
||||
}
|
||||
|
||||
(before, after)
|
||||
}
|
||||
|
||||
|
||||
struct RenderItemContext<'a> {
|
||||
handlebars: &'a Handlebars,
|
||||
book: &'a MDBook,
|
||||
destination: PathBuf,
|
||||
data: serde_json::Map<String, serde_json::Value>,
|
||||
is_index: bool,
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[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="bar.rs#foo" id="foo"><h1>Foo</h1></a>"#),
|
||||
("<h1>Foo</h1>", r#"<a class="header" href="bar.rs#foo" id="foo"><h1>Foo</h1></a>"#),
|
||||
("<h3>Foo^bar</h3>", r#"<a class="header" href="bar.rs#foobar" id="foobar"><h3>Foo^bar</h3></a>"#),
|
||||
("<h4></h4>", r#"<a class="header" href="bar.rs#" id=""><h4></h4></a>"#),
|
||||
("<h4><em>Hï</em></h4>", r#"<a class="header" href="bar.rs#hï" id="hï"><h4><em>Hï</em></h4></a>"#),
|
||||
("<h1>Foo</h1><h3>Foo</h3>",
|
||||
r#"<a class="header" href="bar.rs#foo" id="foo"><h1>Foo</h1></a><a class="header" href="bar.rs#foo-1" id="foo-1"><h3>Foo</h3></a>"#),
|
||||
];
|
||||
|
||||
for (src, should_be) in inputs {
|
||||
let got = build_header_links(src, "bar.rs");
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +1,83 @@
|
||||
extern crate handlebars;
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use self::rustc_serialize::json::{self, ToJson};
|
||||
use self::handlebars::{Handlebars, RenderError, RenderContext, Helper, Context, Renderable};
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Renderable, Context};
|
||||
|
||||
|
||||
// Handlebars helper for navigation
|
||||
|
||||
pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
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 = c.navigate(rc.get_path(), "chapters");
|
||||
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 = c.navigate(rc.get_path(), "path")
|
||||
.to_string()
|
||||
let current = rc.evaluate_absolute("path")?
|
||||
.as_str().ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
|
||||
debug!("[*]: Decode chapters from JSON");
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String, String>> = match json::decode(&chapters.to_string()) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return Err(RenderError{ desc: "Could not decode the JSON data".to_owned()}),
|
||||
};
|
||||
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 {
|
||||
for item in chapters {
|
||||
|
||||
match item.get("path") {
|
||||
Some(path) if !path.is_empty() => {
|
||||
if path == ¤t {
|
||||
|
||||
debug!("[*]: Found current chapter");
|
||||
if let Some(previous) = previous{
|
||||
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(), n.to_json())
|
||||
},
|
||||
None => {
|
||||
debug!("[*]: No title found for chapter");
|
||||
return Err(RenderError{ desc: "No title found for chapter in JSON data".to_owned() })
|
||||
}
|
||||
};
|
||||
previous
|
||||
.get("name").ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
|
||||
.and_then(|n| {
|
||||
previous_chapter.insert("title".to_owned(), json!(n));
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
|
||||
// Chapter link
|
||||
previous
|
||||
.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"))
|
||||
.and_then(|p| {
|
||||
previous_chapter
|
||||
.insert("link".to_owned(), json!(p.replace("\\", "/")));
|
||||
Ok(())
|
||||
})
|
||||
})?;
|
||||
|
||||
match previous.get("path") {
|
||||
Some(p) => {
|
||||
let path = Path::new(p).with_extension("html");
|
||||
debug!("[*]: Inserting link: {:?}", path);
|
||||
|
||||
match path.to_str() {
|
||||
Some(p) => { previous_chapter.insert("link".to_owned(), p.to_json()); },
|
||||
None => return Err(RenderError{ desc: "Link could not be converted to str".to_owned() })
|
||||
}
|
||||
},
|
||||
None => return Err(RenderError{ desc: "No path found for chapter in JSON data".to_owned() })
|
||||
}
|
||||
|
||||
debug!("[*]: Inject in context");
|
||||
// Inject in current context
|
||||
let updated_context = c.extend(&previous_chapter);
|
||||
|
||||
debug!("[*]: Render template");
|
||||
// Render template
|
||||
match _h.template() {
|
||||
Some(t) => {
|
||||
try!(t.render(&updated_context, r, rc));
|
||||
},
|
||||
None => return Err(RenderError{ desc: "Error with the handlebars template".to_owned() })
|
||||
}
|
||||
|
||||
_h.template().ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
||||
.and_then(|t| {
|
||||
let mut local_rc = rc.with_context(Context::wraps(&previous_chapter));
|
||||
t.render(r, &mut local_rc)
|
||||
})?;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
previous = Some(item.clone());
|
||||
}
|
||||
},
|
||||
_ => continue,
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -107,30 +86,24 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
|
||||
|
||||
|
||||
|
||||
pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: next (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 = c.navigate(rc.get_path(), "chapters");
|
||||
|
||||
let current = c.navigate(rc.get_path(), "path")
|
||||
.to_string()
|
||||
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("\"", "");
|
||||
|
||||
debug!("[*]: Decode chapters from JSON");
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String, String>> = match json::decode(&chapters.to_string()) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return Err(RenderError{ desc: "Could not decode the JSON data".to_owned() }),
|
||||
};
|
||||
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 {
|
||||
for item in chapters {
|
||||
|
||||
match item.get("path") {
|
||||
|
||||
@@ -138,10 +111,8 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
|
||||
|
||||
if let Some(previous) = previous {
|
||||
|
||||
let previous_path = match previous.get("path") {
|
||||
Some(p) => p,
|
||||
None => return Err(RenderError{ desc: "No path found for chapter in JSON data".to_owned() })
|
||||
};
|
||||
let previous_path = previous
|
||||
.get("path").ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
|
||||
|
||||
if previous_path == ¤t {
|
||||
|
||||
@@ -150,38 +121,31 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
|
||||
// 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(), n.to_json());
|
||||
}
|
||||
None => return Err(RenderError{ desc: "No title found for chapter in JSON data".to_owned() })
|
||||
}
|
||||
item.get("name").ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
|
||||
.and_then(|n| {
|
||||
next_chapter.insert("title".to_owned(), json!(n));
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
|
||||
let link = Path::new(path).with_extension("html");
|
||||
debug!("[*]: Inserting link: {:?}", link);
|
||||
|
||||
match link.to_str() {
|
||||
Some(l) => { next_chapter.insert("link".to_owned(), l.to_json()); },
|
||||
None => return Err(RenderError{ desc: "Link could not converted to str".to_owned() })
|
||||
}
|
||||
|
||||
debug!("[*]: Inject in context");
|
||||
// Inject in current context
|
||||
let updated_context = c.extend(&next_chapter);
|
||||
Path::new(path)
|
||||
.with_extension("html")
|
||||
.to_str().ok_or_else(|| RenderError::new("Link could not converted to str"))
|
||||
.and_then(|l| {
|
||||
debug!("[*]: Inserting link: {:?}", l);
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
next_chapter.insert("link".to_owned(), json!(l.replace("\\", "/")));
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
debug!("[*]: Render template");
|
||||
|
||||
// Render template
|
||||
match _h.template() {
|
||||
Some(t) => {
|
||||
try!(t.render(&updated_context, r, rc));
|
||||
},
|
||||
None => return Err(RenderError{ desc: "Error with the handlebars template".to_owned() })
|
||||
}
|
||||
|
||||
break
|
||||
_h.template().ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
||||
.and_then(|t| {
|
||||
let mut local_rc = rc.with_context(Context::wraps(&next_chapter));
|
||||
t.render(r, &mut local_rc)
|
||||
})?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,130 +1,140 @@
|
||||
extern crate handlebars;
|
||||
extern crate rustc_serialize;
|
||||
extern crate pulldown_cmark;
|
||||
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use self::rustc_serialize::json;
|
||||
use self::handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
|
||||
use self::pulldown_cmark::{Parser, html, Event, Tag};
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper};
|
||||
use pulldown_cmark::{Parser, html, Event, Tag};
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RenderToc;
|
||||
|
||||
impl HelperDef for RenderToc {
|
||||
fn call(&self, c: &Context, _h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
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 = c.navigate(rc.get_path(), "chapters");
|
||||
let current = c.navigate(rc.get_path(), "path").to_string().replace("\"", "");
|
||||
try!(rc.writer.write("<ul class=\"chapter\">".as_bytes()));
|
||||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = rc.evaluate_absolute("chapters")
|
||||
.and_then(|c| {
|
||||
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>> = json::decode(&chapters.to_string()).unwrap();
|
||||
rc.writer.write_all(b"<ul class=\"chapter\">")?;
|
||||
|
||||
let mut current_level = 1;
|
||||
let mut current_level = 1;
|
||||
|
||||
for item in decoded {
|
||||
for item in chapters {
|
||||
|
||||
// Spacer
|
||||
if let Some(_) = item.get("spacer") {
|
||||
try!(rc.writer.write("<li class=\"spacer\"></li>".as_bytes()));
|
||||
continue
|
||||
}
|
||||
|
||||
let level = if let Some(s) = item.get("section") { s.len() / 2 } else { 1 };
|
||||
|
||||
if level > current_level {
|
||||
try!(rc.writer.write("<li>".as_bytes()));
|
||||
try!(rc.writer.write("<ul class=\"section\">".as_bytes()));
|
||||
try!(rc.writer.write("<li>".as_bytes()));
|
||||
} else if level < current_level {
|
||||
while level < current_level {
|
||||
try!(rc.writer.write("</ul>".as_bytes()));
|
||||
try!(rc.writer.write("</li>".as_bytes()));
|
||||
current_level = current_level - 1;
|
||||
// Spacer
|
||||
if item.get("spacer").is_some() {
|
||||
rc.writer
|
||||
.write_all(b"<li class=\"spacer\"></li>")?;
|
||||
continue;
|
||||
}
|
||||
try!(rc.writer.write("<li>".as_bytes()));
|
||||
}
|
||||
else {
|
||||
try!(rc.writer.write("<li".as_bytes()));
|
||||
if let None = item.get("section") {
|
||||
try!(rc.writer.write(" class=\"affix\"".as_bytes()));
|
||||
}
|
||||
try!(rc.writer.write(">".as_bytes()));
|
||||
}
|
||||
|
||||
// Link
|
||||
let path_exists = if let Some(path) = item.get("path") {
|
||||
if !path.is_empty() {
|
||||
try!(rc.writer.write("<a href=\"".as_bytes()));
|
||||
let level = if let Some(s) = item.get("section") {
|
||||
s.matches('.').count()
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
// Add link
|
||||
try!(rc.writer.write(
|
||||
Path::new(
|
||||
item.get("path")
|
||||
.expect("Error: path should be Some(_)")
|
||||
).with_extension("html")
|
||||
.to_str().unwrap().as_bytes()
|
||||
));
|
||||
|
||||
try!(rc.writer.write("\"".as_bytes()));
|
||||
|
||||
if path == ¤t {
|
||||
try!(rc.writer.write(" class=\"active\"".as_bytes()));
|
||||
if level > current_level {
|
||||
while level > current_level {
|
||||
rc.writer.write_all(b"<li>")?;
|
||||
rc.writer.write_all(b"<ul class=\"section\">")?;
|
||||
current_level += 1;
|
||||
}
|
||||
rc.writer.write_all(b"<li>")?;
|
||||
} else if level < current_level {
|
||||
while level < current_level {
|
||||
rc.writer.write_all(b"</ul>")?;
|
||||
rc.writer.write_all(b"</li>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
rc.writer.write_all(b"<li>")?;
|
||||
} else {
|
||||
rc.writer.write_all(b"<li")?;
|
||||
if item.get("section").is_none() {
|
||||
rc.writer.write_all(b" class=\"affix\"")?;
|
||||
}
|
||||
rc.writer.write_all(b">")?;
|
||||
}
|
||||
|
||||
try!(rc.writer.write(">".as_bytes()));
|
||||
true
|
||||
// Link
|
||||
let path_exists = if let Some(path) = item.get("path") {
|
||||
if !path.is_empty() {
|
||||
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
|
||||
rc.writer.write_all(tmp.as_bytes())?;
|
||||
rc.writer.write_all(b"\"")?;
|
||||
|
||||
if path == ¤t {
|
||||
rc.writer.write_all(b" class=\"active\"")?;
|
||||
}
|
||||
|
||||
rc.writer.write_all(b">")?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Section does not necessarily exist
|
||||
if let Some(section) = item.get("section") {
|
||||
rc.writer.write_all(b"<strong>")?;
|
||||
rc.writer.write_all(section.as_bytes())?;
|
||||
rc.writer.write_all(b"</strong> ")?;
|
||||
}
|
||||
}else {
|
||||
false
|
||||
};
|
||||
|
||||
// Section does not necessarily exist
|
||||
if let Some(section) = item.get("section") {
|
||||
try!(rc.writer.write("<strong>".as_bytes()));
|
||||
try!(rc.writer.write(section.as_bytes()));
|
||||
try!(rc.writer.write("</strong> ".as_bytes()));
|
||||
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,
|
||||
});
|
||||
|
||||
// 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
|
||||
rc.writer.write_all(markdown_parsed_name.as_bytes())?;
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
rc.writer.write_all(b"</a>")?;
|
||||
}
|
||||
|
||||
rc.writer.write_all(b"</li>")?;
|
||||
|
||||
}
|
||||
while current_level > 1 {
|
||||
rc.writer.write_all(b"</ul>")?;
|
||||
rc.writer.write_all(b"</li>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
|
||||
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) => true,
|
||||
&Event::InlineHtml(_) => true,
|
||||
&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(markdown_parsed_name.as_bytes()));
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
try!(rc.writer.write("</a>".as_bytes()));
|
||||
}
|
||||
|
||||
try!(rc.writer.write("</li>".as_bytes()));
|
||||
|
||||
current_level = level;
|
||||
rc.writer.write_all(b"</ul>")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
try!(rc.writer.write("</ul>".as_bytes()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
pub use self::renderer::Renderer;
|
||||
pub use self::html_handlebars::HtmlHandlebars;
|
||||
|
||||
pub mod renderer;
|
||||
mod html_handlebars;
|
||||
|
||||
use errors::*;
|
||||
|
||||
pub trait Renderer {
|
||||
fn render(&self, book: &::book::MDBook) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
use std::error::Error;
|
||||
|
||||
pub trait Renderer {
|
||||
fn render(&self, book: &::book::MDBook) -> Result<(), Box<Error>>;
|
||||
}
|
||||
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,6 +3,13 @@ body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
code {
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
.left {
|
||||
float: left;
|
||||
}
|
||||
@@ -25,8 +32,19 @@ h5 {
|
||||
.header + .header h5 {
|
||||
margin-top: 1em;
|
||||
}
|
||||
table {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table td {
|
||||
padding: 3px 20px;
|
||||
border: 1px solid;
|
||||
}
|
||||
table thead td {
|
||||
font-weight: 700;
|
||||
}
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
@@ -61,7 +79,7 @@ h5 {
|
||||
.chapter {
|
||||
list-style: none outside none;
|
||||
padding-left: 0;
|
||||
line-height: 1.9em;
|
||||
line-height: 2.2em;
|
||||
}
|
||||
.chapter li a {
|
||||
padding: 5px 0;
|
||||
@@ -78,7 +96,7 @@ h5 {
|
||||
.section {
|
||||
list-style: none outside none;
|
||||
padding-left: 20px;
|
||||
line-height: 2.5em;
|
||||
line-height: 1.9em;
|
||||
}
|
||||
.section li {
|
||||
-o-text-overflow: ellipsis;
|
||||
@@ -87,43 +105,31 @@ h5 {
|
||||
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;
|
||||
@@ -194,7 +200,7 @@ h5 {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 50px /* Height of menu-bar */;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
@@ -234,15 +240,31 @@ h5 {
|
||||
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: fixed;
|
||||
left: -40px;
|
||||
-webkit-border-radius: 4px;
|
||||
position: relative;
|
||||
left: 10px;
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
@@ -251,6 +273,12 @@ h5 {
|
||||
padding: 2px 10px;
|
||||
line-height: 25px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.theme-popup .theme:hover:first-child,
|
||||
.theme-popup .theme:hover:last-child {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
}
|
||||
@media only screen and (max-width: 1250px) {
|
||||
.nav-chapters {
|
||||
@@ -274,7 +302,6 @@ h5 {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: 50px;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.next {
|
||||
@@ -285,29 +312,18 @@ h5 {
|
||||
}
|
||||
}
|
||||
.light {
|
||||
/* Inline code */
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
/* Inline code */
|
||||
}
|
||||
.light :not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
.light .content .header:link,
|
||||
.light .content .header:visited {
|
||||
color: #333;
|
||||
pointer: cursor;
|
||||
}
|
||||
.light pre {
|
||||
position: relative;
|
||||
}
|
||||
.light pre > i {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #364149;
|
||||
cursor: pointer;
|
||||
}
|
||||
.light pre > i :hover {
|
||||
color: #008cff;
|
||||
.light .content .header:link:hover,
|
||||
.light .content .header:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.light .sidebar {
|
||||
background-color: #fafafa;
|
||||
@@ -347,40 +363,87 @@ h5 {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.light .content a:link,
|
||||
.light a:visited {
|
||||
.light a:visited,
|
||||
.light a > .hljs {
|
||||
color: #4183c4;
|
||||
}
|
||||
.light .theme-popup {
|
||||
color: #333;
|
||||
background: #fafafa;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.light .theme-popup .theme:hover {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
.coal {
|
||||
/* Inline code */
|
||||
color: #98a3ad;
|
||||
background-color: #141617;
|
||||
.light .theme-popup .default {
|
||||
color: #ccc;
|
||||
}
|
||||
.coal :not(pre) > .hljs {
|
||||
.light blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
color: #333;
|
||||
background-color: #f2f7f9;
|
||||
border-top: 0.1em solid #e1edf1;
|
||||
border-bottom: 0.1em solid #e1edf1;
|
||||
}
|
||||
.light table td {
|
||||
border-color: #f2f2f2;
|
||||
}
|
||||
.light table tbody tr:nth-child(2n) {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.light table thead {
|
||||
background: #ccc;
|
||||
}
|
||||
.light table thead td {
|
||||
border: none;
|
||||
}
|
||||
.light table thead tr {
|
||||
border: 1px #ccc solid;
|
||||
}
|
||||
.light :not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
color: #6e6b5e;
|
||||
}
|
||||
.coal pre {
|
||||
.light a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.light pre {
|
||||
position: relative;
|
||||
}
|
||||
.coal pre > i {
|
||||
.light pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #a1adb8;
|
||||
color: #364149;
|
||||
cursor: pointer;
|
||||
}
|
||||
.coal pre > i :hover {
|
||||
color: #3473ad;
|
||||
.light pre > .buttons :hover {
|
||||
color: #008cff;
|
||||
}
|
||||
.light pre > .buttons i {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.light pre > .result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.coal {
|
||||
color: #98a3ad;
|
||||
background-color: #141617;
|
||||
/* Inline code */
|
||||
}
|
||||
.coal .content .header:link,
|
||||
.coal .content .header:visited {
|
||||
color: #98a3ad;
|
||||
pointer: cursor;
|
||||
}
|
||||
.coal .content .header:link:hover,
|
||||
.coal .content .header:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.coal .sidebar {
|
||||
background-color: #292c2f;
|
||||
@@ -420,40 +483,87 @@ h5 {
|
||||
background-color: #292c2f;
|
||||
}
|
||||
.coal .content a:link,
|
||||
.coal a:visited {
|
||||
.coal a:visited,
|
||||
.coal a > .hljs {
|
||||
color: #2b79a2;
|
||||
}
|
||||
.coal .theme-popup {
|
||||
color: #98a3ad;
|
||||
background: #141617;
|
||||
border: 1px solid #43484d;
|
||||
}
|
||||
.coal .theme-popup .theme:hover {
|
||||
background-color: #1f2124;
|
||||
}
|
||||
.navy {
|
||||
/* Inline code */
|
||||
color: #bcbdd0;
|
||||
background-color: #161923;
|
||||
.coal .theme-popup .default {
|
||||
color: #43484d;
|
||||
}
|
||||
.navy :not(pre) > .hljs {
|
||||
.coal blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
color: #98a3ad;
|
||||
background-color: #242637;
|
||||
border-top: 0.1em solid #2c2f44;
|
||||
border-bottom: 0.1em solid #2c2f44;
|
||||
}
|
||||
.coal table td {
|
||||
border-color: #1f2223;
|
||||
}
|
||||
.coal table tbody tr:nth-child(2n) {
|
||||
background: #1b1d1e;
|
||||
}
|
||||
.coal table thead {
|
||||
background: #3f4649;
|
||||
}
|
||||
.coal table thead td {
|
||||
border: none;
|
||||
}
|
||||
.coal table thead tr {
|
||||
border: 1px #3f4649 solid;
|
||||
}
|
||||
.coal :not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
color: #c5c8c6;
|
||||
}
|
||||
.navy pre {
|
||||
.coal a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.coal pre {
|
||||
position: relative;
|
||||
}
|
||||
.navy pre > i {
|
||||
.coal pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #c8c9db;
|
||||
color: #a1adb8;
|
||||
cursor: pointer;
|
||||
}
|
||||
.navy pre > i :hover {
|
||||
color: #2b79a2;
|
||||
.coal pre > .buttons :hover {
|
||||
color: #3473ad;
|
||||
}
|
||||
.coal pre > .buttons i {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.coal pre > .result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.navy {
|
||||
color: #bcbdd0;
|
||||
background-color: #161923;
|
||||
/* Inline code */
|
||||
}
|
||||
.navy .content .header:link,
|
||||
.navy .content .header:visited {
|
||||
color: #bcbdd0;
|
||||
pointer: cursor;
|
||||
}
|
||||
.navy .content .header:link:hover,
|
||||
.navy .content .header:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.navy .sidebar {
|
||||
background-color: #282d3f;
|
||||
@@ -493,40 +603,87 @@ h5 {
|
||||
background-color: #282d3f;
|
||||
}
|
||||
.navy .content a:link,
|
||||
.navy a:visited {
|
||||
.navy a:visited,
|
||||
.navy a > .hljs {
|
||||
color: #2b79a2;
|
||||
}
|
||||
.navy .theme-popup {
|
||||
color: #bcbdd0;
|
||||
background: #161923;
|
||||
border: 1px solid #737480;
|
||||
}
|
||||
.navy .theme-popup .theme:hover {
|
||||
background-color: #282e40;
|
||||
}
|
||||
.rust {
|
||||
/* Inline code */
|
||||
color: #262625;
|
||||
background-color: #e1e1db;
|
||||
.navy .theme-popup .default {
|
||||
color: #737480;
|
||||
}
|
||||
.rust :not(pre) > .hljs {
|
||||
.navy blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
color: #bcbdd0;
|
||||
background-color: #262933;
|
||||
border-top: 0.1em solid #2f333f;
|
||||
border-bottom: 0.1em solid #2f333f;
|
||||
}
|
||||
.navy table td {
|
||||
border-color: #1f2331;
|
||||
}
|
||||
.navy table tbody tr:nth-child(2n) {
|
||||
background: #1b1f2b;
|
||||
}
|
||||
.navy table thead {
|
||||
background: #39415b;
|
||||
}
|
||||
.navy table thead td {
|
||||
border: none;
|
||||
}
|
||||
.navy table thead tr {
|
||||
border: 1px #39415b solid;
|
||||
}
|
||||
.navy :not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
color: #c5c8c6;
|
||||
}
|
||||
.rust pre {
|
||||
.navy a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.navy pre {
|
||||
position: relative;
|
||||
}
|
||||
.rust pre > i {
|
||||
.navy pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #c8c9db;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rust pre > i :hover {
|
||||
color: #e69f67;
|
||||
.navy pre > .buttons :hover {
|
||||
color: #2b79a2;
|
||||
}
|
||||
.navy pre > .buttons i {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.navy pre > .result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.rust {
|
||||
color: #262625;
|
||||
background-color: #e1e1db;
|
||||
/* Inline code */
|
||||
}
|
||||
.rust .content .header:link,
|
||||
.rust .content .header:visited {
|
||||
color: #262625;
|
||||
pointer: cursor;
|
||||
}
|
||||
.rust .content .header:link:hover,
|
||||
.rust .content .header:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.rust .sidebar {
|
||||
background-color: #3b2e2a;
|
||||
@@ -566,13 +723,268 @@ h5 {
|
||||
background-color: #3b2e2a;
|
||||
}
|
||||
.rust .content a:link,
|
||||
.rust a:visited {
|
||||
.rust a:visited,
|
||||
.rust a > .hljs {
|
||||
color: #2b79a2;
|
||||
}
|
||||
.rust .theme-popup {
|
||||
color: #262625;
|
||||
background: #e1e1db;
|
||||
border: 1px solid #b38f6b;
|
||||
}
|
||||
.rust .theme-popup .theme:hover {
|
||||
background-color: #99908a;
|
||||
}
|
||||
.rust .theme-popup .default {
|
||||
color: #737480;
|
||||
}
|
||||
.rust blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
color: #262625;
|
||||
background-color: #c1c1bb;
|
||||
border-top: 0.1em solid #b8b8b1;
|
||||
border-bottom: 0.1em solid #b8b8b1;
|
||||
}
|
||||
.rust table td {
|
||||
border-color: #d7d7cf;
|
||||
}
|
||||
.rust table tbody tr:nth-child(2n) {
|
||||
background: #dbdbd4;
|
||||
}
|
||||
.rust table thead {
|
||||
background: #b3a497;
|
||||
}
|
||||
.rust table thead td {
|
||||
border: none;
|
||||
}
|
||||
.rust table thead tr {
|
||||
border: 1px #b3a497 solid;
|
||||
}
|
||||
.rust :not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
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;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rust pre > .buttons :hover {
|
||||
color: #e69f67;
|
||||
}
|
||||
.rust pre > .buttons i {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.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 {
|
||||
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,
|
||||
.nav-chapters,
|
||||
.mobile-nav-chapters {
|
||||
display: none;
|
||||
}
|
||||
#page-wrapper {
|
||||
left: 0;
|
||||
overflow-y: initial;
|
||||
}
|
||||
#content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.page {
|
||||
overflow-y: initial;
|
||||
}
|
||||
code {
|
||||
background-color: #666;
|
||||
border-radius: 5px;
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
pre > .buttons {
|
||||
z-index: 2;
|
||||
}
|
||||
a,
|
||||
a:visited,
|
||||
a:active,
|
||||
a:hover {
|
||||
color: #4183c4;
|
||||
text-decoration: none;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: avoid;
|
||||
/*break-after: avoid*/
|
||||
}
|
||||
pre,
|
||||
code {
|
||||
page-break-inside: avoid;
|
||||
white-space: pre-wrap /* CSS 3 */;
|
||||
white-space: -moz-pre-wrap /* Mozilla, since 1999 */;
|
||||
white-space: -pre-wrap /* Opera 4-6 */;
|
||||
white-space: -o-pre-wrap /* Opera 7 */;
|
||||
word-wrap: break-word /* Internet Explorer 5.5+ */;
|
||||
}
|
||||
}
|
||||
.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,21 +7,34 @@ $( document ).ready(function() {
|
||||
window.onunload = function(){};
|
||||
|
||||
// Set theme
|
||||
var theme = localStorage.getItem('theme');
|
||||
if (theme === null) { theme = 'light'; }
|
||||
var theme = store.get('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
|
||||
});
|
||||
|
||||
if (window.ace) {
|
||||
// language-rust class needs to be removed for editable
|
||||
// blocks or highlightjs will capture events
|
||||
$('code.editable').removeClass('language-rust');
|
||||
|
||||
$('code').each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
$('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');
|
||||
|
||||
var KEY_CODES = {
|
||||
PREVIOUS_KEY: 37,
|
||||
@@ -29,6 +42,7 @@ $( document ).ready(function() {
|
||||
};
|
||||
|
||||
$(document).on('keydown', function (e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
switch (e.keyCode) {
|
||||
case KEY_CODES.NEXT_KEY:
|
||||
e.preventDefault();
|
||||
@@ -46,40 +60,22 @@ $( document ).ready(function() {
|
||||
});
|
||||
|
||||
// Interesting DOM Elements
|
||||
var html = $("html");
|
||||
var sidebar = $("#sidebar");
|
||||
var page_wrapper = $("#page-wrapper");
|
||||
var content = $("#content");
|
||||
|
||||
|
||||
// Add anchors for all content headers
|
||||
content.find("h1, h2, h3, h4, h5").wrap(function(){
|
||||
var wrapper = $("<a class=\"header\">");
|
||||
wrapper.attr("name", $(this).text());
|
||||
return wrapper;
|
||||
});
|
||||
|
||||
// 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) {
|
||||
@@ -103,33 +99,63 @@ $( document ).ready(function() {
|
||||
$('.theme-popup').remove();
|
||||
} else {
|
||||
var popup = $('<div class="theme-popup"></div>')
|
||||
.append($('<div class="theme" id="light">Light (default)<div>'))
|
||||
.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>'));
|
||||
|
||||
|
||||
$(this).append(popup);
|
||||
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";
|
||||
}
|
||||
|
||||
if (window.ace && window.editors) {
|
||||
window.editors.forEach(function(editor) {
|
||||
editor.setTheme(ace_theme);
|
||||
});
|
||||
}
|
||||
|
||||
localStorage.setItem('theme', theme);
|
||||
store.set('theme', theme);
|
||||
|
||||
$('body').removeClass().addClass(theme);
|
||||
}
|
||||
@@ -140,18 +166,20 @@ $( document ).ready(function() {
|
||||
|
||||
$("code.language-rust").each(function(i, block){
|
||||
|
||||
var code_block = $(this);
|
||||
var pre_block = $(this).parent();
|
||||
// hide lines
|
||||
var lines = $(this).html().split("\n");
|
||||
var lines = code_block.html().split("\n");
|
||||
var first_non_hidden_line = false;
|
||||
var lines_hidden = false;
|
||||
|
||||
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].substr(1) + "</span>";
|
||||
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)# ?/, "$1") + "</span>";
|
||||
}
|
||||
else {
|
||||
lines[n] = "<span class=\"hidden\">" + lines[n].substr(1) + "\n" + "</span>";
|
||||
lines[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)# ?/, "$1") + "\n" + "</span>";
|
||||
}
|
||||
lines_hidden = true;
|
||||
}
|
||||
@@ -162,25 +190,151 @@ $( document ).ready(function() {
|
||||
first_non_hidden_line = true;
|
||||
}
|
||||
}
|
||||
$(this).html(lines.join(""));
|
||||
code_block.html(lines.join(""));
|
||||
|
||||
// If no lines were hidden, return
|
||||
if(!lines_hidden) { return; }
|
||||
|
||||
// add expand button
|
||||
$(this).parent().prepend("<i class=\"fa fa-expand\"></i>");
|
||||
pre_block.prepend("<div class=\"buttons\"><i class=\"fa fa-expand\"></i></div>");
|
||||
|
||||
$(this).parent().find("i").click(function(e){
|
||||
pre_block.find("i").click(function(e){
|
||||
if( $(this).hasClass("fa-expand") ) {
|
||||
$(this).removeClass("fa-expand").addClass("fa-compress");
|
||||
$(this).parent().find("span.hidden").removeClass("hidden").addClass("unhidden");
|
||||
pre_block.find("span.hidden").removeClass("hidden").addClass("unhidden");
|
||||
}
|
||||
else {
|
||||
$(this).removeClass("fa-compress").addClass("fa-expand");
|
||||
$(this).parent().find("span.unhidden").removeClass("unhidden").addClass("hidden");
|
||||
pre_block.find("span.unhidden").removeClass("unhidden").addClass("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Process playpen code blocks
|
||||
$(".playpen").each(function(block){
|
||||
var pre_block = $(this);
|
||||
// Add play button
|
||||
var buttons = pre_block.find(".buttons");
|
||||
if( buttons.length === 0 ) {
|
||||
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-copy clip-button\"><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\"></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");
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
clipboardSnippets.on('success', function(e) {
|
||||
e.clearSelection();
|
||||
showTooltip(e.trigger, "Copied!");
|
||||
});
|
||||
clipboardSnippets.on('error', function(e) {
|
||||
showTooltip(e.trigger, "Clipboard error!");
|
||||
});
|
||||
});
|
||||
|
||||
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('sidebar', 'visible');
|
||||
} else if ( html.hasClass("sidebar-visible") ) {
|
||||
html.removeClass("sidebar-visible").addClass("sidebar-hidden");
|
||||
store.set('sidebar', 'hidden');
|
||||
} else {
|
||||
if($("#sidebar").position().left === 0){
|
||||
html.addClass("sidebar-hidden");
|
||||
store.set('sidebar', 'hidden');
|
||||
} else {
|
||||
html.addClass("sidebar-visible");
|
||||
store.set('sidebar', 'visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function run_rust_code(code_block) {
|
||||
var result_block = code_block.find(".result");
|
||||
if(result_block.length === 0) {
|
||||
code_block.append("<code class=\"result hljs language-bash\"></code>");
|
||||
result_block = code_block.find(".result");
|
||||
}
|
||||
|
||||
let text;
|
||||
|
||||
let inner_code_block = code_block.find("code").first();
|
||||
if (window.ace && inner_code_block.hasClass("editable")) {
|
||||
let editor = window.ace.edit(inner_code_block.get(0));
|
||||
text = editor.getValue();
|
||||
} else {
|
||||
text = inner_code_block.text();
|
||||
}
|
||||
|
||||
var params = {
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
code: text,
|
||||
};
|
||||
|
||||
if(text.indexOf("#![feature") !== -1) {
|
||||
params.version = "nightly";
|
||||
}
|
||||
|
||||
result_block.text("Running...");
|
||||
|
||||
$.ajax({
|
||||
url: "https://play.rust-lang.org/evaluate.json",
|
||||
method: "POST",
|
||||
crossDomain: true,
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(params),
|
||||
timeout: 15000,
|
||||
success: function(response){
|
||||
result_block.text(response.result);
|
||||
},
|
||||
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
BIN
src/theme/favicon.png
Normal file
BIN
src/theme/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
@@ -1,5 +1,56 @@
|
||||
/* Modified Base16 Atelier Dune Light - Theme
|
||||
/* Original by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */
|
||||
/* 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,
|
||||
.hljs-quote {
|
||||
color: #AAA;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Red */
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-regexp,
|
||||
.hljs-link,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #d73737;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Orange */
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #b65611;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Green */
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #60ac39;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Blue */
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #6684e1;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Purple */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #b854d4;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
@@ -7,101 +58,12 @@
|
||||
background: #f1f1f1;
|
||||
color: #6e6b5e;
|
||||
padding: 0.5em;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
|
||||
/* Atelier-Dune Comment */
|
||||
.hljs-comment {
|
||||
color: #AAA;
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Red */
|
||||
.hljs-variable,
|
||||
|
||||
.hljs-tag,
|
||||
.hljs-regexp,
|
||||
.hljs-name,
|
||||
.ruby .hljs-constant,
|
||||
.xml .hljs-tag .hljs-title,
|
||||
.xml .hljs-pi,
|
||||
.xml .hljs-doctype,
|
||||
.html .hljs-doctype,
|
||||
.css .hljs-id,
|
||||
.css .hljs-class,
|
||||
.css .hljs-pseudo {
|
||||
color: #d73737;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Orange */
|
||||
.hljs-number,
|
||||
.hljs-preprocessor,
|
||||
.hljs-built_in,
|
||||
.hljs-literal,
|
||||
.hljs-params,
|
||||
.hljs-attribute,
|
||||
.hljs-constant {
|
||||
color: #b65611;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Yellow */
|
||||
.ruby .hljs-class .hljs-title,
|
||||
.css .hljs-rule .hljs-attribute {
|
||||
color: #ae9513;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Green */
|
||||
.hljs-string,
|
||||
.hljs-value,
|
||||
.hljs-inheritance,
|
||||
.ruby .hljs-symbol,
|
||||
.xml .hljs-cdata {
|
||||
color: #2a9292;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Aqua */
|
||||
.hljs-title,
|
||||
.css .hljs-hexcolor {
|
||||
color: #1fad83;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Blue */
|
||||
.hljs-function,
|
||||
.python .hljs-decorator,
|
||||
.python .hljs-title,
|
||||
.ruby .hljs-function .hljs-title,
|
||||
.ruby .hljs-title .hljs-keyword,
|
||||
.perl .hljs-sub,
|
||||
.javascript .hljs-title,
|
||||
.coffeescript .hljs-title {
|
||||
color: #6684e1;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Purple */
|
||||
.hljs-keyword,
|
||||
.javascript .hljs-function {
|
||||
color: #b854d4;
|
||||
}
|
||||
|
||||
.coffeescript .javascript,
|
||||
.javascript .xml,
|
||||
.tex .hljs-formula,
|
||||
.xml .javascript,
|
||||
.xml .vbscript,
|
||||
.xml .css,
|
||||
.xml .hljs-cdata {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* markdown */
|
||||
.hljs-header {
|
||||
color: #A30000;
|
||||
}
|
||||
|
||||
.hljs-link_label {
|
||||
color: #33CCCC;
|
||||
}
|
||||
|
||||
.hljs-link_url {
|
||||
color: #CC66FF;
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,44 +2,72 @@
|
||||
<html lang="{{ language }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
<title>{{ chapter_title }} - {{ title }}</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="description" content="{% block description %}{% endblock %}">
|
||||
<meta name="description" content="{{ description }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<base href="{{ path_to_root }}">
|
||||
|
||||
<link rel="stylesheet" href="book.css">
|
||||
<link href='http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/css'>
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
|
||||
|
||||
<link rel="shortcut icon" href="{{ favicon }}">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
|
||||
|
||||
<link rel="stylesheet" href="highlight.css">
|
||||
<link rel="stylesheet" href="tomorrow-night.css">
|
||||
<link rel="stylesheet" href="ayu-highlight.css">
|
||||
|
||||
<!-- Custom theme -->
|
||||
{{#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="http://code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
<script>
|
||||
if (typeof jQuery == 'undefined') {
|
||||
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>
|
||||
|
||||
<!-- Custom JS script -->
|
||||
{{#each additional_js}}
|
||||
<script type="text/javascript" src="{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<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('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('sidebar');
|
||||
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
|
||||
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
|
||||
</script>
|
||||
@@ -50,7 +78,7 @@
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page">
|
||||
<div class="page" tabindex="-1">
|
||||
<div id="menu-bar" class="menu-bar">
|
||||
<div class="left-buttons">
|
||||
<i id="sidebar-toggle" class="fa fa-bars"></i>
|
||||
@@ -70,13 +98,13 @@
|
||||
|
||||
<!-- Mobile navigation buttons -->
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="mobile-nav-chapters previous">
|
||||
<a rel="prev" href="{{link}}" class="mobile-nav-chapters previous">
|
||||
<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">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
{{/next}}
|
||||
@@ -105,6 +133,29 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
{{{livereload}}}
|
||||
|
||||
{{#if google_analytics}}
|
||||
<script>
|
||||
(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}}
|
||||
|
||||
<script src="highlight.js"></script>
|
||||
<script src="book.js"></script>
|
||||
</body>
|
||||
|
||||
201
src/theme/mod.rs
201
src/theme/mod.rs
@@ -1,16 +1,22 @@
|
||||
pub mod playpen_editor;
|
||||
|
||||
use std::path::Path;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
use utils::{PathExt};
|
||||
use errors::*;
|
||||
|
||||
pub static INDEX: &'static [u8] = include_bytes!("index.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");
|
||||
@@ -19,85 +25,156 @@ pub static FONT_AWESOME_WOFF: &'static [u8] = include_bytes!("_FontAwesome/fonts
|
||||
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 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(),
|
||||
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() {
|
||||
return theme
|
||||
// If the theme directory doesn't exist there's no point continuing...
|
||||
if !theme_dir.exists() || !theme_dir.is_dir() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
let src = src.join("theme");
|
||||
// If src does exist, check if there is a theme directory in it
|
||||
if !src.exists() || !src.is_dir() {
|
||||
return theme
|
||||
}
|
||||
// Check for individual files, if they exist copy them across
|
||||
{
|
||||
let files = vec![
|
||||
(theme_dir.join("index.hbs"), &mut theme.index),
|
||||
(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),
|
||||
];
|
||||
|
||||
// Check for individual files if they exist
|
||||
for (filename, dest) in files {
|
||||
if !filename.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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(),
|
||||
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(),
|
||||
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);
|
||||
71
src/theme/playpen_editor/mod.rs
Normal file
71
src/theme/playpen_editor/mod.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
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
@@ -7,3 +7,5 @@
|
||||
@import 'nav-icons'
|
||||
@import 'theme-popup'
|
||||
@import 'themes'
|
||||
@import 'print'
|
||||
@import 'tooltip'
|
||||
|
||||
@@ -3,6 +3,15 @@ html, body {
|
||||
color: #333
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.left {
|
||||
float: left
|
||||
}
|
||||
@@ -19,3 +28,17 @@ h2, h3 { margin-top: 2.5em }
|
||||
h4, h5 { margin-top: 2em }
|
||||
|
||||
.header + .header h3, .header + .header h4, .header + .header h5 { margin-top: 1em }
|
||||
|
||||
table {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
|
||||
td {
|
||||
padding: 3px 20px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
thead {
|
||||
td { font-weight: 700; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,31 +1,42 @@
|
||||
@media only print {
|
||||
|
||||
.sidebar,
|
||||
.menu-bar,
|
||||
#sidebar,
|
||||
#menu-bar,
|
||||
.nav-chapters,
|
||||
.mobile-nav-chapters {
|
||||
display: none
|
||||
display: none
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
left: 0
|
||||
#page-wrapper {
|
||||
left: 0;
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 100%
|
||||
#content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page {
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #666666
|
||||
border-radius: 5px
|
||||
background-color: #666666
|
||||
border-radius: 5px
|
||||
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact
|
||||
}
|
||||
|
||||
pre > .buttons {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
a, a:visited, a:active, a:hover {
|
||||
color: #4183c4
|
||||
text-decoration: none
|
||||
color: #4183c4
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
.chapter {
|
||||
list-style: none outside none
|
||||
padding-left: 0
|
||||
line-height: 1.9em
|
||||
line-height: 2.2em
|
||||
|
||||
li a {
|
||||
padding: 5px 0
|
||||
@@ -54,7 +54,7 @@
|
||||
.section {
|
||||
list-style: none outside none
|
||||
padding-left: 20px
|
||||
line-height: 2.5em
|
||||
line-height: 1.9em
|
||||
|
||||
li {
|
||||
text-overflow: ellipsis
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.theme-popup {
|
||||
position: fixed
|
||||
left: -40px
|
||||
position: relative
|
||||
left: 10px
|
||||
|
||||
z-index: 1000;
|
||||
|
||||
border-radius: 4px
|
||||
font-size: 0.7em
|
||||
@@ -10,7 +12,15 @@
|
||||
padding: 2px 10px
|
||||
line-height: 25px
|
||||
white-space: nowrap
|
||||
cursor: pointer
|
||||
|
||||
&:hover:first-child,
|
||||
&:hover:last-child {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1250px) {
|
||||
|
||||
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'
|
||||
@@ -1,31 +1,17 @@
|
||||
.{unquote($theme-name)} {
|
||||
/* Inline code */
|
||||
:not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
}
|
||||
pre > i {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
|
||||
color: $sidebar-fg;
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
color: $sidebar-active;
|
||||
}
|
||||
}
|
||||
|
||||
color: $fg
|
||||
background-color: $bg
|
||||
|
||||
.content .header:link, .content .header:visited {
|
||||
color: $fg;
|
||||
pointer: cursor;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: $sidebar-bg
|
||||
color: $sidebar-fg
|
||||
@@ -70,14 +56,79 @@
|
||||
background-color: $sidebar-bg
|
||||
}
|
||||
|
||||
.content a:link, a:visited {
|
||||
.content a:link, a:visited, a > .hljs {
|
||||
color: $links
|
||||
}
|
||||
|
||||
.theme-popup {
|
||||
color: $fg
|
||||
background: $theme-popup-bg
|
||||
border: 1px solid $theme-popup-border
|
||||
|
||||
.theme:hover { background-color: $theme-hover }
|
||||
|
||||
.default { color: $icons }
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
color: $fg;
|
||||
background-color: $quote-bg;
|
||||
border-top: .1em solid $quote-border;
|
||||
border-bottom: .1em solid $quote-border;
|
||||
}
|
||||
|
||||
|
||||
table {
|
||||
|
||||
td {
|
||||
border-color: $table-border-color;
|
||||
}
|
||||
|
||||
// Alternate background colors for rows
|
||||
tbody tr:nth-child(2n) {
|
||||
background: $table-alternate-bg;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: $table-header-bg;
|
||||
td { border: none; }
|
||||
tr { border: 1px $table-header-bg solid; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
color: $inline-code-color;
|
||||
}
|
||||
|
||||
a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
|
||||
& > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
|
||||
color: $sidebar-fg;
|
||||
cursor: pointer;
|
||||
|
||||
:hover { color: $sidebar-active; }
|
||||
i { margin-left: 8px; }
|
||||
}
|
||||
|
||||
& > .result { margin-top: 10px; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,17 @@ $icons-hover = #b3c0cc
|
||||
|
||||
$links = #2b79a2
|
||||
|
||||
$inline-code-color = #c5c8c6;
|
||||
|
||||
$theme-popup-bg = #141617
|
||||
$theme-popup-border = #43484d
|
||||
$theme-hover = #1f2124
|
||||
|
||||
$quote-bg = #242637
|
||||
$quote-border = lighten($quote-bg, 5%)
|
||||
|
||||
$table-border-color = lighten($bg, 5%)
|
||||
$table-header-bg = lighten($bg, 20%)
|
||||
$table-alternate-bg = lighten($bg, 3%)
|
||||
|
||||
@import 'base'
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
@import 'coal'
|
||||
@import 'navy'
|
||||
@import 'rust'
|
||||
@import 'ayu'
|
||||
|
||||
@@ -14,8 +14,17 @@ $icons-hover = #333333
|
||||
|
||||
$links = #4183c4
|
||||
|
||||
$inline-code-color = #6e6b5e;
|
||||
|
||||
$theme-popup-bg = #fafafa
|
||||
$theme-popup-border = #cccccc
|
||||
$theme-hover = #e6e6e6
|
||||
|
||||
$quote-bg = #f2f7f9
|
||||
$quote-border = darken($quote-bg, 5%)
|
||||
|
||||
$table-border-color = darken($bg, 5%)
|
||||
$table-header-bg = darken($bg, 20%)
|
||||
$table-alternate-bg = darken($bg, 3%)
|
||||
|
||||
@import 'base'
|
||||
|
||||
@@ -14,8 +14,17 @@ $icons-hover = #b7b9cc
|
||||
|
||||
$links = #2b79a2
|
||||
|
||||
$inline-code-color = #c5c8c6;
|
||||
|
||||
$theme-popup-bg = #161923
|
||||
$theme-popup-border = #737480
|
||||
$theme-hover = #282e40
|
||||
|
||||
$quote-bg = #262933
|
||||
$quote-border = lighten($quote-bg, 5%)
|
||||
|
||||
$table-border-color = lighten($bg, 5%)
|
||||
$table-header-bg = lighten($bg, 20%)
|
||||
$table-alternate-bg = lighten($bg, 3%)
|
||||
|
||||
@import 'base'
|
||||
|
||||
@@ -14,8 +14,17 @@ $icons-hover = #262625
|
||||
|
||||
$links = #2b79a2
|
||||
|
||||
$inline-code-color = #6e6b5e;
|
||||
|
||||
$theme-popup-bg = #e1e1db
|
||||
$theme-popup-border = #b38f6b
|
||||
$theme-hover = #99908a
|
||||
|
||||
$quote-bg = #c1c1bb
|
||||
$quote-border = darken($quote-bg, 5%)
|
||||
|
||||
$table-border-color = darken($bg, 5%)
|
||||
$table-header-bg = #b3a497
|
||||
$table-alternate-bg = darken($bg, 3%)
|
||||
|
||||
@import 'base'
|
||||
|
||||
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
|
||||
|
||||
240
src/utils/fs.rs
Normal file
240
src/utils/fs.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use std::path::{Path, PathBuf, Component};
|
||||
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<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);
|
||||
bail!(e);
|
||||
},
|
||||
};
|
||||
|
||||
let mut content = String::new();
|
||||
|
||||
if let Err(e) = file.read_to_string(&mut content) {
|
||||
debug!("[*]: Failed to read {:?}", path);
|
||||
bail!(e);
|
||||
}
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// ```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), "../../");
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// **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.
|
||||
|
||||
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||
debug!("[fn]: path_to_root");
|
||||
// Remove filename and add "../" for every directory
|
||||
|
||||
path.into()
|
||||
.parent()
|
||||
.expect("")
|
||||
.components()
|
||||
.fold(String::new(), |mut s, c| {
|
||||
match c {
|
||||
Component::Normal(_) => s.push_str("../"),
|
||||
_ => {
|
||||
debug!("[*]: Other path component... {:?}", c);
|
||||
},
|
||||
}
|
||||
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.
|
||||
|
||||
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);
|
||||
|
||||
fs::create_dir_all(p)?;
|
||||
}
|
||||
|
||||
debug!("[*]: Create file: {:?}", 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<()> {
|
||||
for item in fs::read_dir(dir)? {
|
||||
if let Ok(item) = item {
|
||||
let item = item.path();
|
||||
if item.is_dir() {
|
||||
fs::remove_dir_all(item)?;
|
||||
} else {
|
||||
fs::remove_file(item)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// 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<()> {
|
||||
debug!("[fn] copy_files_except_ext");
|
||||
// Check that from and to are different
|
||||
if from == to {
|
||||
return Ok(());
|
||||
}
|
||||
debug!("[*] Loop");
|
||||
for entry in fs::read_dir(from)? {
|
||||
let entry = entry?;
|
||||
debug!("[*] {:?}", entry.path());
|
||||
let metadata = entry.metadata()?;
|
||||
|
||||
// If the entry is a dir and the recursive option is enabled, call itself
|
||||
if metadata.is_dir() && recursive {
|
||||
if entry.path() == to.to_path_buf() {
|
||||
continue;
|
||||
}
|
||||
debug!("[*] is dir");
|
||||
|
||||
// check if output dir already exists
|
||||
if !to.join(entry.file_name()).exists() {
|
||||
fs::create_dir(&to.join(entry.file_name()))?;
|
||||
}
|
||||
|
||||
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()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
debug!("[*] creating path for file: {:?}",
|
||||
&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...")));
|
||||
fs::copy(entry.path(),
|
||||
&to.join(entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
// tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate tempdir;
|
||||
|
||||
use super::copy_files_except_ext;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn copy_files_except_ext_test() {
|
||||
let tmp = match tempdir::TempDir::new("") {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic!("Could not create a temp dir"),
|
||||
};
|
||||
|
||||
// Create a couple of files
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.txt")) {
|
||||
panic!("Could not create file.txt")
|
||||
}
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.md")) {
|
||||
panic!("Could not create file.md")
|
||||
}
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.png")) {
|
||||
panic!("Could not create file.png")
|
||||
}
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir")) {
|
||||
panic!("Could not create sub_dir")
|
||||
}
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir/file.png")) {
|
||||
panic!("Could not create sub_dir/file.png")
|
||||
}
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir_exists")) {
|
||||
panic!("Could not create sub_dir_exists")
|
||||
}
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) {
|
||||
panic!("Could not create sub_dir_exists/file.txt")
|
||||
}
|
||||
|
||||
// Create output dir
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("output")) {
|
||||
panic!("Could not create output")
|
||||
}
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) {
|
||||
panic!("Could not create output/sub_dir_exists")
|
||||
}
|
||||
|
||||
match copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"]) {
|
||||
Err(e) => panic!("Error while executing the function:\n{:?}", e),
|
||||
Ok(_) => {},
|
||||
}
|
||||
|
||||
// Check if the correct files where created
|
||||
if !(&tmp.path().join("output/file.txt")).exists() {
|
||||
panic!("output/file.txt should exist")
|
||||
}
|
||||
if (&tmp.path().join("output/file.md")).exists() {
|
||||
panic!("output/file.md should not exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/file.png")).exists() {
|
||||
panic!("output/file.png should exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/sub_dir/file.png")).exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
406
src/utils/mod.rs
406
src/utils/mod.rs
@@ -1,244 +1,208 @@
|
||||
extern crate pulldown_cmark;
|
||||
pub mod fs;
|
||||
|
||||
use std::path::{Path, PathBuf, Component};
|
||||
use std::error::Error;
|
||||
use std::fs::{self, metadata, File};
|
||||
|
||||
use self::pulldown_cmark::{Parser, html};
|
||||
|
||||
/// This is copied from the rust source code until Path_ Ext stabilizes.
|
||||
/// You can use it, but be aware that it will be removed when those features go to rust stable
|
||||
pub trait PathExt {
|
||||
fn exists(&self) -> bool;
|
||||
fn is_file(&self) -> bool;
|
||||
fn is_dir(&self) -> bool;
|
||||
}
|
||||
|
||||
impl PathExt for Path {
|
||||
fn exists(&self) -> bool {
|
||||
metadata(self).is_ok()
|
||||
}
|
||||
|
||||
fn is_file(&self) -> bool {
|
||||
metadata(self).map(|s| s.is_file()).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_dir(&self) -> bool {
|
||||
metadata(self).map(|s| s.is_dir()).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// ```ignore
|
||||
/// let mut path = Path::new("some/relative/path");
|
||||
///
|
||||
/// println!("{}", 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.
|
||||
|
||||
pub fn path_to_root(path: &Path) -> String {
|
||||
debug!("[fn]: path_to_root");
|
||||
// Remove filename and add "../" for every directory
|
||||
|
||||
path.to_path_buf().parent().expect("")
|
||||
.components().fold(String::new(), |mut s, c| {
|
||||
match c {
|
||||
Component::Normal(_) => s.push_str("../"),
|
||||
_ => {
|
||||
debug!("[*]: Other path component... {:?}", c);
|
||||
}
|
||||
}
|
||||
s
|
||||
})
|
||||
}
|
||||
|
||||
/// This function checks for every component in a path if the directory exists,
|
||||
/// if it does not it is created.
|
||||
|
||||
pub fn create_path(path: &Path) -> Result<(), Box<Error>> {
|
||||
debug!("[fn]: create_path");
|
||||
|
||||
// Create directories if they do not exist
|
||||
let mut constructed_path = PathBuf::new();
|
||||
|
||||
for component in path.components() {
|
||||
|
||||
let dir;
|
||||
match component {
|
||||
Component::Normal(_) => { dir = PathBuf::from(component.as_os_str()); },
|
||||
Component::RootDir => {
|
||||
debug!("[*]: Root directory");
|
||||
// This doesn't look very compatible with Windows...
|
||||
constructed_path.push("/");
|
||||
continue
|
||||
},
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
constructed_path.push(&dir);
|
||||
debug!("[*]: {:?}", constructed_path);
|
||||
|
||||
if !constructed_path.exists() || !constructed_path.is_dir() {
|
||||
try!(fs::create_dir(&constructed_path));
|
||||
debug!("[*]: Directory created {:?}", constructed_path);
|
||||
} else {
|
||||
debug!("[*]: Directory exists {:?}", constructed_path);
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
debug!("[*]: Constructed path: {:?}", constructed_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This function creates a file and returns it. But before creating the file it checks every
|
||||
/// directory in the path to see if it exists, and if it does not it will be created.
|
||||
|
||||
pub fn create_file(path: &Path) -> Result<File, Box<Error>> {
|
||||
debug!("[fn]: create_file");
|
||||
|
||||
// Construct path
|
||||
if let Some(p) = path.parent() {
|
||||
try!(create_path(p));
|
||||
}
|
||||
|
||||
debug!("[*]: Create file: {:?}", path);
|
||||
let f = try!(File::create(path));
|
||||
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
/// 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)) {
|
||||
if let Ok(item) = item {
|
||||
let item = item.path();
|
||||
if item.is_dir() { try!(fs::remove_dir_all(item)); } else { try!(fs::remove_file(item)); }
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// 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>> {
|
||||
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);
|
||||
debug!("[*] {:?}", entry.path());
|
||||
let metadata = try!(entry.metadata());
|
||||
|
||||
// If the entry is a dir and the recursive option is enabled, call itself
|
||||
if metadata.is_dir() && recursive {
|
||||
if entry.path() == to.to_path_buf() { continue }
|
||||
debug!("[*] is dir");
|
||||
|
||||
// check if output dir already exists
|
||||
if !to.join(entry.file_name()).exists() {
|
||||
try!(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
|
||||
));
|
||||
} 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()) { continue }
|
||||
debug!("[*] creating path for file: {:?}", &to.join(entry.path().file_name().expect("a file should have a file name...")));
|
||||
//try!(create_path(&to.join(entry.path())));
|
||||
output!("[*] 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..."))));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
use pulldown_cmark::{Parser, Event, Tag, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
|
||||
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 p = Parser::new(&text);
|
||||
html::push_html(&mut s, p);
|
||||
|
||||
let mut opts = Options::empty();
|
||||
opts.insert(OPTION_ENABLE_TABLES);
|
||||
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
||||
|
||||
let p = Parser::new_ext(text, opts);
|
||||
let mut converter = EventQuoteConverter::new(curly_quotes);
|
||||
let events = p.map(clean_codeblock_headers).map(|event| converter.convert(event));
|
||||
|
||||
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;
|
||||
|
||||
// tests
|
||||
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 {
|
||||
extern crate tempdir;
|
||||
mod render_markdown {
|
||||
use super::super::render_markdown;
|
||||
|
||||
use super::copy_files_except_ext;
|
||||
use super::PathExt;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn copy_files_except_ext_test() {
|
||||
let tmp = match tempdir::TempDir::new("") {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic!("Could not create a temp dir"),
|
||||
};
|
||||
|
||||
// Create a couple of files
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.txt")) { panic!("Could not create file.txt") }
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.md")) { panic!("Could not create file.md") }
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.png")) { panic!("Could not create file.png") }
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir")) { panic!("Could not create sub_dir") }
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir/file.png")) { panic!("Could not create sub_dir/file.png") }
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir_exists")) { panic!("Could not create sub_dir_exists") }
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) { panic!("Could not create sub_dir_exists/file.txt") }
|
||||
|
||||
// Create output dir
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("output")) { panic!("Could not create output") }
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) { panic!("Could not create output/sub_dir_exists") }
|
||||
|
||||
match copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"]) {
|
||||
Err(e) => panic!("Error while executing the function:\n{:?}", e),
|
||||
Ok(_) => {},
|
||||
#[test]
|
||||
fn it_can_keep_quotes_straight() {
|
||||
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
|
||||
}
|
||||
|
||||
// Check if the correct files where created
|
||||
if !(&tmp.path().join("output/file.txt")).exists() { panic!("output/file.txt should exist") }
|
||||
if (&tmp.path().join("output/file.md")).exists() { panic!("output/file.md should not exist") }
|
||||
if !(&tmp.path().join("output/file.png")).exists() { panic!("output/file.png should exist") }
|
||||
if !(&tmp.path().join("output/sub_dir/file.png")).exists() { panic!("output/sub_dir/file.png should exist") }
|
||||
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() { panic!("output/sub_dir/file.png should exist") }
|
||||
#[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’");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
46
tests/config.rs
Normal file
46
tests/config.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
extern crate mdbook;
|
||||
extern crate tempdir;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
use mdbook::MDBook;
|
||||
use tempdir::TempDir;
|
||||
|
||||
// Tests that config values unspecified in the configuration file do not overwrite
|
||||
// values specified earlier.
|
||||
#[test]
|
||||
fn do_not_overwrite_unspecified_config_values() {
|
||||
let dir = TempDir::new("mdbook").expect("Could not create a temp dir");
|
||||
|
||||
let book = MDBook::new(dir.path())
|
||||
.with_source("bar")
|
||||
.with_destination("baz")
|
||||
.with_mathjax_support(true);
|
||||
|
||||
assert_eq!(book.get_root(), dir.path());
|
||||
assert_eq!(book.get_source(), dir.path().join("bar"));
|
||||
assert_eq!(book.get_destination(), dir.path().join("baz"));
|
||||
|
||||
// Test when trying to read a config file that does not exist
|
||||
let book = book.read_config().expect("Error reading the config file");
|
||||
|
||||
assert_eq!(book.get_root(), dir.path());
|
||||
assert_eq!(book.get_source(), dir.path().join("bar"));
|
||||
assert_eq!(book.get_destination(), dir.path().join("baz"));
|
||||
assert_eq!(book.get_mathjax_support(), true);
|
||||
|
||||
// Try with a partial config file
|
||||
let file_path = dir.path().join("book.toml");
|
||||
let mut f = File::create(file_path).expect("Could not create config file");
|
||||
f.write_all(br#"source = "barbaz""#).expect("Could not write to config file");
|
||||
f.sync_all().expect("Could not sync the file");
|
||||
|
||||
let book = book.read_config().expect("Error reading the config file");
|
||||
|
||||
assert_eq!(book.get_root(), dir.path());
|
||||
assert_eq!(book.get_source(), dir.path().join("barbaz"));
|
||||
assert_eq!(book.get_destination(), dir.path().join("baz"));
|
||||
assert_eq!(book.get_mathjax_support(), true);
|
||||
}
|
||||
|
||||
9
tests/dummy-book/SUMMARY.md
Normal file
9
tests/dummy-book/SUMMARY.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# 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/conclusion.md
Normal file
1
tests/dummy-book/conclusion.md
Normal file
@@ -0,0 +1 @@
|
||||
# Conclusion
|
||||
3
tests/dummy-book/first/index.md
Normal file
3
tests/dummy-book/first/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# First Chapter
|
||||
|
||||
more text.
|
||||
7
tests/dummy-book/first/nested.md
Normal file
7
tests/dummy-book/first/nested.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Nested Chapter
|
||||
|
||||
This file has some testable code.
|
||||
|
||||
```rust
|
||||
assert!($TEST_STATUS);
|
||||
```
|
||||
3
tests/dummy-book/intro.md
Normal file
3
tests/dummy-book/intro.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Introduction
|
||||
|
||||
Here's some interesting text...
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user