mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-27 17:23:51 -05:00
Compare commits
810 Commits
v0.0.11-rc
...
version
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dce539928 | ||
|
|
206a00915b | ||
|
|
ced74ca4dd | ||
|
|
09667c9956 | ||
|
|
48c97dadd0 | ||
|
|
65198a7632 | ||
|
|
a0e7b19784 | ||
|
|
7e2e095c26 | ||
|
|
5baaf55abc | ||
|
|
9157f6e32d | ||
|
|
3688f73052 | ||
|
|
3e89e8b1bd | ||
|
|
e08fc148b1 | ||
|
|
d9c1c77aae | ||
|
|
bb2ca4f938 | ||
|
|
42aded9577 | ||
|
|
7fb2d5437a | ||
|
|
4cc3a1333b | ||
|
|
322e8fcf77 | ||
|
|
a8a460545f | ||
|
|
1d69ccae48 | ||
|
|
762d89ebbf | ||
|
|
91ffca1bbc | ||
|
|
6f963bbe3c | ||
|
|
93af92910a | ||
|
|
f30ce0184d | ||
|
|
ccb2340fbe | ||
|
|
bbe6e324d0 | ||
|
|
a776aa9783 | ||
|
|
b8f8e76899 | ||
|
|
ac4e00c7c6 | ||
|
|
67fde37030 | ||
|
|
b2eb1ace08 | ||
|
|
b5fd170008 | ||
|
|
b3665c287d | ||
|
|
436c084b9e | ||
|
|
47f85e71a8 | ||
|
|
1d448fc8cc | ||
|
|
add23a43c2 | ||
|
|
8ba1830750 | ||
|
|
76c1c9e0a8 | ||
|
|
d054140117 | ||
|
|
512826c465 | ||
|
|
99019b74aa | ||
|
|
d87e77edd0 | ||
|
|
abfc3009fc | ||
|
|
028c8b0f75 | ||
|
|
05f3c693a7 | ||
|
|
8b3038e3ef | ||
|
|
bc432c8f42 | ||
|
|
e88970d172 | ||
|
|
4c87a0b5f0 | ||
|
|
ac38f05bb6 | ||
|
|
3119a7e4bf | ||
|
|
cc745d04f2 | ||
|
|
d1a23109e2 | ||
|
|
b3e0942bc9 | ||
|
|
3e998eb766 | ||
|
|
2bbabdcd62 | ||
|
|
a287a0dcc8 | ||
|
|
e7afb3340c | ||
|
|
b4e15e5357 | ||
|
|
1d1d4d7c30 | ||
|
|
35c2d1ff91 | ||
|
|
fd9d27e082 | ||
|
|
0e1787c617 | ||
|
|
e5563182fc | ||
|
|
a08255316a | ||
|
|
ebea8c2337 | ||
|
|
a7342230d5 | ||
|
|
15b18a682b | ||
|
|
a8590928cf | ||
|
|
c9a9987aec | ||
|
|
775377acce | ||
|
|
5dd0496a4f | ||
|
|
f300a21a47 | ||
|
|
0f89abb1c7 | ||
|
|
b5e32f57dc | ||
|
|
b88abb171c | ||
|
|
7c8dd5085b | ||
|
|
4f793af53b | ||
|
|
29b3ff14c7 | ||
|
|
0ac36f2183 | ||
|
|
5835da2432 | ||
|
|
646e3f8fd2 | ||
|
|
da9be67516 | ||
|
|
d9dbba49ea | ||
|
|
384582aeba | ||
|
|
e94078cc9c | ||
|
|
e1a46d213e | ||
|
|
62c8311301 | ||
|
|
b8011de3e8 | ||
|
|
019e74041d | ||
|
|
8cd7061ff2 | ||
|
|
96b99472fd | ||
|
|
4d357b6779 | ||
|
|
1e6328c112 | ||
|
|
21c24c2815 | ||
|
|
bb8b43d396 | ||
|
|
f07e734efc | ||
|
|
db2c16102e | ||
|
|
cae8a8ffe2 | ||
|
|
bdb37ec117 | ||
|
|
01656b610f | ||
|
|
b9ff0e8a77 | ||
|
|
0bda57175d | ||
|
|
374e1d3f94 | ||
|
|
6287e6a44f | ||
|
|
953d3821b6 | ||
|
|
488ace15ff | ||
|
|
b452d5e0c7 | ||
|
|
289028850f | ||
|
|
2a55ff62f3 | ||
|
|
6bf86806e4 | ||
|
|
90bd7207ec | ||
|
|
27b29fdaf2 | ||
|
|
154e0fb308 | ||
|
|
0de177a344 | ||
|
|
f154b2fb65 | ||
|
|
d7759fbf4d | ||
|
|
f84e670edd | ||
|
|
9a9c625319 | ||
|
|
b9ca108fca | ||
|
|
e99dc51fb3 | ||
|
|
7ee5b6643b | ||
|
|
42781bcd6b | ||
|
|
41d372de26 | ||
|
|
69599646e7 | ||
|
|
69fef40e57 | ||
|
|
a323620e02 | ||
|
|
ea0b835b38 | ||
|
|
58f0f3b0f2 | ||
|
|
e7a61efb39 | ||
|
|
d48bc29373 | ||
|
|
72f154bee4 | ||
|
|
1c71eaa964 | ||
|
|
c195aa990d | ||
|
|
34bdcaf8b3 | ||
|
|
41399fc29c | ||
|
|
7f82a197b9 | ||
|
|
71d44933f0 | ||
|
|
f01bf88e69 | ||
|
|
b5ea84c60d | ||
|
|
148c806e34 | ||
|
|
38279deed7 | ||
|
|
55f7ed1c37 | ||
|
|
eb0f7179ab | ||
|
|
5fb3675151 | ||
|
|
77b4f6a940 | ||
|
|
6308da699a | ||
|
|
62a727c041 | ||
|
|
3bc5d907f4 | ||
|
|
3cd12e7092 | ||
|
|
c8bbfd4bc1 | ||
|
|
d48a27f94f | ||
|
|
8c456666ff | ||
|
|
48b0f547c5 | ||
|
|
867fbfec05 | ||
|
|
951c873df6 | ||
|
|
4af155e963 | ||
|
|
07719a8e0e | ||
|
|
cc92d665ca | ||
|
|
b86533b2a1 | ||
|
|
b2ad669c61 | ||
|
|
bb043ef660 | ||
|
|
82aef1bc3f | ||
|
|
38c883e1ef | ||
|
|
8a00a004d8 | ||
|
|
6af77a7792 | ||
|
|
b5ca820345 | ||
|
|
b765023da3 | ||
|
|
d306aed587 | ||
|
|
89a5dbaf9a | ||
|
|
6961247f56 | ||
|
|
07551760c9 | ||
|
|
990daceed5 | ||
|
|
2989096188 | ||
|
|
03c6c44e5b | ||
|
|
31a370d149 | ||
|
|
0bc1030a02 | ||
|
|
43fcd00cd5 | ||
|
|
3d83b784b3 | ||
|
|
5d42738a79 | ||
|
|
1f4dab3e5c | ||
|
|
7181993b43 | ||
|
|
bf9f58e11b | ||
|
|
3ba71c570c | ||
|
|
674e58e747 | ||
|
|
348c5d07c5 | ||
|
|
1790b04e03 | ||
|
|
50ee15472b | ||
|
|
ffb90bb9e2 | ||
|
|
186e649530 | ||
|
|
adc1f4ade7 | ||
|
|
b777a318f7 | ||
|
|
30e3b83167 | ||
|
|
f082187844 | ||
|
|
6119972fa7 | ||
|
|
a910435fd9 | ||
|
|
53b902b479 | ||
|
|
2e9d8671a0 | ||
|
|
50cdfc9623 | ||
|
|
d47f4dce7f | ||
|
|
bda23f0183 | ||
|
|
1fbad982d8 | ||
|
|
fada73eb74 | ||
|
|
dee91b146b | ||
|
|
492f393c0b | ||
|
|
bcfb37d964 | ||
|
|
b1b8ba4b98 | ||
|
|
0531b585e4 | ||
|
|
5379a0bdf8 | ||
|
|
0d146ffa82 | ||
|
|
0bc3544c81 | ||
|
|
c89245b45b | ||
|
|
05e4157c2e | ||
|
|
9fe19d8f31 | ||
|
|
232a923676 | ||
|
|
3d1a311638 | ||
|
|
80f42675d6 | ||
|
|
fa84da0856 | ||
|
|
947d0312c6 | ||
|
|
05a0d7e625 | ||
|
|
7b356b7530 | ||
|
|
d44b67bbcc | ||
|
|
7b4b70a49d | ||
|
|
1136f671a0 | ||
|
|
80a20eb730 | ||
|
|
0d62578c7b | ||
|
|
57f960a03b | ||
|
|
47cc57177d | ||
|
|
90fa1b4909 | ||
|
|
f2d7b705af | ||
|
|
4177288b11 | ||
|
|
b599956516 | ||
|
|
08027b86cc | ||
|
|
b98ed3f794 | ||
|
|
9c922cf26b | ||
|
|
144358bec6 | ||
|
|
4cc708e00f | ||
|
|
12815fe399 | ||
|
|
f282a553fd | ||
|
|
966811061b | ||
|
|
cad76a9f6c | ||
|
|
01df904bb3 | ||
|
|
5a4adcce53 | ||
|
|
bf093e2f5f | ||
|
|
be9a524eeb | ||
|
|
b0e91193e9 | ||
|
|
61fad2786b | ||
|
|
9ab54412ea | ||
|
|
be949ceae8 | ||
|
|
a1b6ccc29a | ||
|
|
e825357848 | ||
|
|
fd8f3bb415 | ||
|
|
fd7e8d1b7b | ||
|
|
dedc208a6a | ||
|
|
e461610dab | ||
|
|
e791f250fa | ||
|
|
cd1abf8ec2 | ||
|
|
2b943d703f | ||
|
|
b9ab71b231 | ||
|
|
7c81335c9a | ||
|
|
e74c376833 | ||
|
|
6ba0162ff7 | ||
|
|
b5382f49c6 | ||
|
|
daade63c27 | ||
|
|
9480ac0ea6 | ||
|
|
65900b87e6 | ||
|
|
d4a5176f26 | ||
|
|
c721ba82b2 | ||
|
|
bd134fbaa4 | ||
|
|
cba16a0083 | ||
|
|
549a9ffd54 | ||
|
|
06c9d67e75 | ||
|
|
61356ce5fe | ||
|
|
31fb443562 | ||
|
|
a2759b7bd2 | ||
|
|
71689da6b1 | ||
|
|
6e90e520d6 | ||
|
|
65acb355d7 | ||
|
|
89eff2d824 | ||
|
|
a280a3003b | ||
|
|
cb6f2289cf | ||
|
|
396426662d | ||
|
|
718d251c7f | ||
|
|
d69bc9c7c3 | ||
|
|
a46e2e2b27 | ||
|
|
75dac15f09 | ||
|
|
5041359817 | ||
|
|
ff9e0b0add | ||
|
|
148511eceb | ||
|
|
f5e9b857de | ||
|
|
4f4e86db3a | ||
|
|
ebcf41c25b | ||
|
|
1b51cd244e | ||
|
|
be4654c9c2 | ||
|
|
ace0b51fb6 | ||
|
|
9950f69c48 | ||
|
|
12d1ed5558 | ||
|
|
751da4f05f | ||
|
|
527fc5cf79 | ||
|
|
f993677626 | ||
|
|
21498631b3 | ||
|
|
8b21da9950 | ||
|
|
47eb4788cb | ||
|
|
cafb8b75e7 | ||
|
|
4c6c696c87 | ||
|
|
3838fa0e68 | ||
|
|
4619ab60b0 | ||
|
|
2a5409db20 | ||
|
|
dc89a82329 | ||
|
|
42ff5a895c | ||
|
|
8ee795045a | ||
|
|
f22835f7bc | ||
|
|
1f84f66041 | ||
|
|
9143110a43 | ||
|
|
e735bc6d3e | ||
|
|
803df90efa | ||
|
|
3b136689ee | ||
|
|
b614b0fd65 | ||
|
|
32df76d077 | ||
|
|
dacc274e0d | ||
|
|
b0b09bad3f | ||
|
|
93874edebf | ||
|
|
1aa9c92ac1 | ||
|
|
5ce05a79be | ||
|
|
c51e080783 | ||
|
|
dd5d94393d | ||
|
|
3d5eb48e32 | ||
|
|
d56ff94ce6 | ||
|
|
fb99276f52 | ||
|
|
5eff572dbb | ||
|
|
238dfb7d1d | ||
|
|
c777913136 | ||
|
|
c25c5d72c8 | ||
|
|
3aa6436679 | ||
|
|
d37821c194 | ||
|
|
1b5137c84e | ||
|
|
18c725ee12 | ||
|
|
1743f2a39f | ||
|
|
cee3296a32 | ||
|
|
ddb0834da8 | ||
|
|
b74c2c18ef | ||
|
|
c056b5cbd0 | ||
|
|
8d7970b32d | ||
|
|
1d22a9a040 | ||
|
|
6059883229 | ||
|
|
8960013322 | ||
|
|
79dd03e8e9 | ||
|
|
aecc403fb8 | ||
|
|
6e4d2485c3 | ||
|
|
cd711bfb1c | ||
|
|
afd9ccb7b1 | ||
|
|
cb5ae21b89 | ||
|
|
dd3bef8000 | ||
|
|
7e5892bd35 | ||
|
|
56cee872e8 | ||
|
|
a554390aa2 | ||
|
|
c64384abc3 | ||
|
|
ba7d40284b | ||
|
|
8f6523a94c | ||
|
|
8fbc59720d | ||
|
|
ac9c150902 | ||
|
|
f2e56c887b | ||
|
|
b4a12fa723 | ||
|
|
382fc4139b | ||
|
|
b45e5e4420 | ||
|
|
a6d4881e00 | ||
|
|
a0515bd104 | ||
|
|
9b64db908f | ||
|
|
f562878131 | ||
|
|
3823fc0e74 | ||
|
|
793fb8f654 | ||
|
|
911683d2cf | ||
|
|
2ae6e6a6e3 | ||
|
|
91fd8a2865 | ||
|
|
a3b6e549e2 | ||
|
|
d450518292 | ||
|
|
c056df597a | ||
|
|
0d6adc5fc9 | ||
|
|
0226da91e4 | ||
|
|
ef5895fa78 | ||
|
|
8e0abfb22f | ||
|
|
c9bc13d786 | ||
|
|
7ce78cbfea | ||
|
|
26544fa531 | ||
|
|
0c93770f4a | ||
|
|
743713ad3a | ||
|
|
e3f4bb5101 | ||
|
|
441bcb5963 | ||
|
|
84ef4d2617 | ||
|
|
bd30cae17e | ||
|
|
6f0b67f44f | ||
|
|
abf86eefd9 | ||
|
|
016ec8836c | ||
|
|
881a1b39ff | ||
|
|
a1e58229b2 | ||
|
|
276eab095c | ||
|
|
f4513d3b5c | ||
|
|
570ce6681f | ||
|
|
ddee839d9c | ||
|
|
99945542ca | ||
|
|
956a5cc7fd | ||
|
|
cef62ec42e | ||
|
|
b1362bfa06 | ||
|
|
a529ca5e65 | ||
|
|
6bc3039b4f | ||
|
|
cd90fdd407 | ||
|
|
a6a7c95c78 | ||
|
|
0a4a2b66da | ||
|
|
2f3c14d609 | ||
|
|
ebcf1e495d | ||
|
|
40a4840867 | ||
|
|
313f9b9403 | ||
|
|
d94c097495 | ||
|
|
b7372d3bf2 | ||
|
|
d0a6aea3aa | ||
|
|
a1926bbe8e | ||
|
|
094c1e7a52 | ||
|
|
4528e24080 | ||
|
|
31983cae6c | ||
|
|
ddf31dcc08 | ||
|
|
c36eca15c2 | ||
|
|
798225bcdc | ||
|
|
eed1a0a591 | ||
|
|
35a447d08a | ||
|
|
16aa545c5b | ||
|
|
6601dbdd61 | ||
|
|
373e36ebfb | ||
|
|
ef435825b0 | ||
|
|
2f8d5ce263 | ||
|
|
ce2d7153f7 | ||
|
|
6628757d8e | ||
|
|
e2eb40bded | ||
|
|
4f754a73ba | ||
|
|
ba719c00be | ||
|
|
64f6f78663 | ||
|
|
3b0d2d1238 | ||
|
|
6ab3d3da2a | ||
|
|
ee29b9d5f6 | ||
|
|
d7ecb1a80c | ||
|
|
f3f6b40ea9 | ||
|
|
c482650e56 | ||
|
|
5da75bc798 | ||
|
|
07b80723b6 | ||
|
|
0c3a2b80f8 | ||
|
|
29e00c0d97 | ||
|
|
7e8819f4d2 | ||
|
|
c90c0f7848 | ||
|
|
cd11035a69 | ||
|
|
8b7c95e02f | ||
|
|
e3f047a35d | ||
|
|
9fab267da1 | ||
|
|
55e7e82e5c | ||
|
|
325458c957 | ||
|
|
9c21fe32c1 | ||
|
|
eaec9eff37 | ||
|
|
287f539b7d | ||
|
|
a220528c15 | ||
|
|
7c023e2d1d | ||
|
|
f2544e0707 | ||
|
|
4974d2cfa1 | ||
|
|
b1ca9cf5b5 | ||
|
|
5a27207844 | ||
|
|
5e088d92c9 | ||
|
|
ea0c8ddea6 | ||
|
|
b3c9ba4555 | ||
|
|
7f51039f9a | ||
|
|
7799ce285e | ||
|
|
fe62d0c407 | ||
|
|
c9a117cc4e | ||
|
|
13ab20ea49 | ||
|
|
f3c8535870 | ||
|
|
35ed9fc286 | ||
|
|
efdd0330c1 | ||
|
|
4c78fdf431 | ||
|
|
b09fdf07e4 | ||
|
|
5c524da3c2 | ||
|
|
99224f40d5 | ||
|
|
83354ab24b | ||
|
|
af05306046 | ||
|
|
b796ee7c36 | ||
|
|
db94b3d839 | ||
|
|
f214c7108f | ||
|
|
2abebfb244 | ||
|
|
fd821a5ead | ||
|
|
487f5ce339 | ||
|
|
1356e0f068 | ||
|
|
0f93cd002b | ||
|
|
6761442241 | ||
|
|
b441066105 | ||
|
|
d50486e337 | ||
|
|
c3dfabd5a2 | ||
|
|
4c187bcb9f | ||
|
|
d42ef1cdbc | ||
|
|
03193e0bd7 | ||
|
|
672d91e6c2 | ||
|
|
6d8ac6a23c | ||
|
|
69b3e2b5cb | ||
|
|
5e93decf6e | ||
|
|
79cdcb46de | ||
|
|
f889eb3d12 | ||
|
|
3306c030e1 | ||
|
|
239886a5cf | ||
|
|
f3cb4265ca | ||
|
|
28afebdca2 | ||
|
|
ab31f4b027 | ||
|
|
4128a78171 | ||
|
|
7f60db069f | ||
|
|
26fc980ffb | ||
|
|
d252dc82d6 | ||
|
|
c186d72b40 | ||
|
|
73160877b3 | ||
|
|
33f3bec301 | ||
|
|
8c30de16d6 | ||
|
|
fa95546988 | ||
|
|
ac16d7aef1 | ||
|
|
e2a7adaa79 | ||
|
|
75f0196c55 | ||
|
|
49336e0698 | ||
|
|
f0c697afd5 | ||
|
|
cff1ed5e08 | ||
|
|
73c845fbbe | ||
|
|
193f014a5b | ||
|
|
bd9b0d29ea | ||
|
|
4af10ce60c | ||
|
|
29708db467 | ||
|
|
deab3ba751 | ||
|
|
c1c06d6dc1 | ||
|
|
b7aa78c3c0 | ||
|
|
2568986fd5 | ||
|
|
f946ef6327 | ||
|
|
0d0deb7c40 | ||
|
|
e8908e32c9 | ||
|
|
9e9a08806d | ||
|
|
ee9fa8c86f | ||
|
|
e890579141 | ||
|
|
9aa39a6a12 | ||
|
|
6ee6da074e | ||
|
|
2bb274d424 | ||
|
|
19692c76df | ||
|
|
a6275ebcdb | ||
|
|
6a279e2775 | ||
|
|
9ce6eebe43 | ||
|
|
0b6378eb13 | ||
|
|
350c86155b | ||
|
|
ad9bda2d69 | ||
|
|
08fd255a56 | ||
|
|
f607978780 | ||
|
|
f96e7e5cba | ||
|
|
6c279453d9 | ||
|
|
56163f69f8 | ||
|
|
a9862a56b3 | ||
|
|
eba90f5440 | ||
|
|
44efc65c63 | ||
|
|
8a05f0d499 | ||
|
|
f1121cf8c2 | ||
|
|
1a8e54bb52 | ||
|
|
23efa9e146 | ||
|
|
bb4ceb481f | ||
|
|
c6bfe0b1d7 | ||
|
|
2e812db13c | ||
|
|
70383d0a25 | ||
|
|
d3ae2eda56 | ||
|
|
170bf8b1eb | ||
|
|
272022621d | ||
|
|
be3418a269 | ||
|
|
3e80268a44 | ||
|
|
3a809e4a1c | ||
|
|
dfc24bec01 | ||
|
|
e567d22f1c | ||
|
|
bfc3fbb405 | ||
|
|
8bfcd9939c | ||
|
|
316bcf7b5d | ||
|
|
453b97bec0 | ||
|
|
4364ec3a7b | ||
|
|
7de24f86a9 | ||
|
|
027c21aef7 | ||
|
|
64f0bdbfba | ||
|
|
cc1cb9edb0 | ||
|
|
35ef31757b | ||
|
|
87f26a82b6 | ||
|
|
1d141aa27b | ||
|
|
a84c1ecf33 | ||
|
|
2518d5c827 | ||
|
|
3f98d69690 | ||
|
|
9c8c819ec3 | ||
|
|
f038dcb404 | ||
|
|
75bbd55128 | ||
|
|
faa716d99c | ||
|
|
b1601b16ea | ||
|
|
379ed9dc16 | ||
|
|
7b6836b75e | ||
|
|
e4dd03c8f0 | ||
|
|
c99ce06370 | ||
|
|
0f67cae1ef | ||
|
|
0443f8a709 | ||
|
|
1442923a0a | ||
|
|
9447274fa5 | ||
|
|
7a59591109 | ||
|
|
d636ca45e8 | ||
|
|
832ef446dd | ||
|
|
74e75d2cfb | ||
|
|
95750be815 | ||
|
|
4ad6fab5e3 | ||
|
|
94dce4f796 | ||
|
|
ada1f29b34 | ||
|
|
4a634f08da | ||
|
|
e8b88cfa5e | ||
|
|
3066597acc | ||
|
|
d1f9174e7f | ||
|
|
7eade3b101 | ||
|
|
435682e95c | ||
|
|
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 |
8
.gitattributes
vendored
Normal file
8
.gitattributes
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
[attr]rust text eol=lf whitespace=tab-in-indent,trailing-space,tabwidth=4
|
||||
|
||||
* text=auto eol=lf
|
||||
*.rs rust
|
||||
*.woff -text
|
||||
*.ttf -text
|
||||
*.otf -text
|
||||
*.png -text
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,5 +1,11 @@
|
||||
Cargo.lock
|
||||
target
|
||||
|
||||
# MacOS temp file
|
||||
.DS_Store
|
||||
|
||||
book-test
|
||||
book-example/book
|
||||
|
||||
.vscode
|
||||
tests/dummy_book/book/
|
||||
|
||||
|
||||
117
.travis.yml
117
.travis.yml
@@ -1,91 +1,28 @@
|
||||
sudo: false
|
||||
|
||||
language: rust
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
cache:
|
||||
timeout: 360
|
||||
cargo: true
|
||||
|
||||
before_cache:
|
||||
- chmod -R a+r $HOME/.cargo
|
||||
|
||||
env:
|
||||
global:
|
||||
- PROJECT_NAME=mdBook
|
||||
|
||||
matrix:
|
||||
include:
|
||||
# Stable channel
|
||||
# TODO re-enable the i686-apple-darwin target when passcod/rsnotify#53 is fixed
|
||||
#- os: osx
|
||||
#env: TARGET=i686-apple-darwin
|
||||
#rust: stable
|
||||
- os: linux
|
||||
env: TARGET=i686-unknown-linux-gnu
|
||||
rust: stable
|
||||
addons:
|
||||
apt:
|
||||
packages: &i686_unknown_linux_gnu
|
||||
- gcc-multilib
|
||||
- os: osx
|
||||
env: TARGET=x86_64-apple-darwin
|
||||
rust: stable
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
rust: stable
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- nodejs
|
||||
- npm
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl
|
||||
rust: stable
|
||||
# Beta channel
|
||||
#- os: osx
|
||||
#env: TARGET=i686-apple-darwin
|
||||
#rust: beta
|
||||
- os: linux
|
||||
env: TARGET=i686-unknown-linux-gnu
|
||||
rust: beta
|
||||
addons:
|
||||
apt:
|
||||
packages: *i686_unknown_linux_gnu
|
||||
- os: osx
|
||||
env: TARGET=x86_64-apple-darwin
|
||||
rust: beta
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
rust: beta
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl
|
||||
rust: beta
|
||||
# Nightly channel
|
||||
#- os: osx
|
||||
#env: TARGET=i686-apple-darwin
|
||||
#rust: nightly
|
||||
- os: linux
|
||||
env: TARGET=i686-unknown-linux-gnu
|
||||
rust: nightly
|
||||
addons:
|
||||
apt:
|
||||
packages: *i686_unknown_linux_gnu
|
||||
- os: osx
|
||||
env: TARGET=x86_64-apple-darwin
|
||||
rust: nightly
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
rust: nightly
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl
|
||||
rust: nightly
|
||||
|
||||
install:
|
||||
- sh ci/install.sh
|
||||
- CRATE_NAME=mdbook
|
||||
|
||||
script:
|
||||
- sh ci/script.sh
|
||||
|
||||
after_success:
|
||||
- test $TRAVIS_PULL_REQUEST == "false" &&
|
||||
test $TRAVIS_BRANCH == "master" &&
|
||||
test $TARGET == "x86_64-unknown-linux-gnu" &&
|
||||
test $TRAVIS_RUST_VERSION = "stable" &&
|
||||
npm install stylus nib &&
|
||||
bash deploy.sh
|
||||
- cargo test --all
|
||||
- cargo test --all --no-default-features
|
||||
|
||||
before_deploy:
|
||||
- sh ci/before_deploy.sh
|
||||
@@ -93,14 +30,18 @@ before_deploy:
|
||||
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
|
||||
- secure: cURRWBr034iqBz/ifD7uOunBfNR30YxIXfgLX0osWz+iafkVbhDGYYz9sBmRraqO2P7L2koEXMADVb/md1kI2+ykiq/ml+l9zuEAZPVmvSGUN7ZD+7s+lu3l5OBPG5z175T+b2q2q2m8XVR7TW20ra4QbE0bq06KAoOyjSgQVBTSCYsL9uTsGwiVRMEqqJT/BmKhKJNkpGsTKyBSKkOXvfeAAbE260vXUDEN9TYdJ3fvteRrpwLX56ee64gIZUq0RjDc4SKIEqilM6iUtNMvurqaewYNGkiXKRruV6BPCHxEHo6NNT46kOJLBJTf7gZw//dWhSoWpg9P0gdAnPWm407kSa3F7aJ1eRShAFQ4BLyfz9efTqm+jP3fOp7Mm7igSh9w6caSRuOnSsUf5+raRQ8E5Y9HsWGzzpZQk24Fx9EGZ04EeDSdpZAFz+jcbMpHf8t2p4CEx0CCNwYvKx6EydMKbMF5QteQ8SQkXNLhv7Rz2OgtXWYZPRVCMfQfOplsi2InsLCrQxTgwh+6u654SqVSgaHG+IncEAxBrdWy4rHcg7qereUcKfcY3k96vaDxdn/T2c00Ig0aNFR91YnixGMd6J6tQgDcRK9jh6fUm1CCBE9hT+pNUmtgYKuWBoLZexUZFFnfuBed0WciBot1bGDDamndqKq0jJiAzg+GMHk=
|
||||
file_glob: true
|
||||
file: "$CRATE_NAME-$TRAVIS_TAG-$TARGET.*"
|
||||
on:
|
||||
condition: $TRAVIS_RUST_VERSION = stable
|
||||
condition: "$TRAVIS_RUST_VERSION = stable"
|
||||
tags: true
|
||||
skip_cleanup: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
|
||||
notifications:
|
||||
email:
|
||||
|
||||
60
CONTRIBUTING.md
Normal file
60
CONTRIBUTING.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Contributing
|
||||
|
||||
Welcome stranger!
|
||||
|
||||
If you have come here to learn how to contribute to mdBook, we have some tips for you!
|
||||
|
||||
First of all, don't hesitate to ask questions!
|
||||
Use the [issue tracker](https://github.com/rust-lang-nursery/mdBook/issues), no question is too simple.
|
||||
If we don't respond in a couple of days, ping us @Michael-F-Bryan, @budziq, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
|
||||
|
||||
### Issues to work on
|
||||
|
||||
Any issue is up for the grabbing, but if you are starting out, you might be interested in the
|
||||
[E-Easy issues](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
||||
Those are issues that are considered more straightforward for beginners to Rust or the codebase itself.
|
||||
These issues can be a good launching pad for more involved issues. Easy tasks for a first time contribution
|
||||
include documentation improvements, new tests, examples, updating dependencies, etc.
|
||||
|
||||
If you come from a web development background, you might be interested in issues related to web technologies tagged
|
||||
[A-JavaScript](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||
[A-Style](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
|
||||
[A-HTML](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
||||
[A-Mobile](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
|
||||
|
||||
When you decide you want to work on a specific issue, ping us on that issue so that we can assign it to you.
|
||||
Again, do not hesitate to ask questions. We will gladly mentor anyone that want to tackle an issue.
|
||||
|
||||
Issues on the issue tracker are categorized with the following labels:
|
||||
|
||||
- **A**-prefixed labels state which area of the project an issue relates to.
|
||||
- **E**-prefixed labels show an estimate of the experience necessary to fix the issue.
|
||||
- **M**-prefixed labels are meta-issues used for questions, discussions, or tracking issues
|
||||
- **S**-prefixed labels show the status of the issue
|
||||
- **T**-prefixed labels show the type of issue
|
||||
|
||||
### Building mdBook
|
||||
|
||||
mdBook builds on stable Rust, if you want to build mdBook from source, here are the steps to follow:
|
||||
|
||||
1. Navigate to the directory of your choice
|
||||
0. Clone this repository with git.
|
||||
|
||||
```
|
||||
git clone https://github.com/rust-lang-nursery/mdBook.git
|
||||
```
|
||||
0. Navigate into the newly created `mdBook` directory
|
||||
0. Run `cargo build`
|
||||
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
|
||||
|
||||
### Making 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:
|
||||
1590
Cargo.lock
generated
Normal file
1590
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
70
Cargo.toml
70
Cargo.toml
@@ -1,44 +1,66 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.0.11"
|
||||
authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
|
||||
description = "create books from markdown files (like Gitbook)"
|
||||
documentation = "http://azerupi.github.io/mdBook/index.html"
|
||||
repository = "https://github.com/azerupi/mdBook"
|
||||
version = "0.2.2-alpha.0"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
"Matt Ickstadt <mattico8@gmail.com>"
|
||||
]
|
||||
description = "Create books from markdown files"
|
||||
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
|
||||
repository = "https://github.com/rust-lang-nursery/mdBook"
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
build = "build.rs"
|
||||
exclude = [
|
||||
"book-example/*",
|
||||
"src/theme/stylus",
|
||||
]
|
||||
exclude = ["book-example/*"]
|
||||
|
||||
[dependencies]
|
||||
clap = "1.5.3"
|
||||
handlebars = "0.12.0"
|
||||
rustc-serialize = "0.3.16"
|
||||
pulldown-cmark = "0.0.6"
|
||||
clap = "2.24"
|
||||
chrono = "0.4"
|
||||
handlebars = "1.0"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
error-chain = "0.12"
|
||||
serde_json = "1.0"
|
||||
pulldown-cmark = "0.1.2"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.5"
|
||||
toml = "0.4"
|
||||
memchr = "2.0"
|
||||
open = "1.1"
|
||||
regex = "1.0.0"
|
||||
tempfile = "3.0"
|
||||
itertools = "0.7"
|
||||
shlex = "0.1"
|
||||
toml-query = "0.7"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "2.5.4", optional = true }
|
||||
time = { version = "0.1.33", optional = true }
|
||||
crossbeam = { version = "0.2.8", optional = true }
|
||||
notify = { version = "4.0", optional = true }
|
||||
|
||||
# Serve feature
|
||||
iron = { version = "0.6", optional = true }
|
||||
staticfile = { version = "0.5", optional = true }
|
||||
ws = { version = "0.7", optional = true}
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
||||
ammonia = { version = "1.1", optional = true }
|
||||
|
||||
# Tests
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3.4"
|
||||
|
||||
select = "0.4"
|
||||
pretty_assertions = "0.5"
|
||||
walkdir = "2.0"
|
||||
pulldown-cmark-to-cmark = "1.1.0"
|
||||
|
||||
[features]
|
||||
default = ["output", "watch"]
|
||||
default = ["output", "watch", "serve", "search"]
|
||||
debug = []
|
||||
output = []
|
||||
regenerate-css = []
|
||||
watch = ["notify", "time", "crossbeam"]
|
||||
watch = ["notify"]
|
||||
serve = ["iron", "staticfile", "ws"]
|
||||
search = ["elasticlunr-rs", "ammonia"]
|
||||
|
||||
[[bin]]
|
||||
doc = false
|
||||
name = "mdbook"
|
||||
path = "src/bin/mdbook.rs"
|
||||
|
||||
159
README.md
159
README.md
@@ -1,78 +1,113 @@
|
||||
# mdBook
|
||||
# mdBook
|
||||
|
||||
<table>
|
||||
<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>
|
||||
<a href="https://travis-ci.org/rust-lang-nursery/mdBook"><img src="https://travis-ci.org/rust-lang-nursery/mdBook.svg?branch=master"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Windows</strong></td>
|
||||
<td>
|
||||
<a href="https://ci.appveyor.com/project/azerupi/mdbook/"><img src="https://ci.appveyor.com/api/projects/status/o38racsnbcospyc8/branch/master?svg=true"></a>
|
||||
<a href="https://ci.appveyor.com/project/rust-lang-libs/mdbook"><img src="https://ci.appveyor.com/api/projects/status/ysyke2rvo85sni55?svg=true"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a href="https://crates.io/crates/mdbook"><img src="https://img.shields.io/crates/v/mdbook.svg"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/crates/l/mdbook.svg"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/rust-lang-nursery/mdBook.svg"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
mdBook is a utility to create modern online books from markdown files.
|
||||
|
||||
**This project is still in its 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.
|
||||
|
||||
|
||||
## What does it look like?
|
||||
|
||||
The [**Documentation**](http://azerupi.github.io/mdBook/) for mdBook has been written in markdown and is using mdBook to generate the online book-like website you can read. The documentation uses the latest version on github and showcases the available features.
|
||||
The [User Guide] for mdBook has been written in Markdown and is using mdBook to
|
||||
generate the online book-like website you can read. The documentation uses the
|
||||
latest version on GitHub and showcases the available features.
|
||||
|
||||
## Installation
|
||||
|
||||
There are 2 ways to install mdBook but both require [Rust and Cargo](https://www.rust-lang.org/) to be installed.
|
||||
There are multiple ways to install mdBook.
|
||||
|
||||
##### Install from Crates.io
|
||||
1. **Binaries**
|
||||
|
||||
Once you have installed Rust, type the following in the terminal:
|
||||
```
|
||||
cargo install mdbook
|
||||
```
|
||||
Binaries are available for download [here][releases]. Make sure to put the
|
||||
path to the binary into your `PATH`.
|
||||
|
||||
This will download and compile mdBook for you, the only thing you that will be left to do is add the Cargo bin directory to your path.
|
||||
2. **From Crates.io**
|
||||
|
||||
##### Install from git
|
||||
This requires at least [Rust] 1.20 and Cargo to be installed. Once you have installed
|
||||
Rust, type the following in the terminal:
|
||||
|
||||
The version published to Crates.io will ever so slightly be behind the version hosted here on Github. If you need the latest version you can build the git version of mdBook yourself. Cargo makes this ***super easy***!
|
||||
```
|
||||
cargo install mdbook
|
||||
```
|
||||
|
||||
First, clone the repository on your computer:
|
||||
This will download and compile mdBook for you, the only thing left to do is
|
||||
to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
```
|
||||
git clone --depth 1 https://github.com/azerupi/mdBook.git
|
||||
```
|
||||
**Note for automatic deployment**
|
||||
|
||||
Then `cd` into the directory and run:
|
||||
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!
|
||||
|
||||
```
|
||||
cargo build --release
|
||||
```
|
||||
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:
|
||||
|
||||
The executable will be in `./target/release/mdbook`.
|
||||
```
|
||||
cargo install mdbook --vers "^0.1.0"
|
||||
```
|
||||
|
||||
3. **From Git**
|
||||
|
||||
The version published to crates.io will ever so slightly be behind the
|
||||
version hosted here on GitHub. If you need the latest version you can build
|
||||
the git version of mdBook yourself. Cargo makes this ***super easy***!
|
||||
|
||||
```
|
||||
cargo install --git https://github.com/rust-lang-nursery/mdBook.git mdbook
|
||||
```
|
||||
|
||||
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
4. **For Contributions**
|
||||
|
||||
If you want to contribute to mdBook you will have to clone the repository on
|
||||
your local machine:
|
||||
|
||||
```
|
||||
git clone https://github.com/rust-lang-nursery/mdBook.git
|
||||
```
|
||||
|
||||
`cd` into `mdBook/` and run
|
||||
|
||||
```
|
||||
cargo build
|
||||
```
|
||||
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name
|
||||
`mdBook` or `mdBook.exe`.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
mdBook will primaraly be used as a command line tool, even though it exposes all its functionality as a Rust crate for integration in other projects.
|
||||
mdBook will primarily be used as a command line tool, even though it exposes
|
||||
all its functionality as a Rust crate for integration in other projects.
|
||||
|
||||
Here are the main commands you will want to run, for a more exhaustive explanation, check out the [documentation](http://azerupi.github.io/mdBook/).
|
||||
Here are the main commands you will want to run. For a more exhaustive
|
||||
explanation, check out the [User Guide].
|
||||
|
||||
- `mdbook init`
|
||||
|
||||
The init command will create a directory with the minimal boilerplate to start with.
|
||||
The init command will create a directory with the minimal boilerplate to
|
||||
start with.
|
||||
|
||||
```
|
||||
book-test/
|
||||
@@ -82,33 +117,75 @@ Here are the main commands you will want to run, for a more exhaustive explanati
|
||||
└── 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.
|
||||
`book` and `src` are both directories. `src` contains the markdown files
|
||||
that will be used to render the output to the `book` directory.
|
||||
|
||||
Please, take a look at the [**Documentation**](http://azerupi.github.io/mdBook/cli/init.html) for more information and some neat tricks.
|
||||
Please, take a look at the [CLI docs] 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.
|
||||
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.
|
||||
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.
|
||||
|
||||
- `mdbook clean`
|
||||
|
||||
Delete directory in which generated book is located.
|
||||
|
||||
|
||||
### 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 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 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 [User Guide] and the [API docs] for more information.
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are highly appreciated and encouraged! Don't hesitate to participate to discussions in the issues, propose new features and ask for help.
|
||||
Contributions are highly appreciated and encouraged! Don't hesitate to
|
||||
participate to discussions in the issues, propose new features and ask for
|
||||
help.
|
||||
|
||||
People who are not familiar with the code can look at [issues that are tagged **easy**](https://github.com/azerupi/mdBook/labels/Easy). A lot of issues are also related to web development, so people that are not comfortable with Rust can also participate! :wink:
|
||||
If you are just starting out with Rust, there are a series of issus that are
|
||||
tagged [E-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.
|
||||
|
||||
You can pick any issue you want to work on. Usually it's a good idea to ask if someone is already working on it and if not to claim the issue.
|
||||
For more info about contributing, check out our [contribution guide] who helps
|
||||
you go through the build and contribution process!
|
||||
|
||||
There is also a [rendered version][master-docs] of the latest API docs
|
||||
available, for those hacking on `master`.
|
||||
|
||||
|
||||
## 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] file.
|
||||
|
||||
|
||||
[User Guide]: https://rust-lang-nursery.github.io/mdBook/
|
||||
[API docs]: https://docs.rs/mdbook/*/mdbook/
|
||||
[E-Easy]: https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy
|
||||
[contribution guide]: https://github.com/rust-lang-nursery/mdBook/blob/master/CONTRIBUTING.md
|
||||
[LICENSE]: https://github.com/rust-lang-nursery/mdBook/blob/master/LICENSE
|
||||
[releases]: https://github.com/rust-lang-nursery/mdBook/releases
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[CLI docs]: http://rust-lang-nursery.github.io/mdBook/cli/init.html
|
||||
[master-docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
|
||||
|
||||
24
appveyor.yml
24
appveyor.yml
@@ -20,25 +20,28 @@ environment:
|
||||
|
||||
# Install Rust and Cargo
|
||||
install:
|
||||
- ps: Start-FileDownload "https://static.rust-lang.org/dist/channel-rust-stable"
|
||||
- ps: $env:RUST_VERSION = Get-Content channel-rust-stable | select -first 1 | %{$_.split('-')[1]}
|
||||
- if NOT "%RUST_CHANNEL%" == "stable" set RUST_VERSION=%RUST_CHANNEL%
|
||||
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-${env:RUST_VERSION}-${env:TARGET}.exe"
|
||||
- rust-%RUST_VERSION%-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust"
|
||||
- SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin
|
||||
- rustc -V
|
||||
- ps: >-
|
||||
If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\msys64\mingw64\bin'
|
||||
} ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\msys64\mingw32\bin'
|
||||
}
|
||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
|
||||
- rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_CHANNEL%
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -Vv
|
||||
- cargo -V
|
||||
|
||||
build: false
|
||||
|
||||
# Equivalent to Travis' `script` phase
|
||||
test_script:
|
||||
- cargo build --verbose
|
||||
- cargo test --verbose
|
||||
- cargo test --all
|
||||
- cargo test --all --no-default-features
|
||||
|
||||
before_deploy:
|
||||
# Generate artifacts for release
|
||||
- cargo build --release
|
||||
- cargo rustc --bin mdbook --release -- -C lto
|
||||
- mkdir staging
|
||||
- copy target\release\mdbook.exe staging
|
||||
- cd staging
|
||||
@@ -58,3 +61,4 @@ deploy:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "mdBook Documentation",
|
||||
"description": "Create book from markdown files. Like Gitbook but implemented in Rust",
|
||||
"author": "Mathieu David"
|
||||
}
|
||||
19
book-example/book.toml
Normal file
19
book-example/book.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David", "Michael-F-Bryan"]
|
||||
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 20
|
||||
use-boolean-and = true
|
||||
boost-title = 2
|
||||
boost-hierarchy = 2
|
||||
boost-paragraph = 1
|
||||
expand = true
|
||||
heading-split-level = 2
|
||||
@@ -1,16 +1,25 @@
|
||||
# mdBook
|
||||
|
||||
**mdBook** is a command line tool and Rust crate to create books using Markdown files. It's very similar to Gitbook but written in [Rust](http://www.rust-lang.org).
|
||||
**mdBook** is a command line tool and Rust crate to create books using Markdown
|
||||
files. It's very similar to Gitbook but written in
|
||||
[Rust](http://www.rust-lang.org).
|
||||
|
||||
What you are reading serves as an example of the output of mdBook and at the same time as high-level docs.
|
||||
What you are reading serves as an example of the output of mdBook and at the
|
||||
same time as a high-level documentation.
|
||||
|
||||
mdBook is free and open source, you can find the source code on [Github](https://github.com/azerupi/mdBook). Issues and feature requests can be posted on the [Github Issue tracker](https://github.com/azerupi/mdBook/issues).
|
||||
mdBook is free and open source, you can find the source code on
|
||||
[GitHub](https://github.com/rust-lang-nursery/mdBook). Issues and feature
|
||||
requests can be posted on the [GitHub issue
|
||||
tracker](https://github.com/rust-lang-nursery/mdBook/issues).
|
||||
|
||||
## API docs
|
||||
|
||||
Alongside this book you can also read the [API docs](mdbook/index.html) generated by Rustdoc if you would like
|
||||
to use mdBook as a crate or write a new renderer and need a more low-level overview.
|
||||
Alongside this book you can also read the [API
|
||||
docs](https://docs.rs/mdbook/*/mdbook/) generated by Rustdoc if you would like
|
||||
to use mdBook as a crate or write a new renderer and need a more low-level
|
||||
overview.
|
||||
|
||||
## License
|
||||
|
||||
mdBook, all the source code, is released under the [Mozilla Public License v2.0](https://www.mozilla.org/MPL/2.0/)
|
||||
mdBook, all the source code, is released under the [Mozilla Public License
|
||||
v2.0](https://www.mozilla.org/MPL/2.0/).
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
# Summary
|
||||
|
||||
- [mdBook](README.md)
|
||||
- [Command Line Tool](cli/cli-tool.md)
|
||||
- [Command Line Tool](cli/README.md)
|
||||
- [init](cli/init.md)
|
||||
- [build](cli/build.md)
|
||||
- [watch](cli/watch.md)
|
||||
- [serve](cli/serve.md)
|
||||
- [test](cli/test.md)
|
||||
- [Format](format/format.md)
|
||||
- [clean](cli/clean.md)
|
||||
- [Format](format/README.md)
|
||||
- [SUMMARY.md](format/summary.md)
|
||||
- [Configuration](format/config.md)
|
||||
- [Theme](format/theme/theme.md)
|
||||
- [Theme](format/theme/README.md)
|
||||
- [index.hbs](format/theme/index-hbs.md)
|
||||
- [Syntax highlighting](format/theme/syntax-highlighting.md)
|
||||
- [Editor](format/theme/editor.md)
|
||||
- [MathJax Support](format/mathjax.md)
|
||||
- [Rust code specific features](format/rust.md)
|
||||
- [Rust Library](lib/lib.md)
|
||||
- [mdBook specific features](format/mdbook.md)
|
||||
- [Continuous Integration](continuous-integration.md)
|
||||
- [For Developers](for_developers/README.md)
|
||||
- [Preprocessors](for_developers/preprocessors.md)
|
||||
- [Alternate Backends](for_developers/backends.md)
|
||||
|
||||
-----------
|
||||
|
||||
[Contributors](misc/contributors.md)
|
||||
|
||||
55
book-example/src/cli/README.md
Normal file
55
book-example/src/cli/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Command Line Tool
|
||||
|
||||
mdBook can be used either as a command line tool or a [Rust
|
||||
crate](https://crates.io/crates/mdbook). Let's focus on the command line tool
|
||||
capabilities first.
|
||||
|
||||
## Install From Binaries
|
||||
|
||||
Precompiled binaries are provided for major platforms on a best-effort basis.
|
||||
Visit [the releases page](https://github.com/rust-lang-nursery/mdBook/releases)
|
||||
to download the appropriate version for your platform.
|
||||
|
||||
## Install From Source
|
||||
|
||||
mdBook can also be installed from source
|
||||
|
||||
### Pre-requisite
|
||||
|
||||
mdBook is written in **[Rust](https://www.rust-lang.org/)** and therefore needs
|
||||
to be compiled with **Cargo**. If you haven't already installed Rust, please go
|
||||
ahead and [install it](https://www.rust-lang.org/downloads.html) now.
|
||||
|
||||
### Install Crates.io version
|
||||
|
||||
Installing mdBook is relatively easy if you already have Rust and Cargo
|
||||
installed. You just have to type this snippet in your terminal:
|
||||
|
||||
```bash
|
||||
cargo install mdbook
|
||||
```
|
||||
|
||||
This will fetch the source code for the latest release from
|
||||
[Crates.io](https://crates.io/) and compile it. You will have to add Cargo's
|
||||
`bin` directory to your `PATH`.
|
||||
|
||||
Run `mdbook help` in your terminal to verify if it works. Congratulations, you
|
||||
have installed mdBook!
|
||||
|
||||
|
||||
### Install Git version
|
||||
|
||||
The **[git version](https://github.com/rust-lang-nursery/mdBook)** contains all
|
||||
the latest bug-fixes and features, that will be released in the next version on
|
||||
**Crates.io**, if you can't wait until the next release. You can build the git
|
||||
version yourself. Open your terminal and navigate to the directory of you
|
||||
choice. We need to clone the git repository and then build it with Cargo.
|
||||
|
||||
```bash
|
||||
git clone --depth=1 https://github.com/rust-lang-nursery/mdBook.git
|
||||
cd mdBook
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The executable `mdbook` will be in the `./target/release` folder, this should be
|
||||
added to the path.
|
||||
@@ -6,21 +6,33 @@ The build command is used to render your book:
|
||||
mdbook build
|
||||
```
|
||||
|
||||
It will try to parse your `SUMMARY.md` file to understand the structure of your book
|
||||
and fetch the corresponding files.
|
||||
It will try to parse your `SUMMARY.md` file to understand the structure of your
|
||||
book and fetch the corresponding files.
|
||||
|
||||
The rendered output will maintain the same directory structure as the source for
|
||||
convenience. Large books will therefore remain structured when rendered.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
Like `init`, the `build` command can take a directory as argument to use instead of the
|
||||
current working directory.
|
||||
The `build` command can take a directory as an argument to use as the book's
|
||||
root instead of the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook build path/to/book
|
||||
```
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the rendered book in
|
||||
your default web browser after building it.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
|
||||
-------------------
|
||||
|
||||
***note:*** *make sure to run the build command in the root directory and not in the source directory*
|
||||
***Note:*** *Make sure to run the build command in the root directory and not in
|
||||
the source directory*
|
||||
|
||||
30
book-example/src/cli/clean.md
Normal file
30
book-example/src/cli/clean.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# The clean command
|
||||
|
||||
The clean command is used to delete the generated book and any other build
|
||||
artifacts.
|
||||
|
||||
```bash
|
||||
mdbook clean
|
||||
```
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
The `clean` command can take a directory as an argument to use as the book's
|
||||
root instead of the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook clean path/to/book
|
||||
```
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to override the book's output
|
||||
directory, which will be deleted by this command. If not specified it will
|
||||
default to the value of the `build.build-dir` key in `book.toml`, or to `./book`
|
||||
relative to the book's root directory.
|
||||
|
||||
```bash
|
||||
mdbook clean --dest-dir=path/to/book
|
||||
```
|
||||
|
||||
`path/to/book` could be absolute or relative.
|
||||
@@ -1,35 +0,0 @@
|
||||
# Command Line Tool
|
||||
|
||||
mdBook can be used either as a command line tool or a [Rust crate](https://crates.io/crates/mdbook).
|
||||
Let's focus on the command line tool capabilities first.
|
||||
|
||||
## Install
|
||||
|
||||
### Pre-requisite
|
||||
|
||||
mdBook is written in **[Rust](https://www.rust-lang.org/)** and therefore needs to be compiled with **Cargo**, because we don't yet offer ready-to-go binaries. If you haven't already installed Rust, please go ahead and [install it](https://www.rust-lang.org/downloads.html) now.
|
||||
|
||||
### Install Crates.io version
|
||||
|
||||
Installing mdBook is relatively easy if you already have Rust and Cargo installed. You just have to type this snippet in your terminal:
|
||||
|
||||
```bash
|
||||
cargo install mdbook
|
||||
```
|
||||
|
||||
This will fetch the source code from [Crates.io](https://crates.io/) and compile it. You will have to add Cargo's `bin` directory to your `PATH`.
|
||||
|
||||
Run `mdbook help` in your terminal to verify if it works. Congratulations, you have installed mdBook!
|
||||
|
||||
|
||||
### Install Git version
|
||||
|
||||
The **[git version](https://github.com/azerupi/mdBook)** contains all the latest bug-fixes and features, that will be released in the next version on **Crates.io**, if you can't wait until the next release. You can build the git version yourself. Open your terminal and navigate to the directory of you choice. We need to clone the git repository and then build it with Cargo.
|
||||
|
||||
```bash
|
||||
git clone --depth=1 https://github.com/azerupi/mdBook.git
|
||||
cd mdBook
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The executable `mdbook` will be in the `./target/release` folder, this should be added to the path.
|
||||
@@ -1,5 +1,7 @@
|
||||
# The init command
|
||||
There is some minimal boilerplate that is the same for every new book. It's for this purpose that mdBook includes an `init` command.
|
||||
|
||||
There is some minimal boilerplate that is the same for every new book. It's for
|
||||
this purpose that mdBook includes an `init` command.
|
||||
|
||||
The `init` command is used like this:
|
||||
|
||||
@@ -7,7 +9,8 @@ The `init` command is used like this:
|
||||
mdbook init
|
||||
```
|
||||
|
||||
When using the `init` command for the first time, a couple of files will be set up for you:
|
||||
When using the `init` command for the first time, a couple of files will be set
|
||||
up for you:
|
||||
```bash
|
||||
book-test/
|
||||
├── book
|
||||
@@ -16,30 +19,36 @@ book-test/
|
||||
└── SUMMARY.md
|
||||
```
|
||||
|
||||
- The `src` directory is were you write your book in markdown. It contains all the source files,
|
||||
configuration files, etc.
|
||||
- The `src` directory is were you write your book in markdown. It contains all
|
||||
the source files, configuration files, etc.
|
||||
|
||||
- The `book` directory is where your book is rendered. All the output is ready to be uploaded
|
||||
to a server to be seen by your audience.
|
||||
- The `book` directory is where your book is rendered. All the output is ready
|
||||
to be uploaded to a server to be seen by your audience.
|
||||
|
||||
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is discussed in more detail in another [chapter](../format/summary.html).
|
||||
- The `SUMMARY.md` file is the most important file, it's the skeleton of your
|
||||
book and is discussed in more detail [in another
|
||||
chapter](../format/summary.md)
|
||||
|
||||
#### Tip & Trick: Hidden Feature
|
||||
When a `SUMMARY.md` file already exists, the `init` command will first parse it and generate the missing files according to the paths used in the `SUMMARY.md`. This allows you to think and create the whole structure of your book and then let mdBook generate it for you.
|
||||
#### Tip: Generate chapters from SUMMARY.md
|
||||
|
||||
When a `SUMMARY.md` file already exists, the `init` command will first parse it
|
||||
and generate the missing files according to the paths used in the `SUMMARY.md`.
|
||||
This allows you to think and create the whole structure of your book and then
|
||||
let mdBook generate it for you.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
When using the `init` command, you can also specify a directory, instead of using the current working directory,
|
||||
by appending a path to the command:
|
||||
The `init` command can take a directory as an argument to use as the book's root
|
||||
instead of the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook init path/to/book
|
||||
```
|
||||
|
||||
## --theme
|
||||
#### --theme
|
||||
|
||||
When you use the `--theme` argument, the default theme will be copied into a directory
|
||||
called `theme` in your source directory so that you can modify it.
|
||||
When you use the `--theme` flag, the default theme will be copied into a
|
||||
directory called `theme` in your source directory so that you can modify it.
|
||||
|
||||
The theme is selectively overwritten, this means that if you don't want to overwrite a
|
||||
specific file, just delete it and the default file will be used.
|
||||
The theme is selectively overwritten, this means that if you don't want to
|
||||
overwrite a specific file, just delete it and the default file will be used.
|
||||
|
||||
49
book-example/src/cli/serve.md
Normal file
49
book-example/src/cli/serve.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# The serve command
|
||||
|
||||
The serve command is used to preview a book by serving it over HTTP at
|
||||
`localhost:3000` by default. Additionally it watches the book's directory for
|
||||
changes, rebuilding the book and refreshing clients for each change. A websocket
|
||||
connection is used to trigger the client-side refresh.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
The `serve` command can take a directory as an argument to use as the book's
|
||||
root instead of the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook serve path/to/book
|
||||
```
|
||||
|
||||
#### Server options
|
||||
|
||||
`serve` has four options: the HTTP port, the WebSocket port, the HTTP hostname
|
||||
to listen on, and the hostname for the browser to connect to for WebSockets.
|
||||
|
||||
For example: suppose you have an nginx server for SSL termination which has a
|
||||
public address of 192.168.1.100 on port 80 and proxied that to 127.0.0.1 on port
|
||||
8000\. To run use the nginx proxy do:
|
||||
|
||||
```bash
|
||||
mdbook serve path/to/book -p 8000 -n 127.0.0.1 --websocket-hostname 192.168.1.100
|
||||
```
|
||||
|
||||
If you were to want live reloading for this you would need to proxy the
|
||||
websocket calls through nginx as well from `192.168.1.100:<WS_PORT>` to
|
||||
`127.0.0.1:<WS_PORT>`. The `-w` flag allows for the websocket port to be
|
||||
configured.
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the book in your your
|
||||
default web browser after starting the server.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
|
||||
-----
|
||||
|
||||
***Note:*** *The `serve` command is for testing, and is not intended to be a
|
||||
complete HTTP server for a website.*
|
||||
@@ -1,19 +1,52 @@
|
||||
# The test command
|
||||
|
||||
When writing a book, you sometimes need to automate some tests. For example, [The Rust Programming Book](https://doc.rust-lang.org/stable/book/) uses a lot of code examples that could get outdated.
|
||||
Therefore it is very important for them to be able to automatically test these code examples.
|
||||
When writing a book, you sometimes need to automate some tests. For example,
|
||||
[The Rust Programming Book](https://doc.rust-lang.org/stable/book/) uses a lot
|
||||
of code examples that could get outdated. Therefore it is very important for
|
||||
them to be able to automatically test these code examples.
|
||||
|
||||
mdBook supports a `test` command that will run all available tests in mdBook. At the moment, only one test is available:
|
||||
*"Test Rust code examples using Rustdoc"*, but I hope this will be expanded in the future to include more tests like:
|
||||
mdBook supports a `test` command that will run all available tests in a book. At
|
||||
the moment, only rustdoc tests are supported, but this may be expanded upon in
|
||||
the future.
|
||||
|
||||
- checking for broken links
|
||||
- checking for unused files
|
||||
- ...
|
||||
#### Disable tests on a code block
|
||||
|
||||
In the future I would like the user to be able to enable / disable test from the `book.json` configuration file and support custom tests.
|
||||
rustdoc doesn't test code blocks which contain the `ignore` attribute:
|
||||
|
||||
```rust,ignore
|
||||
fn main() {}
|
||||
```
|
||||
|
||||
rustdoc also doesn't test code blocks which specify a language other than Rust:
|
||||
|
||||
```markdown
|
||||
**Foo**: _bar_
|
||||
```
|
||||
|
||||
rustdoc *does* test code blocks which have no language specified:
|
||||
|
||||
```
|
||||
This is going to cause an error!
|
||||
```
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
The `test` command can take a directory as an argument to use as the book's root
|
||||
instead of the current working directory.
|
||||
|
||||
**How to use it:**
|
||||
```bash
|
||||
$ mdbook test
|
||||
[*]: Testing file: "/mdBook/book-example/src/README.md”
|
||||
mdbook test path/to/book
|
||||
```
|
||||
|
||||
#### --library-path
|
||||
|
||||
The `--library-path` (`-L`) option allows you to add directories to the library
|
||||
search path used by `rustdoc` when it builds and tests the examples. Multiple
|
||||
directories can be specified with multiple options (`-L foo -L bar`) or with a
|
||||
comma-delimited list (`-L foo,bar`).
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
# The watch command
|
||||
|
||||
The `watch` command is useful when you want your book to be rendered on every file change.
|
||||
You could repeatedly issue `mdbook build` every time a file is changed. But using `mdbook watch` once will watch your files and will trigger a build automatically whenever you modify a file.
|
||||
The `watch` command is useful when you want your book to be rendered on every
|
||||
file change. You could repeatedly issue `mdbook build` every time a file is
|
||||
changed. But using `mdbook watch` once will watch your files and will trigger a
|
||||
build automatically whenever you modify a file.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
Like `init` and `build`, `watch` can take a directory as argument to use instead of the
|
||||
current working directory.
|
||||
The `watch` command can take a directory as an argument to use as the book's
|
||||
root instead of the current working directory.
|
||||
|
||||
```bash
|
||||
mdbook watch path/to/book
|
||||
```
|
||||
|
||||
#### --open
|
||||
|
||||
-----
|
||||
When you use the `--open` (`-o`) option, mdbook will open the rendered book in
|
||||
your default web browser.
|
||||
|
||||
***note:*** *the `watch` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/azerupi/mdBook/issues)*
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
|
||||
56
book-example/src/continuous-integration.md
Normal file
56
book-example/src/continuous-integration.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Running `mdbook` in Continuous Integration
|
||||
|
||||
While the following examples use Travis CI, their principles should
|
||||
straightforwardly transfer to other continuous integration providers as well.
|
||||
|
||||
## Ensuring Your Book Builds and Tests Pass
|
||||
|
||||
Here is a sample Travis CI `.travis.yml` configuration that ensures `mdbook
|
||||
build` and `mdbook test` run successfully. The key to fast CI turnaround times
|
||||
is caching `mdbook` installs, so that you aren't compiling `mdbook` on every CI
|
||||
run.
|
||||
|
||||
```yaml
|
||||
language: rust
|
||||
sudo: false
|
||||
|
||||
cache:
|
||||
- cargo
|
||||
|
||||
rust:
|
||||
- stable
|
||||
|
||||
before_script:
|
||||
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
|
||||
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.1" mdbook)
|
||||
- cargo install-update -a
|
||||
|
||||
script:
|
||||
- cd path/to/mybook && mdbook build && mdbook test
|
||||
```
|
||||
|
||||
## Deploying Your Book to GitHub Pages
|
||||
|
||||
Following these instructions will result in your book being published to GitHub
|
||||
pages after a successful CI run on your repository's `master` branch.
|
||||
|
||||
First, create a new GitHub "Personal Access Token" with the "public_repo"
|
||||
permissions (or "repo" for private repositories). Go to your repository's Travis
|
||||
CI settings page and add an environment variable named `GITHUB_TOKEN` that is
|
||||
marked secure and *not* shown in the logs.
|
||||
|
||||
Then, append this snippet to your `.travis.yml` and update the path to the
|
||||
`book` directory:
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
provider: pages
|
||||
skip-cleanup: true
|
||||
github-token: $GITHUB_TOKEN
|
||||
local-dir: path/to/mybook/book
|
||||
keep-history: false
|
||||
on:
|
||||
branch: master
|
||||
```
|
||||
|
||||
That's it!
|
||||
46
book-example/src/for_developers/README.md
Normal file
46
book-example/src/for_developers/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# For Developers
|
||||
|
||||
While `mdbook` is mainly used as a command line tool, you can also import the
|
||||
underlying library directly and use that to manage a book. It also has a fairly
|
||||
flexible plugin mechanism, allowing you to create your own custom tooling and
|
||||
consumers (often referred to as *backends*) if you need to do some analysis of
|
||||
the book or render it in a different format.
|
||||
|
||||
The *For Developers* chapters are here to show you the more advanced usage of
|
||||
`mdbook`.
|
||||
|
||||
The two main ways a developer can hook into the book's build process is via,
|
||||
|
||||
- [Preprocessors](preprocessors.md)
|
||||
- [Alternate Backends](backends.md)
|
||||
|
||||
|
||||
## The Build Process
|
||||
|
||||
The process of rendering a book project goes through several steps.
|
||||
|
||||
1. Load the book
|
||||
- Parse the `book.toml`, falling back to the default `Config` if it doesn't
|
||||
exist
|
||||
- Load the book chapters into memory
|
||||
- Discover which preprocessors/backends should be used
|
||||
2. Run the preprocessors
|
||||
3. Call each backend in turn
|
||||
|
||||
|
||||
## Using `mdbook` as a Library
|
||||
|
||||
The `mdbook` binary is just a wrapper around the `mdbook` crate, exposing its
|
||||
functionality as a command-line program. As such it is quite easy to create your
|
||||
own programs which use `mdbook` internally, adding your own functionality (e.g.
|
||||
a custom preprocessor) or tweaking the build process.
|
||||
|
||||
The easiest way to find out how to use the `mdbook` crate is by looking at the
|
||||
[API Docs]. The top level documentation explains how one would use the
|
||||
[`MDBook`] type to load and build a book, while the [config] module gives a good
|
||||
explanation on the configuration system.
|
||||
|
||||
|
||||
[`MDBook`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.MDBook.html
|
||||
[API Docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
|
||||
[config]: file:///home/michael/Documents/forks/mdBook/target/doc/mdbook/config/index.html
|
||||
351
book-example/src/for_developers/backends.md
Normal file
351
book-example/src/for_developers/backends.md
Normal file
@@ -0,0 +1,351 @@
|
||||
# Alternate Backends
|
||||
|
||||
A "backend" is simply a program which `mdbook` will invoke during the book
|
||||
rendering process. This program is passed a JSON representation of the book and
|
||||
configuration information via `stdin`. Once the backend receives this
|
||||
information it is free to do whatever it wants.
|
||||
|
||||
There are already several alternate backends on GitHub which can be used as a
|
||||
rough example of how this is accomplished in practice.
|
||||
|
||||
- [mdbook-linkcheck] - a simple program for verifying the book doesn't contain
|
||||
any broken links
|
||||
- [mdbook-epub] - an EPUB renderer
|
||||
- [mdbook-test] - a program to run the book's contents through [rust-skeptic] to
|
||||
verify everything compiles and runs correctly (similar to `rustdoc --test`)
|
||||
|
||||
This page will step you through creating your own alternate backend in the form
|
||||
of a simple word counting program. Although it will be written in Rust, there's
|
||||
no reason why it couldn't be accomplished using something like Python or Ruby.
|
||||
|
||||
|
||||
## Setting Up
|
||||
|
||||
First you'll want to create a new binary program and add `mdbook` as a
|
||||
dependency.
|
||||
|
||||
```
|
||||
$ cargo new --bin mdbook-wordcount
|
||||
$ cd mdbook-wordcount
|
||||
$ cargo add mdbook
|
||||
```
|
||||
|
||||
When our `mdbook-wordcount` plugin is invoked, `mdbook` will send it a JSON
|
||||
version of [`RenderContext`] via our plugin's `stdin`. For convenience, there's
|
||||
a [`RenderContext::from_json()`] constructor which will load a `RenderContext`.
|
||||
|
||||
This is all the boilerplate necessary for our backend to load the book.
|
||||
|
||||
```rust
|
||||
// src/main.rs
|
||||
extern crate mdbook;
|
||||
|
||||
use std::io;
|
||||
use mdbook::renderer::RenderContext;
|
||||
|
||||
fn main() {
|
||||
let mut stdin = io::stdin();
|
||||
let ctx = RenderContext::from_json(&mut stdin).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** The `RenderContext` contains a `version` field. This lets backends
|
||||
figure out whether they are compatible with the version of `mdbook` it's being
|
||||
called by. This `version` comes directly from the corresponding field in
|
||||
`mdbook`'s `Cargo.toml`.
|
||||
|
||||
It is recommended that backends use the [`semver`] crate to inspect this field
|
||||
and emit a warning if there may be a compatibility issue.
|
||||
|
||||
|
||||
## Inspecting the Book
|
||||
|
||||
Now our backend has a copy of the book, lets count how many words are in each
|
||||
chapter!
|
||||
|
||||
Because the `RenderContext` contains a [`Book`] field (`book`), and a `Book` has
|
||||
the [`Book::iter()`] method for iterating over all items in a `Book`, this step
|
||||
turns out to be just as easy as the first.
|
||||
|
||||
```rust
|
||||
|
||||
fn main() {
|
||||
let mut stdin = io::stdin();
|
||||
let ctx = RenderContext::from_json(&mut stdin).unwrap();
|
||||
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn count_words(ch: &Chapter) -> usize {
|
||||
ch.content.split_whitespace().count()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Enabling the Backend
|
||||
|
||||
Now we've got the basics running, we want to actually use it. First, install the
|
||||
program.
|
||||
|
||||
```
|
||||
$ cargo install
|
||||
```
|
||||
|
||||
Then `cd` to the particular book you'd like to count the words of and update its
|
||||
`book.toml` file.
|
||||
|
||||
```diff
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David", "Michael-F-Bryan"]
|
||||
|
||||
+ [output.html]
|
||||
|
||||
+ [output.wordcount]
|
||||
```
|
||||
|
||||
When it loads a book into memory, `mdbook` will inspect your `book.toml` file to
|
||||
try and figure out which backends to use by looking for all `output.*` tables.
|
||||
If none are provided it'll fall back to using the default HTML renderer.
|
||||
|
||||
Notably, this means if you want to add your own custom backend you'll also need
|
||||
to make sure to add the HTML backend, even if its table just stays empty.
|
||||
|
||||
Now you just need to build your book like normal, and everything should *Just
|
||||
Work*.
|
||||
|
||||
```
|
||||
$ mdbook build
|
||||
...
|
||||
2018-01-16 07:31:15 [INFO] (mdbook::renderer): Invoking the "mdbook-wordcount" renderer
|
||||
mdBook: 126
|
||||
Command Line Tool: 224
|
||||
init: 283
|
||||
build: 145
|
||||
watch: 146
|
||||
serve: 292
|
||||
test: 139
|
||||
Format: 30
|
||||
SUMMARY.md: 259
|
||||
Configuration: 784
|
||||
Theme: 304
|
||||
index.hbs: 447
|
||||
Syntax highlighting: 314
|
||||
MathJax Support: 153
|
||||
Rust code specific features: 148
|
||||
For Developers: 788
|
||||
Alternate Backends: 710
|
||||
Contributors: 85
|
||||
```
|
||||
|
||||
The reason we didn't need to specify the full name/path of our `wordcount`
|
||||
backend is because `mdbook` will try to *infer* the program's name via
|
||||
convention. The executable for the `foo` backend is typically called
|
||||
`mdbook-foo`, with an associated `[output.foo]` entry in the `book.toml`. To
|
||||
explicitly tell `mdbook` what command to invoke (it may require command-line
|
||||
arguments or be an interpreted script), you can use the `command` field.
|
||||
|
||||
```diff
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David", "Michael-F-Bryan"]
|
||||
|
||||
[output.html]
|
||||
|
||||
[output.wordcount]
|
||||
+ command = "python /path/to/wordcount.py"
|
||||
```
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Now imagine you don't want to count the number of words on a particular chapter
|
||||
(it might be generated text/code, etc). The canonical way to do this is via the
|
||||
usual `book.toml` configuration file by adding items to your `[output.foo]`
|
||||
table.
|
||||
|
||||
The `Config` can be treated roughly as a nested hashmap which lets you call
|
||||
methods like `get()` to access the config's contents, with a
|
||||
`get_deserialized()` convenience method for retrieving a value and automatically
|
||||
deserializing to some arbitrary type `T`.
|
||||
|
||||
To implement this, we'll create our own serializable `WordcountConfig` struct
|
||||
which will encapsulate all configuration for this backend.
|
||||
|
||||
First add `serde` and `serde_derive` to your `Cargo.toml`,
|
||||
|
||||
```
|
||||
$ cargo add serde serde_derive
|
||||
```
|
||||
|
||||
And then you can create the config struct,
|
||||
|
||||
```rust
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
...
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct WordcountConfig {
|
||||
pub ignores: Vec<String>,
|
||||
}
|
||||
```
|
||||
|
||||
Now we just need to deserialize the `WordcountConfig` from our `RenderContext`
|
||||
and then add a check to make sure we skip ignored chapters.
|
||||
|
||||
```diff
|
||||
fn main() {
|
||||
let mut stdin = io::stdin();
|
||||
let ctx = RenderContext::from_json(&mut stdin).unwrap();
|
||||
+ let cfg: WordcountConfig = ctx.config
|
||||
+ .get_deserialized("output.wordcount")
|
||||
+ .unwrap_or_default();
|
||||
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
+ if cfg.ignores.contains(&ch.name) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Output and Signalling Failure
|
||||
|
||||
While it's nice to print word counts to the terminal when a book is built, it
|
||||
might also be a good idea to output them to a file somewhere. `mdbook` tells a
|
||||
backend where it should place any generated output via the `destination` field
|
||||
in [`RenderContext`].
|
||||
|
||||
```diff
|
||||
+ use std::fs::{self, File};
|
||||
+ use std::io::{self, Write};
|
||||
- use std::io;
|
||||
use mdbook::renderer::RenderContext;
|
||||
use mdbook::book::{BookItem, Chapter};
|
||||
|
||||
fn main() {
|
||||
...
|
||||
|
||||
+ let _ = fs::create_dir_all(&ctx.destination);
|
||||
+ let mut f = File::create(ctx.destination.join("wordcounts.txt")).unwrap();
|
||||
+
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
...
|
||||
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
+ writeln!(f, "{}: {}", ch.name, num_words).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** There is no guarantee that the destination directory exists or is
|
||||
> empty (`mdbook` may leave the previous contents to let backends do caching),
|
||||
> so it's always a good idea to create it with `fs::create_dir_all()`.
|
||||
|
||||
There's always the possibility that an error will occur while processing a book
|
||||
(just look at all the `unwrap()`'s we've written already), so `mdbook` will
|
||||
interpret a non-zero exit code as a rendering failure.
|
||||
|
||||
For example, if we wanted to make sure all chapters have an *even* number of
|
||||
words, erroring out if an odd number is encountered, then you may do something
|
||||
like this:
|
||||
|
||||
```diff
|
||||
+ use std::process;
|
||||
...
|
||||
|
||||
fn main() {
|
||||
...
|
||||
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
...
|
||||
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
writeln!(f, "{}: {}", ch.name, num_words).unwrap();
|
||||
|
||||
+ if cfg.deny_odds && num_words % 2 == 1 {
|
||||
+ eprintln!("{} has an odd number of words!", ch.name);
|
||||
+ process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct WordcountConfig {
|
||||
pub ignores: Vec<String>,
|
||||
+ pub deny_odds: bool,
|
||||
}
|
||||
```
|
||||
|
||||
Now, if we reinstall the backend and build a book,
|
||||
|
||||
```
|
||||
$ cargo install --force
|
||||
$ mdbook build /path/to/book
|
||||
...
|
||||
2018-01-16 21:21:39 [INFO] (mdbook::renderer): Invoking the "wordcount" renderer
|
||||
mdBook: 126
|
||||
Command Line Tool: 224
|
||||
init: 283
|
||||
init has an odd number of words!
|
||||
2018-01-16 21:21:39 [ERROR] (mdbook::renderer): Renderer exited with non-zero return code.
|
||||
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Error: Rendering failed
|
||||
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Caused By: The "mdbook-wordcount" renderer failed
|
||||
```
|
||||
|
||||
As you've probably already noticed, output from the plugin's subprocess is
|
||||
immediately passed through to the user. It is encouraged for plugins to follow
|
||||
the "rule of silence" and only generate output when necessary (e.g. an error in
|
||||
generation or a warning).
|
||||
|
||||
All environment variables are passed through to the backend, allowing you to use
|
||||
the usual `RUST_LOG` to control logging verbosity.
|
||||
|
||||
|
||||
## Wrapping Up
|
||||
|
||||
Although contrived, hopefully this example was enough to show how you'd create
|
||||
an alternate backend for `mdbook`. If you feel it's missing something, don't
|
||||
hesitate to create an issue in the [issue tracker] so we can improve the user
|
||||
guide.
|
||||
|
||||
The existing backends mentioned towards the start of this chapter should serve
|
||||
as a good example of how it's done in real life, so feel free to skim through
|
||||
the source code or ask questions.
|
||||
|
||||
|
||||
[mdbook-linkcheck]: https://github.com/Michael-F-Bryan/mdbook-linkcheck
|
||||
[mdbook-epub]: https://github.com/Michael-F-Bryan/mdbook-epub
|
||||
[mdbook-test]: https://github.com/Michael-F-Bryan/mdbook-test
|
||||
[rust-skeptic]: https://github.com/budziq/rust-skeptic
|
||||
[`RenderContext`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html
|
||||
[`RenderContext::from_json()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html#method.from_json
|
||||
[`semver`]: https://crates.io/crates/semver
|
||||
[`Book`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html
|
||||
[`Book::iter()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html#method.iter
|
||||
[`Config`]: http://rust-lang-nursery.github.io/mdBook/mdbook/config/struct.Config.html
|
||||
[issue tracker]: https://github.com/rust-lang-nursery/mdBook/issues
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "mdbook-wordcount"
|
||||
version = "0.1.0"
|
||||
authors = ["Michael Bryan <michaelfbryan@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
mdbook = { path = "../../../..", version = "*" }
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
49
book-example/src/for_developers/mdbook-wordcount/src/main.rs
Normal file
49
book-example/src/for_developers/mdbook-wordcount/src/main.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
extern crate mdbook;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
use std::process;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Write};
|
||||
use mdbook::renderer::RenderContext;
|
||||
use mdbook::book::{BookItem, Chapter};
|
||||
|
||||
fn main() {
|
||||
let mut stdin = io::stdin();
|
||||
let ctx = RenderContext::from_json(&mut stdin).unwrap();
|
||||
let cfg: WordcountConfig = ctx.config
|
||||
.get_deserialized("output.wordcount")
|
||||
.unwrap_or_default();
|
||||
|
||||
let _ = fs::create_dir_all(&ctx.destination);
|
||||
let mut f = File::create(ctx.destination.join("wordcounts.txt")).unwrap();
|
||||
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
if cfg.ignores.contains(&ch.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
writeln!(f, "{}: {}", ch.name, num_words).unwrap();
|
||||
|
||||
if cfg.deny_odds && num_words % 2 == 1 {
|
||||
eprintln!("{} has an odd number of words!", ch.name);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn count_words(ch: &Chapter) -> usize {
|
||||
ch.content.split_whitespace().count()
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct WordcountConfig {
|
||||
pub ignores: Vec<String>,
|
||||
pub deny_odds: bool,
|
||||
}
|
||||
109
book-example/src/for_developers/preprocessors.md
Normal file
109
book-example/src/for_developers/preprocessors.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Preprocessors
|
||||
|
||||
A *preprocessor* is simply a bit of code which gets run immediately after the
|
||||
book is loaded and before it gets rendered, allowing you to update and mutate
|
||||
the book. Possible use cases are:
|
||||
|
||||
- Creating custom helpers like `\{{#include /path/to/file.md}}`
|
||||
- Updating links so `[some chapter](some_chapter.md)` is automatically changed
|
||||
to `[some chapter](some_chapter.html)` for the HTML renderer
|
||||
- Substituting in latex-style expressions (`$$ \frac{1}{3} $$`) with their
|
||||
mathjax equivalents
|
||||
|
||||
|
||||
## Implementing a Preprocessor
|
||||
|
||||
A preprocessor is represented by the `Preprocessor` trait.
|
||||
|
||||
```rust
|
||||
pub trait Preprocessor {
|
||||
fn name(&self) -> &str;
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>;
|
||||
fn supports_renderer(&self, _renderer: &str) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Where the `PreprocessorContext` is defined as
|
||||
|
||||
```rust
|
||||
pub struct PreprocessorContext {
|
||||
pub root: PathBuf,
|
||||
pub config: Config,
|
||||
/// The `Renderer` this preprocessor is being used with.
|
||||
pub renderer: String,
|
||||
}
|
||||
```
|
||||
|
||||
The `renderer` value allows you react accordingly, for example, PDF or HTML.
|
||||
|
||||
## A complete Example
|
||||
|
||||
The magic happens within the `run(...)` method of the
|
||||
[`Preprocessor`][preprocessor-docs] trait implementation.
|
||||
|
||||
As direct access to the chapters is not possible, you will probably end up
|
||||
iterating them using `for_each_mut(...)`:
|
||||
|
||||
```rust
|
||||
book.for_each_mut(|item: &mut BookItem| {
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
|
||||
res = Some(
|
||||
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
|
||||
Ok(md) => {
|
||||
chapter.content = md;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The `chapter.content` is just a markdown formatted string, and you will have to
|
||||
process it in some way. Even though it's entirely possible to implement some
|
||||
sort of manual find & replace operation, if that feels too unsafe you can use
|
||||
[`pulldown-cmark`][pc] to parse the string into events and work on them instead.
|
||||
|
||||
Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events
|
||||
back to a string.
|
||||
|
||||
The following code block shows how to remove all emphasis from markdown, and do
|
||||
so safely.
|
||||
|
||||
```rust
|
||||
fn remove_emphasis(
|
||||
num_removed_items: &mut usize,
|
||||
chapter: &mut Chapter,
|
||||
) -> Result<String> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
|
||||
let events = Parser::new(&chapter.content).filter(|e| {
|
||||
let should_keep = match *e {
|
||||
Event::Start(Tag::Emphasis)
|
||||
| Event::Start(Tag::Strong)
|
||||
| Event::End(Tag::Emphasis)
|
||||
| Event::End(Tag::Strong) => false,
|
||||
_ => true,
|
||||
};
|
||||
if !should_keep {
|
||||
*num_removed_items += 1;
|
||||
}
|
||||
should_keep
|
||||
});
|
||||
|
||||
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
|
||||
Error::from(format!("Markdown serialization failed: {}", err))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
For everything else, have a look [at the complete example][example].
|
||||
|
||||
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
|
||||
[pc]: https://crates.io/crates/pulldown-cmark
|
||||
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
|
||||
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs
|
||||
@@ -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,23 +1,259 @@
|
||||
# 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:
|
||||
Here is an example of what a ***book.toml*** file might look like:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Example book",
|
||||
"author": "Name",
|
||||
"description": "The example book covers examples.",
|
||||
"dest": "output/my-book"
|
||||
}
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
author = "John Doe"
|
||||
description = "The example book covers examples."
|
||||
|
||||
[build]
|
||||
build-dir = "my-example-book"
|
||||
create-missing = false
|
||||
|
||||
[preprocess.index]
|
||||
|
||||
[preprocess.links]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["custom.css"]
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 15
|
||||
```
|
||||
|
||||
#### Supported variables
|
||||
## Supported configuration options
|
||||
|
||||
- **title:** title of the book
|
||||
- **author:** author of the book
|
||||
- **description:** description, which is added as meta in the html head of each page.
|
||||
- **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.
|
||||
|
||||
***note:*** *the supported configurable parameters are scarce at the moment, but more will be added in the future*
|
||||
### General metadata
|
||||
|
||||
This is general information about your book.
|
||||
|
||||
- **title:** The title of the book
|
||||
- **authors:** The author(s) of the book
|
||||
- **description:** A description for the book, which is added as meta
|
||||
information in the html `<head>` of each page
|
||||
- **src:** By default, the source directory is found in the directory named
|
||||
`src` directly under the root folder. But this is configurable with the `src`
|
||||
key in the configuration file.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
|
||||
```
|
||||
|
||||
### Build options
|
||||
|
||||
This controls the build process of your book.
|
||||
|
||||
- **build-dir:** The directory to put the rendered book in. By default this is
|
||||
`book/` in the book's root directory.
|
||||
- **create-missing:** By default, any missing files specified in `SUMMARY.md`
|
||||
will be created when the book is built (i.e. `create-missing = true`). If this
|
||||
is `false` then the build process will instead exit with an error if any files
|
||||
do not exist.
|
||||
- **use-default-preprocessors:** Disable the default preprocessors of (`links` &
|
||||
`index`) by setting this option to `false`.
|
||||
|
||||
If you have the same, and/or other preprocessors declared via their table
|
||||
of configuration, they will run instead.
|
||||
|
||||
- For clarity, with no preprocessor configuration, the default `links` and
|
||||
`index` will run.
|
||||
- Setting `use-default-preprocessors = false` will disable these
|
||||
default preprocessors from running.
|
||||
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
|
||||
`use-default-preprocessors` that `links` it will run.
|
||||
|
||||
## Configuring Preprocessors
|
||||
|
||||
The following preprocessors are available and included by default:
|
||||
|
||||
- `links`: Expand the `{{ #playpen }}` and `{{ #include }}` handlebars helpers in
|
||||
a chapter to include the contents of a file.
|
||||
- `index`: Convert all chapter files named `README.md` into `index.md`. That is
|
||||
to say, all `README.md` would be rendered to an index file `index.html` in the
|
||||
rendered book.
|
||||
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
[build]
|
||||
build-dir = "build"
|
||||
create-missing = false
|
||||
|
||||
[preprocess.links]
|
||||
|
||||
[preprocess.index]
|
||||
```
|
||||
|
||||
### Custom Preprocessor Configuration
|
||||
|
||||
Like renderers, preprocessor will need to be given its own table (e.g. `[preprocessor.mathjax]`).
|
||||
In the section, you may then pass extra configuration to the preprocessor by adding key-value pairs to the table.
|
||||
|
||||
For example
|
||||
|
||||
```
|
||||
[preprocess.links]
|
||||
# set the renderers this preprocessor will run for
|
||||
renderers = ["html"]
|
||||
some_extra_feature = true
|
||||
```
|
||||
|
||||
#### Locking a Preprocessor dependency to a renderer
|
||||
|
||||
You can explicitly specify that a preprocessor should run for a renderer by binding the two together.
|
||||
|
||||
```
|
||||
[preprocessor.mathjax]
|
||||
renderers = ["html"] # mathjax only makes sense with the HTML renderer
|
||||
```
|
||||
|
||||
## Configuring Renderers
|
||||
|
||||
### HTML renderer options
|
||||
|
||||
The HTML renderer has a couple of options as well. All the options for the
|
||||
renderer need to be specified under the TOML table `[output.html]`.
|
||||
|
||||
The following configuration options are available:
|
||||
|
||||
- **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.
|
||||
- **no-section-label:** mdBook by defaults adds section label in table of
|
||||
contents column. For example, "1.", "2.1". Set this option to true to disable
|
||||
those labels. Defaults to `false`.
|
||||
- **playpen:** A subtable for configuring various playpen settings.
|
||||
- **search:** A subtable for configuring the in-browser search functionality.
|
||||
mdBook must be compiled with the `search` feature enabled (on by default).
|
||||
|
||||
Available configuration options for the `[output.html.playpen]` table:
|
||||
|
||||
- **editable:** Allow editing the source code. Defaults to `false`.
|
||||
- **copy-js:** Copy JavaScript files for the editor to the output directory.
|
||||
Defaults to `true`.
|
||||
|
||||
[Ace]: https://ace.c9.io/
|
||||
|
||||
Available configuration options for the `[output.html.search]` table:
|
||||
|
||||
- **enable:** Enables the search feature. Defaults to `true`.
|
||||
- **limit-results:** The maximum number of search results. Defaults to `30`.
|
||||
- **teaser-word-count:** The number of words used for a search result teaser.
|
||||
Defaults to `30`.
|
||||
- **use-boolean-and:** Define the logical link between multiple search words. If
|
||||
true, all search words must appear in each result. Defaults to `true`.
|
||||
- **boost-title:** Boost factor for the search result score if a search word
|
||||
appears in the header. Defaults to `2`.
|
||||
- **boost-hierarchy:** Boost factor for the search result score if a search word
|
||||
appears in the hierarchy. The hierarchy contains all titles of the parent
|
||||
documents and all parent headings. Defaults to `1`.
|
||||
- **boost-paragraph:** Boost factor for the search result score if a search word
|
||||
appears in the text. Defaults to `1`.
|
||||
- **expand:** True if search should match longer results e.g. search `micro`
|
||||
should match `microwave`. Defaults to `true`.
|
||||
- **heading-split-level:** Search results will link to a section of the document
|
||||
which contains the result. Documents are split into sections by headings this
|
||||
level or less. Defaults to `3`. (`### This is a level 3 heading`)
|
||||
- **copy-js:** Copy JavaScript files for the search implementation to the output
|
||||
directory. Defaults to `true`.
|
||||
|
||||
This shows all available options in the **book.toml**:
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
[build]
|
||||
build-dir = "book"
|
||||
create-missing = true
|
||||
preprocess = ["links", "index"]
|
||||
|
||||
[output.html]
|
||||
theme = "my-theme"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
|
||||
[output.html.playpen]
|
||||
editor = "./path/to/editor"
|
||||
editable = false
|
||||
|
||||
[output.html.search]
|
||||
enable = true
|
||||
searcher = "./path/to/searcher"
|
||||
limit-results = 30
|
||||
teaser-word-count = 30
|
||||
use-boolean-and = true
|
||||
boost-title = 2
|
||||
boost-hierarchy = 1
|
||||
boost-paragraph = 1
|
||||
expand = true
|
||||
heading-split-level = 3
|
||||
copy-js = true
|
||||
```
|
||||
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All configuration values can be overridden from the command line by setting the
|
||||
corresponding environment variable. Because many operating systems restrict
|
||||
environment variables to be alphanumeric characters or `_`, the configuration
|
||||
key needs to be formatted slightly differently to the normal `foo.bar.baz` form.
|
||||
|
||||
Variables starting with `MDBOOK_` are used for configuration. The key is created
|
||||
by removing the `MDBOOK_` prefix and turning the resulting string into
|
||||
`kebab-case`. Double underscores (`__`) separate nested keys, while a single
|
||||
underscore (`_`) is replaced with a dash (`-`).
|
||||
|
||||
For example:
|
||||
|
||||
- `MDBOOK_foo` -> `foo`
|
||||
- `MDBOOK_FOO` -> `foo`
|
||||
- `MDBOOK_FOO__BAR` -> `foo.bar`
|
||||
- `MDBOOK_FOO_BAR` -> `foo-bar`
|
||||
- `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
|
||||
|
||||
So by setting the `MDBOOK_BOOK__TITLE` environment variable you can override the
|
||||
book's title without needing to touch your `book.toml`.
|
||||
|
||||
> **Note:** To facilitate setting more complex config items, the value of an
|
||||
> environment variable is first parsed as JSON, falling back to a string if the
|
||||
> parse fails.
|
||||
>
|
||||
> This means, if you so desired, you could override all book metadata when
|
||||
> building the book with something like
|
||||
>
|
||||
> ```text
|
||||
> $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
|
||||
> $ mdbook build
|
||||
> ```
|
||||
|
||||
The latter case may be useful in situations where `mdbook` is invoked from a
|
||||
script or CI, where it sometimes isn't possible to update the `book.toml` before
|
||||
building.
|
||||
@@ -1,21 +1,43 @@
|
||||
# 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.
|
||||
|
||||
>**Note:** When you use double backslashes in MathJax blocks (for example in
|
||||
> commands such as `\begin{cases} \frac 1 2 \\ \frac 3 4 \end{cases}`) you need
|
||||
> to add _two extra_ backslashes (e.g., `\begin{cases} \frac 1 2 \\\\ \frac 3 4
|
||||
> \end{cases}`).
|
||||
|
||||
|
||||
### Inline equations
|
||||
Inline equations are delimited by `\\(` and `\\)`. So for example, to render the
|
||||
following inline equation \\( \int x dx = \frac{x^2}{2} + C \\) you would write
|
||||
the following:
|
||||
```
|
||||
\\( \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 \\]
|
||||
```
|
||||
|
||||
74
book-example/src/format/mdbook.md
Normal file
74
book-example/src/format/mdbook.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# mdBook-specific markdown
|
||||
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that lets 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);
|
||||
# }
|
||||
```
|
||||
|
||||
## Including files
|
||||
|
||||
With the following syntax, you can include files into your book:
|
||||
|
||||
```hbs
|
||||
\{{#include file.rs}}
|
||||
```
|
||||
|
||||
The path to the file has to be relative from the current source file.
|
||||
|
||||
Usually, this command is used for including code snippets and examples. In this
|
||||
case, oftens one would include a specific part of the file e.g. which only
|
||||
contains the relevant lines for the example. We support four different modes of
|
||||
partial includes:
|
||||
|
||||
```hbs
|
||||
\{{#include file.rs:2}}
|
||||
\{{#include file.rs::10}}
|
||||
\{{#include file.rs:2:}}
|
||||
\{{#include file.rs:2:10}}
|
||||
```
|
||||
|
||||
The first command only includes the second line from file `file.rs`. The second
|
||||
command includes all lines up to line 10, i.e. the lines from 11 till the end of
|
||||
the file are omitted. The third command includes all lines from line 2, i.e. the
|
||||
first line is omitted. The last command includes the excerpt of `file.rs`
|
||||
consisting of lines 2 to 10.
|
||||
|
||||
## 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 sent to the [Rust Playpen] to be
|
||||
compiled and run. The result is sent back and displayed directly underneath the
|
||||
code.
|
||||
|
||||
Here is what a rendered code snippet looks like:
|
||||
|
||||
{{#playpen example.rs}}
|
||||
|
||||
[Rust Playpen]: https://play.rust-lang.org/
|
||||
@@ -1,42 +0,0 @@
|
||||
# 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}}
|
||||
@@ -1,32 +1,38 @@
|
||||
# SUMMARY.md
|
||||
|
||||
The summary file is used by mdBook to know what chapters to include,
|
||||
in what order they should appear, what their hierarchy is and where the source files are.
|
||||
Without this file, there is no book.
|
||||
The summary file is used by mdBook to know what chapters to include, in what
|
||||
order they should appear, what their hierarchy is and where the source files
|
||||
are. Without this file, there is no book.
|
||||
|
||||
Even though `SUMMARY.md` is a markdown file, the formatting is very strict to
|
||||
allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
|
||||
|
||||
#### Allowed elements
|
||||
|
||||
1. ***Title*** It's common practice to begin with a title, generally
|
||||
<code class="language-markdown"># Summary</code>.
|
||||
But it is not mandatory, the parser just ignores it. So you can too
|
||||
if you feel like it.
|
||||
1. ***Title*** It's common practice to begin with a title, generally <code
|
||||
class="language-markdown"># Summary</code>. But it is not mandatory, the
|
||||
parser just ignores it. So you can too if you feel like it.
|
||||
|
||||
2. ***Prefix Chapter*** Before the main numbered chapters you can add a couple of elements that will not be numbered. This is useful for
|
||||
forewords, introductions, etc. There are however some constraints. You can not nest prefix chapters, they should all be on the root level. And you can not add prefix chapters once you have added numbered chapters.
|
||||
2. ***Prefix Chapter*** Before the main numbered chapters you can add a couple
|
||||
of elements that will not be numbered. This is useful for forewords,
|
||||
introductions, etc. There are however some constraints. You can not nest
|
||||
prefix chapters, they should all be on the root level. And you can not add
|
||||
prefix chapters once you have added numbered chapters.
|
||||
```markdown
|
||||
[Title of prefix element](relative/path/to/markdown.md)
|
||||
```
|
||||
|
||||
3. ***Numbered Chapter*** Numbered chapters are the main content of the book, they will be numbered and can be nested,
|
||||
resulting in a nice hierarchy (chapters, sub-chapters, etc.)
|
||||
3. ***Numbered Chapter*** Numbered chapters are the main content of the book,
|
||||
they will be numbered and can be nested, resulting in a nice hierarchy
|
||||
(chapters, sub-chapters, etc.)
|
||||
```markdown
|
||||
- [Title of the Chapter](relative/path/to/markdown.md)
|
||||
```
|
||||
You can either use `-` or `*` to indicate a numbered chapter.
|
||||
|
||||
4. ***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.
|
||||
All other elements are unsupported and will be ignored at best or result in an
|
||||
error.
|
||||
|
||||
34
book-example/src/format/theme/README.md
Normal file
34
book-example/src/format/theme/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Theme
|
||||
|
||||
The default renderer uses a [handlebars](http://handlebarsjs.com/) template to
|
||||
render your markdown files and comes with a default theme included in the mdBook
|
||||
binary.
|
||||
|
||||
The theme is totally customizable, you can selectively replace every file from
|
||||
the theme by your own by adding a `theme` directory next to `src` folder in your
|
||||
project root. Create a new file with the name of the file you want to override
|
||||
and now that file will be used instead of the default file.
|
||||
|
||||
Here are the files you can override:
|
||||
|
||||
- ***index.hbs*** is the handlebars template.
|
||||
- ***book.css*** is the style used in the output. If you want to change the
|
||||
design of your book, this is probably the file you want to modify. Sometimes
|
||||
in conjunction with `index.hbs` when you want to radically change the layout.
|
||||
- ***book.js*** is mostly used to add client side functionality, like hiding /
|
||||
un-hiding the sidebar, changing the theme, ...
|
||||
- ***highlight.js*** is the JavaScript that is used to highlight code snippets,
|
||||
you should not need to modify this.
|
||||
- ***highlight.css*** is the theme used for the code highlighting
|
||||
- ***favicon.png*** the favicon that will be used
|
||||
|
||||
Generally, when you want to tweak the theme, you don't need to override all the
|
||||
files. If you only need changes in the stylesheet, there is no point in
|
||||
overriding all the other files. Because custom files take precedence over
|
||||
built-in ones, they will not get updated with new fixes / features.
|
||||
|
||||
**Note:** When you override a file, it is possible that you break some
|
||||
functionality. Therefore I recommend to use the file from the default theme as
|
||||
template and only add / modify what you need. You can copy the default theme
|
||||
into your source directory automatically by using `mdbook init --theme` just
|
||||
remove the files you don't want to override.
|
||||
46
book-example/src/format/theme/editor.md
Normal file
46
book-example/src/format/theme/editor.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Editor
|
||||
|
||||
In addition to providing runnable code playpens, mdBook optionally allows them
|
||||
to be editable. In order to enable editable code blocks, the following needs to
|
||||
be added to the ***book.toml***:
|
||||
|
||||
```toml
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
```
|
||||
|
||||
To make a specific block available for editing, the attribute `editable` needs
|
||||
to be added to it:
|
||||
|
||||
<pre><code class="language-markdown">```rust,editable
|
||||
fn main() {
|
||||
let number = 5;
|
||||
print!("{}", number);
|
||||
}
|
||||
```</code></pre>
|
||||
|
||||
The above will result in this editable playpen:
|
||||
|
||||
```rust,editable
|
||||
fn main() {
|
||||
let number = 5;
|
||||
print!("{}", number);
|
||||
}
|
||||
```
|
||||
|
||||
Note the new `Undo Changes` button in the editable playpens.
|
||||
|
||||
## Customizing the Editor
|
||||
|
||||
By default, the editor is the [Ace](https://ace.c9.io/) editor, but, if desired,
|
||||
the functionality may be overriden by providing a different folder:
|
||||
|
||||
```toml
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
editor = "/path/to/editor"
|
||||
```
|
||||
|
||||
Note that for the editor changes to function correctly, the `book.js` inside of
|
||||
the `theme` folder will need to be overriden as it has some couplings with the
|
||||
default Ace editor.
|
||||
@@ -1,15 +1,15 @@
|
||||
# index.hbs
|
||||
|
||||
`index.hbs` is the handlebars template that is used to render the book.
|
||||
The markdown files are processed to html and then injected in that template.
|
||||
`index.hbs` is the handlebars template that is used to render the book. The
|
||||
markdown files are processed to html and then injected in that template.
|
||||
|
||||
If you want to change the layout or style of your book, chances are that you will
|
||||
have to modify this template a little bit. Here is what you need to know.
|
||||
If you want to change the layout or style of your book, chances are that you
|
||||
will have to modify this template a little bit. Here is what you need to know.
|
||||
|
||||
## Data
|
||||
|
||||
A lot of data is exposed to the handlebars template with the "context".
|
||||
In the handlebars template you can access this information by using
|
||||
A lot of data is exposed to the handlebars template with the "context". In the
|
||||
handlebars template you can access this information by using
|
||||
|
||||
```handlebars
|
||||
{{name_of_property}}
|
||||
@@ -17,26 +17,33 @@ In the handlebars template you can access this information by using
|
||||
|
||||
Here is a list of the properties that are exposed:
|
||||
|
||||
- ***language*** Language of the book in the form `en`. To use in <code class="language-html">\<html lang="{{ language }}"></code> for example.
|
||||
At the moment it is hardcoded.
|
||||
- ***title*** Title of the book, as specified in `book.json`
|
||||
- ***language*** Language of the book in the form `en`. To use in <code
|
||||
class="language-html">\<html lang="{{ language }}"></code> for example. At the
|
||||
moment it is hardcoded.
|
||||
- ***title*** Title of the book, as specified in `book.toml`
|
||||
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
||||
|
||||
- ***path*** Relative path to the original markdown file from the source directory
|
||||
- ***path*** Relative path to the original markdown file from the source
|
||||
directory
|
||||
- ***content*** This is the rendered markdown.
|
||||
- ***path_to_root*** This is a path containing exclusively `../`'s that points to the root of the book from the current file.
|
||||
Since the original directory structure is maintained, it is useful to prepend relative links with this `path_to_root`.
|
||||
- ***path_to_root*** This is a path containing exclusively `../`'s that points
|
||||
to the root of the book from the current file. Since the original directory
|
||||
structure is maintained, it is useful to prepend relative links with this
|
||||
`path_to_root`.
|
||||
|
||||
- ***chapters*** Is an array of dictionaries of the form
|
||||
```json
|
||||
{"section": "1.2.1", "name": "name of this chapter", "path": "dir/markdown.md"}
|
||||
```
|
||||
containing all the chapters of the book. It is used for example to construct the table of contents (sidebar).
|
||||
containing all the chapters of the book. It is used for example to construct
|
||||
the table of contents (sidebar).
|
||||
|
||||
## Handlebars Helpers
|
||||
|
||||
In addition to the properties you can access, there are some handlebars helpers at your disposal.
|
||||
In addition to the properties you can access, there are some handlebars helpers
|
||||
at your disposal.
|
||||
|
||||
1. ### toc
|
||||
### 1. toc
|
||||
|
||||
The toc helper is used like this
|
||||
|
||||
@@ -67,7 +74,7 @@ In addition to the properties you can access, there are some handlebars helpers
|
||||
</script>
|
||||
```
|
||||
|
||||
2. ### previous / next
|
||||
### 2. previous / next
|
||||
|
||||
The previous and next helpers expose a `link` and `name` property to the previous and next chapters.
|
||||
|
||||
@@ -86,5 +93,5 @@ In addition to the properties you can access, there are some handlebars helpers
|
||||
|
||||
------
|
||||
|
||||
*If you would like me to expose other properties or helpers, please [create a new issue](https://github.com/azerupi/mdBook/issues)
|
||||
and I will consider it.*
|
||||
*If you would like other properties or helpers exposed, please [create a new
|
||||
issue](https://github.com/rust-lang-nursery/mdBook/issues)*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Syntax Highlighting
|
||||
|
||||
For syntax highlighting I use [Highlight.js](https://highlightjs.org) with a custom theme.
|
||||
For syntax highlighting I use [Highlight.js](https://highlightjs.org) with a
|
||||
custom theme.
|
||||
|
||||
Automatic language detection has been turned off, so you will probably want to
|
||||
specify the programming language you use like this
|
||||
@@ -12,48 +13,57 @@ fn main() {
|
||||
```</code></pre>
|
||||
|
||||
## Custom theme
|
||||
Like the rest of the theme, the files used for syntax highlighting can be overridden with your own.
|
||||
Like the rest of the theme, the files used for syntax highlighting can be
|
||||
overridden with your own.
|
||||
|
||||
- ***highlight.js*** normally you shouldn't have to overwrite this file, unless you want to use a more recent version.
|
||||
- ***highlight.js*** normally you shouldn't have to overwrite this file, unless
|
||||
you want to use a more recent version.
|
||||
- ***highlight.css*** theme used by highlight.js for syntax highlighting.
|
||||
|
||||
If you want to use another theme for `highlight.js` download it from their website, or make it yourself,
|
||||
rename it to `highlight.css` and put it in `src/theme` (or the equivalent if you changed your source folder)
|
||||
If you want to use another theme for `highlight.js` download it from their
|
||||
website, or make it yourself, rename it to `highlight.css` and put it in
|
||||
`src/theme` (or the equivalent if you changed your source folder)
|
||||
|
||||
Now your theme will be used instead of the default theme.
|
||||
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that let's you hide code lines by prepending them with a `#`.
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them
|
||||
with a `#`.
|
||||
|
||||
|
||||
```bash
|
||||
#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
|
||||
|
||||
If you think the default theme doesn't look quite right for a specific language, or could be improved.
|
||||
Feel free to [submit a new issue](https://github.com/azerupi/mdBook/issues) explaining what you have in mind and I will take a look at it.
|
||||
If you think the default theme doesn't look quite right for a specific language,
|
||||
or could be improved. Feel free to [submit a new
|
||||
issue](https://github.com/rust-lang-nursery/mdBook/issues) explaining what you
|
||||
have in mind and I will take a look at it.
|
||||
|
||||
You could also create a pull-request with the proposed improvements.
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# Theme
|
||||
|
||||
The default renderer uses a [handlebars](http://handlebarsjs.com/) template to render your markdown files and comes with a default theme
|
||||
included in the mdBook binary.
|
||||
|
||||
The theme is totally customizable, you can selectively replace every file from the theme by your own by adding a
|
||||
`theme` directory in your source folder. Create a new file with the name of the file you want to override
|
||||
and now that file will be used instead of the default file.
|
||||
|
||||
Here are the files you can override:
|
||||
|
||||
- ***index.hbs*** is the handlebars template.
|
||||
- ***book.css*** is the style used in the output. If you want to change the design of your book, this is probably the file you want to modify. Sometimes in conjunction with `index.hbs` when you want to radically change the layout.
|
||||
- ***book.js*** is mostly used to add client side functionality, like hiding / un-hiding the sidebar, changing the theme, ...
|
||||
- ***highlight.js*** is the JavaScript that is used to highlight code snippets, you should not need to modify this.
|
||||
- ***highlight.css*** is the theme used for the code highlighting
|
||||
- ***favicon.png*** the favicon that will be used
|
||||
|
||||
Generally, when you want to tweak the theme, you don't need to override all the files. If you only need changes in the stylesheet,
|
||||
there is no point in overriding all the other files. Because custom files take precedence over built-in ones, they will not get updated with new fixes / features.
|
||||
|
||||
**Note:** When you override a file, it is possible that you break some functionality. Therefore I recommend to use the file from the default theme as template and only add / modify what you need. You can copy the default theme into your source directory automatically by using `mdbook init --theme` just remove the files you don't want to override.
|
||||
@@ -1,22 +0,0 @@
|
||||
# Rust Library
|
||||
|
||||
mdBook is not only a command line tool, it can be used as a crate. You can extend it,
|
||||
integrate it in current projects. Here is a short example:
|
||||
|
||||
```rust,ignore
|
||||
extern crate mdbook;
|
||||
|
||||
use mdbook::MDBook;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let mut book = MDBook::new(Path::new("my-book")) // Path to root
|
||||
.set_src(Path::new("src")) // Path from root to source directory
|
||||
.set_dest(Path::new("book")) // Path from root to output directory
|
||||
.read_config(); // Parse book.json file for configuration
|
||||
|
||||
book.build().unwrap(); // Render the book
|
||||
}
|
||||
```
|
||||
|
||||
Check here for the [API docs](../mdbook/index.html) generated by rustdoc.
|
||||
@@ -1,8 +1,7 @@
|
||||
# Contributors
|
||||
|
||||
Here is a list of the contributors who have helped improving mdBook. Big shout-out to them!
|
||||
|
||||
If you have contributed to mdBook and I forgot to add you, don't hesitate to add yourself to the list. If you are in the list, feel free to add your real name & contact information if you wish.
|
||||
Here is a list of the contributors who have helped improving mdBook. Big
|
||||
shout-out to them!
|
||||
|
||||
- [mdinger](https://github.com/mdinger)
|
||||
- Kevin ([kbknapp](https://github.com/kbknapp))
|
||||
@@ -11,3 +10,11 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add
|
||||
- Wayne Nilsen ([waynenilsen](https://github.com/waynenilsen))
|
||||
- [funnkill](https://github.com/funkill)
|
||||
- Fu Gangqiang ([FuGangqiang](https://github.com/FuGangqiang))
|
||||
- [Michael-F-Bryan](https://github.com/Michael-F-Bryan)
|
||||
- Chris Spiegel ([cspiegel](https://github.com/cspiegel))
|
||||
- [projektir](https://github.com/projektir)
|
||||
- [Phaiax](https://github.com/Phaiax)
|
||||
- Matt Ickstadt ([mattico](https://github.com/mattico))
|
||||
- Weihang Lo ([@weihanglo](https://github.com/weihanglo))
|
||||
|
||||
If you feel you're missing from this list, feel free to add yourself in a PR.
|
||||
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.
|
||||
29
build.rs
29
build.rs
@@ -1,29 +0,0 @@
|
||||
// build.rs
|
||||
|
||||
use std::process::Command;
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
|
||||
if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") {
|
||||
|
||||
// Compile stylus stylesheet to css
|
||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
|
||||
let theme_dir = Path::new(&manifest_dir).join("src/theme/");
|
||||
let stylus_dir = theme_dir.join("stylus/book.styl");
|
||||
|
||||
if !Command::new("stylus")
|
||||
.arg(format!("{}", stylus_dir.to_str().unwrap()))
|
||||
.arg("--out")
|
||||
.arg(format!("{}", theme_dir.to_str().unwrap()))
|
||||
.arg("--use")
|
||||
.arg("nib")
|
||||
.status().unwrap()
|
||||
.success() {
|
||||
panic!("Stylus encoutered an error");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,32 @@
|
||||
# `before_deploy` phase: here we package the build artifacts
|
||||
# This script takes care of building your crate and packaging it for release
|
||||
|
||||
set -ex
|
||||
|
||||
cargo build --target $TARGET --release
|
||||
main() {
|
||||
local src=$(pwd) \
|
||||
stage=
|
||||
|
||||
mkdir staging
|
||||
case $TRAVIS_OS_NAME in
|
||||
linux)
|
||||
stage=$(mktemp -d)
|
||||
;;
|
||||
osx)
|
||||
stage=$(mktemp -d -t tmp)
|
||||
;;
|
||||
esac
|
||||
|
||||
cp target/$TARGET/release/mdbook staging
|
||||
# This will slow down the build, but is necessary to not run out of disk space
|
||||
cargo clean
|
||||
|
||||
cd staging
|
||||
cargo rustc --bin mdbook --target $TARGET --release -- -C lto
|
||||
|
||||
tar czf ../${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}.tar.gz *
|
||||
cp target/$TARGET/release/mdbook $stage/
|
||||
|
||||
cd $stage
|
||||
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
|
||||
cd $src
|
||||
|
||||
rm -rf $stage
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# `install` phase: install stuff needed for the `script` phase
|
||||
|
||||
set -ex
|
||||
|
||||
case $TARGET in
|
||||
# Install standard libraries needed for cross compilation
|
||||
i686-apple-darwin | \
|
||||
i686-unknown-linux-gnu | \
|
||||
x86_64-unknown-linux-musl)
|
||||
case $TRAVIS_RUST_VERSION in
|
||||
stable)
|
||||
# e.g. 1.6.0
|
||||
version=$(rustc -V | cut -d' ' -f2)
|
||||
;;
|
||||
*)
|
||||
version=$TRAVIS_RUST_VERSION
|
||||
;;
|
||||
esac
|
||||
tarball=rust-std-${version}-${TARGET}
|
||||
|
||||
curl -Os http://static.rust-lang.org/dist/${tarball}.tar.gz
|
||||
|
||||
tar xzf ${tarball}.tar.gz
|
||||
|
||||
${tarball}/install.sh --prefix=$(rustc --print sysroot)
|
||||
|
||||
rm -r ${tarball}
|
||||
rm ${tarball}.tar.gz
|
||||
;;
|
||||
# Nothing to do for native builds
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
24
ci/script.sh
24
ci/script.sh
@@ -1,24 +0,0 @@
|
||||
# `script` phase: you usually build, test and generate docs in this phase
|
||||
|
||||
set -ex
|
||||
|
||||
case "$TRAVIS_OS_NAME" in
|
||||
linux)
|
||||
host=x86_64-unknown-linux-gnu
|
||||
;;
|
||||
osx)
|
||||
host=x86_64-apple-darwin
|
||||
;;
|
||||
esac
|
||||
|
||||
# NOTE Workaround for rust-lang/rust#31907 - disable doc tests when crossing
|
||||
if [ "$host" != "$TARGET" ]; 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
|
||||
|
||||
cargo build --target $TARGET --verbose
|
||||
cargo test --target $TARGET --verbose
|
||||
42
deploy.sh
42
deploy.sh
@@ -1,42 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit on error or variable unset
|
||||
set -o errexit -o nounset
|
||||
|
||||
NC='\033[39m'
|
||||
CYAN='\033[36m'
|
||||
GREEN='\033[32m'
|
||||
|
||||
rev=$(git rev-parse --short HEAD)
|
||||
|
||||
echo -e "${CYAN}Running cargo doc${NC}"
|
||||
# Run cargo doc
|
||||
cargo doc
|
||||
|
||||
echo -e "${CYAN}Running mdbook build${NC}"
|
||||
# Run mdbook to generate the book
|
||||
target/$TARGET/debug/mdbook build book-example/
|
||||
|
||||
echo -e "${CYAN}Copying book to target/doc${NC}"
|
||||
# Copy files from rendered book to doc root
|
||||
cp -R book-example/book/* target/$TARGET/doc/
|
||||
|
||||
cd target/$TARGET/doc
|
||||
|
||||
echo -e "${CYAN}Initializing Git${NC}"
|
||||
git init
|
||||
git config user.name "Mathieu David"
|
||||
git config user.email "mathieudavid@mathieudavid.org"
|
||||
|
||||
git remote add upstream "https://$GH_TOKEN@github.com/azerupi/mdBook.git"
|
||||
git fetch upstream
|
||||
git reset upstream/gh-pages
|
||||
|
||||
touch .
|
||||
|
||||
echo -e "${CYAN}Pushing changes to gh-pages${NC}"
|
||||
git add -A .
|
||||
git commit -m "rebuild pages at ${rev}"
|
||||
git push -q upstream HEAD:gh-pages
|
||||
|
||||
echo -e "${GREEN}Deployement done${NC}"
|
||||
98
examples/de-emphasize.rs
Normal file
98
examples/de-emphasize.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
//! This program removes all forms of emphasis from the markdown of the book.
|
||||
extern crate mdbook;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate pulldown_cmark_to_cmark;
|
||||
|
||||
use mdbook::book::{Book, BookItem, Chapter};
|
||||
use mdbook::errors::{Error, Result};
|
||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
||||
use mdbook::MDBook;
|
||||
use pulldown_cmark::{Event, Parser, Tag};
|
||||
use pulldown_cmark_to_cmark::fmt::cmark;
|
||||
|
||||
use std::env::{args, args_os};
|
||||
use std::ffi::OsString;
|
||||
use std::process;
|
||||
|
||||
const NAME: &str = "md-links-to-html-links";
|
||||
|
||||
fn do_it(book: OsString) -> Result<()> {
|
||||
let mut book = MDBook::load(book)?;
|
||||
book.with_preprecessor(Deemphasize);
|
||||
book.build()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if args_os().count() != 2 {
|
||||
eprintln!("USAGE: {} <book>", args().next().expect("executable"));
|
||||
return;
|
||||
}
|
||||
if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
struct Deemphasize;
|
||||
|
||||
impl Preprocessor for Deemphasize {
|
||||
fn name(&self) -> &str {
|
||||
NAME
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
||||
eprintln!("Running '{}' preprocessor", self.name());
|
||||
let mut num_removed_items = 0;
|
||||
|
||||
process(&mut book.sections, &mut num_removed_items)?;
|
||||
|
||||
eprintln!(
|
||||
"{}: removed {} events from markdown stream.",
|
||||
self.name(),
|
||||
num_removed_items
|
||||
);
|
||||
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
fn process<'a, I>(items: I, num_removed_items: &mut usize) -> Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = &'a mut BookItem> + 'a,
|
||||
{
|
||||
for item in items {
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
eprintln!("{}: processing chapter '{}'", NAME, chapter.name);
|
||||
|
||||
let md = remove_emphasis(num_removed_items, chapter)?;
|
||||
chapter.content = md;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_emphasis(
|
||||
num_removed_items: &mut usize,
|
||||
chapter: &mut Chapter,
|
||||
) -> Result<String> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
|
||||
let events = Parser::new(&chapter.content).filter(|e| {
|
||||
let should_keep = match *e {
|
||||
Event::Start(Tag::Emphasis)
|
||||
| Event::Start(Tag::Strong)
|
||||
| Event::End(Tag::Emphasis)
|
||||
| Event::End(Tag::Strong) => false,
|
||||
_ => true,
|
||||
};
|
||||
if !should_keep {
|
||||
*num_removed_items += 1;
|
||||
}
|
||||
should_keep
|
||||
});
|
||||
|
||||
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
|
||||
Error::from(format!("Markdown serialization failed: {}", err))
|
||||
})
|
||||
}
|
||||
3
release.toml
Normal file
3
release.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
sign-commit = true
|
||||
push-remote = "origin"
|
||||
tag-prefix = "v"
|
||||
@@ -1,240 +0,0 @@
|
||||
#[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;
|
||||
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
|
||||
// Uses for the Watch feature
|
||||
#[cfg(feature = "watch")]
|
||||
use notify::Watcher;
|
||||
#[cfg(feature = "watch")]
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
|
||||
use mdbook::MDBook;
|
||||
|
||||
const NAME: &'static str = "mdbook";
|
||||
|
||||
fn main() {
|
||||
// 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();
|
||||
|
||||
// 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),
|
||||
#[cfg(feature = "watch")]
|
||||
("watch", Some(sub_matches)) => watch(sub_matches),
|
||||
("test", Some(sub_matches)) => test(sub_matches),
|
||||
(_, _) => unreachable!()
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
writeln!(&mut io::stderr(), "An error occured:\n{}", e).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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Init command implementation
|
||||
fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir);
|
||||
|
||||
// Call the function that does the initialization
|
||||
try!(book.init());
|
||||
|
||||
// If flag `--theme` is present, copy theme to src
|
||||
if args.is_present("theme") {
|
||||
|
||||
// Skip this if `--force` is present
|
||||
if !args.is_present("force") {
|
||||
// Print warning
|
||||
print!("\nCopying the default theme to {:?}", book.get_src());
|
||||
println!("could potentially overwrite files already present in that directory.");
|
||||
print!("\nAre you sure you want to continue? (y/n) ");
|
||||
|
||||
// Read answer from user and exit if it's not 'yes'
|
||||
if !confirm() {
|
||||
println!("\nSkipping...\n");
|
||||
println!("All done, no errors...");
|
||||
::std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Call the function that copies the theme
|
||||
try!(book.copy_theme());
|
||||
println!("\nTheme copied.");
|
||||
|
||||
}
|
||||
|
||||
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`
|
||||
let is_dest_inside_root = book.get_dest().starts_with(book.get_root());
|
||||
|
||||
if !args.is_present("force") && is_dest_inside_root {
|
||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||
|
||||
if confirm() {
|
||||
book.create_gitignore();
|
||||
println!("\n.gitignore created.");
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nAll done, no errors...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// Build command implementation
|
||||
fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let 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)
|
||||
} else {
|
||||
p.to_path_buf()
|
||||
}
|
||||
} else {
|
||||
env::current_dir().unwrap()
|
||||
}
|
||||
}
|
||||
567
src/book/book.rs
Normal file
567
src/book/book.rs
Normal file
@@ -0,0 +1,567 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
use config::BuildConfig;
|
||||
use errors::*;
|
||||
|
||||
/// Load a book into memory from its `src/` directory.
|
||||
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
let summary_md = src_dir.join("SUMMARY.md");
|
||||
|
||||
let mut summary_content = String::new();
|
||||
File::open(summary_md)
|
||||
.chain_err(|| "Couldn't open SUMMARY.md")?
|
||||
.read_to_string(&mut summary_content)?;
|
||||
|
||||
let summary = parse_summary(&summary_content).chain_err(|| "Summary parsing failed")?;
|
||||
|
||||
if cfg.create_missing {
|
||||
create_missing(&src_dir, &summary).chain_err(|| "Unable to create missing chapters")?;
|
||||
}
|
||||
|
||||
load_book_from_disk(&summary, src_dir)
|
||||
}
|
||||
|
||||
fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||
let mut items: Vec<_> = summary
|
||||
.prefix_chapters
|
||||
.iter()
|
||||
.chain(summary.numbered_chapters.iter())
|
||||
.chain(summary.suffix_chapters.iter())
|
||||
.collect();
|
||||
|
||||
while !items.is_empty() {
|
||||
let next = items.pop().expect("already checked");
|
||||
|
||||
if let SummaryItem::Link(ref link) = *next {
|
||||
let filename = src_dir.join(&link.location);
|
||||
if !filename.exists() {
|
||||
if let Some(parent) = filename.parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
debug!("Creating missing file {}", filename.display());
|
||||
|
||||
let mut f = File::create(&filename)?;
|
||||
writeln!(f, "# {}", link.name)?;
|
||||
}
|
||||
|
||||
items.extend(&link.nested_items);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A dumb tree structure representing a book.
|
||||
///
|
||||
/// For the moment a book is just a collection of `BookItems` which are
|
||||
/// accessible by either iterating (immutably) over the book with [`iter()`], or
|
||||
/// recursively applying a closure to each section to mutate the chapters, using
|
||||
/// [`for_each_mut()`].
|
||||
///
|
||||
/// [`iter()`]: #method.iter
|
||||
/// [`for_each_mut()`]: #method.for_each_mut
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Book {
|
||||
/// The sections in this book.
|
||||
pub sections: Vec<BookItem>,
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
impl Book {
|
||||
/// Create an empty book.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Get a depth-first iterator over the items in the book.
|
||||
pub fn iter(&self) -> BookItems {
|
||||
BookItems {
|
||||
items: self.sections.iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively apply a closure to each item in the book, allowing you to
|
||||
/// mutate them.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Unlike the `iter()` method, this requires a closure instead of returning
|
||||
/// an iterator. This is because using iterators can possibly allow you
|
||||
/// to have iterator invalidation errors.
|
||||
pub fn for_each_mut<F>(&mut self, mut func: F)
|
||||
where
|
||||
F: FnMut(&mut BookItem),
|
||||
{
|
||||
for_each_mut(&mut func, &mut self.sections);
|
||||
}
|
||||
|
||||
/// Append a `BookItem` to the `Book`.
|
||||
pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
|
||||
self.sections.push(item.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_each_mut<'a, F, I>(func: &mut F, items: I)
|
||||
where
|
||||
F: FnMut(&mut BookItem),
|
||||
I: IntoIterator<Item = &'a mut BookItem>,
|
||||
{
|
||||
for item in items {
|
||||
if let &mut BookItem::Chapter(ref mut ch) = item {
|
||||
for_each_mut(func, &mut ch.sub_items);
|
||||
}
|
||||
|
||||
func(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum representing any type of item which can be added to a book.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum BookItem {
|
||||
/// A nested chapter.
|
||||
Chapter(Chapter),
|
||||
/// A section separator.
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl From<Chapter> for BookItem {
|
||||
fn from(other: Chapter) -> BookItem {
|
||||
BookItem::Chapter(other)
|
||||
}
|
||||
}
|
||||
|
||||
/// The representation of a "chapter", usually mapping to a single file on
|
||||
/// disk however it may contain multiple sub-chapters.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Chapter {
|
||||
/// The chapter's name.
|
||||
pub name: String,
|
||||
/// The chapter's contents.
|
||||
pub content: String,
|
||||
/// The chapter's section number, if it has one.
|
||||
pub number: Option<SectionNumber>,
|
||||
/// Nested items.
|
||||
pub sub_items: Vec<BookItem>,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
pub path: PathBuf,
|
||||
/// An ordered list of the names of each chapter above this one, in the hierarchy.
|
||||
pub parent_names: Vec<String>,
|
||||
}
|
||||
|
||||
impl Chapter {
|
||||
/// Create a new chapter with the provided content.
|
||||
pub fn new<P: Into<PathBuf>>(
|
||||
name: &str,
|
||||
content: String,
|
||||
path: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Chapter {
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content: content,
|
||||
path: path.into(),
|
||||
parent_names: parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the provided `Summary` to load a `Book` from disk.
|
||||
///
|
||||
/// You need to pass in the book's source directory because all the links in
|
||||
/// `SUMMARY.md` give the chapter locations relative to it.
|
||||
fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
|
||||
debug!("Loading the book from disk");
|
||||
let src_dir = src_dir.as_ref();
|
||||
|
||||
let prefix = summary.prefix_chapters.iter();
|
||||
let numbered = summary.numbered_chapters.iter();
|
||||
let suffix = summary.suffix_chapters.iter();
|
||||
|
||||
let summary_items = prefix.chain(numbered).chain(suffix);
|
||||
|
||||
let mut chapters = Vec::new();
|
||||
|
||||
for summary_item in summary_items {
|
||||
let chapter = load_summary_item(summary_item, src_dir, Vec::new())?;
|
||||
chapters.push(chapter);
|
||||
}
|
||||
|
||||
Ok(Book {
|
||||
sections: chapters,
|
||||
__non_exhaustive: (),
|
||||
})
|
||||
}
|
||||
|
||||
fn load_summary_item<P: AsRef<Path>>(
|
||||
item: &SummaryItem,
|
||||
src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Result<BookItem> {
|
||||
match *item {
|
||||
SummaryItem::Separator => Ok(BookItem::Separator),
|
||||
SummaryItem::Link(ref link) => {
|
||||
load_chapter(link, src_dir, parent_names).map(|c| BookItem::Chapter(c))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_chapter<P: AsRef<Path>>(
|
||||
link: &Link,
|
||||
src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Result<Chapter> {
|
||||
debug!("Loading {} ({})", link.name, link.location.display());
|
||||
let src_dir = src_dir.as_ref();
|
||||
|
||||
let location = if link.location.is_absolute() {
|
||||
link.location.clone()
|
||||
} else {
|
||||
src_dir.join(&link.location)
|
||||
};
|
||||
|
||||
let mut f = File::open(&location)
|
||||
.chain_err(|| format!("Chapter file not found, {}", link.location.display()))?;
|
||||
|
||||
let mut content = String::new();
|
||||
f.read_to_string(&mut content)
|
||||
.chain_err(|| format!("Unable to read \"{}\" ({})", link.name, location.display()))?;
|
||||
|
||||
let stripped = location
|
||||
.strip_prefix(&src_dir)
|
||||
.expect("Chapters are always inside a book");
|
||||
|
||||
let mut sub_item_parents = parent_names.clone();
|
||||
let mut ch = Chapter::new(&link.name, content, stripped, parent_names);
|
||||
ch.number = link.number.clone();
|
||||
|
||||
sub_item_parents.push(link.name.clone());
|
||||
let sub_items = link
|
||||
.nested_items
|
||||
.iter()
|
||||
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
ch.sub_items = sub_items;
|
||||
|
||||
Ok(ch)
|
||||
}
|
||||
|
||||
/// A depth-first iterator over the items in a book.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This struct shouldn't be created directly, instead prefer the
|
||||
/// [`Book::iter()`] method.
|
||||
///
|
||||
/// [`Book::iter()`]: struct.Book.html#method.iter
|
||||
pub struct BookItems<'a> {
|
||||
items: VecDeque<&'a BookItem>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for BookItems<'a> {
|
||||
type Item = &'a BookItem;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let item = self.items.pop_front();
|
||||
|
||||
if let Some(&BookItem::Chapter(ref ch)) = item {
|
||||
// if we wanted a breadth-first iterator we'd `extend()` here
|
||||
for sub_item in ch.sub_items.iter().rev() {
|
||||
self.items.push_front(sub_item);
|
||||
}
|
||||
}
|
||||
|
||||
item
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Chapter {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
if let Some(ref section_number) = self.number {
|
||||
write!(f, "{} ", section_number)?;
|
||||
}
|
||||
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
const DUMMY_SRC: &'static str = "
|
||||
# Dummy Chapter
|
||||
|
||||
this is some dummy text.
|
||||
|
||||
And here is some \
|
||||
more text.
|
||||
";
|
||||
|
||||
/// Create a dummy `Link` in a temporary directory.
|
||||
fn dummy_link() -> (Link, TempDir) {
|
||||
let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap();
|
||||
|
||||
let chapter_path = temp.path().join("chapter_1.md");
|
||||
File::create(&chapter_path)
|
||||
.unwrap()
|
||||
.write(DUMMY_SRC.as_bytes())
|
||||
.unwrap();
|
||||
|
||||
let link = Link::new("Chapter 1", chapter_path);
|
||||
|
||||
(link, temp)
|
||||
}
|
||||
|
||||
/// Create a nested `Link` written to a temporary directory.
|
||||
fn nested_links() -> (Link, TempDir) {
|
||||
let (mut root, temp_dir) = dummy_link();
|
||||
|
||||
let second_path = temp_dir.path().join("second.md");
|
||||
|
||||
File::create(&second_path)
|
||||
.unwrap()
|
||||
.write_all("Hello World!".as_bytes())
|
||||
.unwrap();
|
||||
|
||||
let mut second = Link::new("Nested Chapter 1", &second_path);
|
||||
second.number = Some(SectionNumber(vec![1, 2]));
|
||||
|
||||
root.nested_items.push(second.clone().into());
|
||||
root.nested_items.push(SummaryItem::Separator);
|
||||
root.nested_items.push(second.clone().into());
|
||||
|
||||
(root, temp_dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_a_single_chapter_from_disk() {
|
||||
let (link, temp_dir) = dummy_link();
|
||||
let should_be = Chapter::new(
|
||||
"Chapter 1",
|
||||
DUMMY_SRC.to_string(),
|
||||
"chapter_1.md",
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_load_a_nonexistent_chapter() {
|
||||
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
|
||||
|
||||
let got = load_chapter(&link, "", Vec::new());
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_recursive_link_with_separators() {
|
||||
let (root, temp) = nested_links();
|
||||
|
||||
let nested = Chapter {
|
||||
name: String::from("Nested Chapter 1"),
|
||||
content: String::from("Hello World!"),
|
||||
number: Some(SectionNumber(vec![1, 2])),
|
||||
path: PathBuf::from("second.md"),
|
||||
parent_names: vec![String::from("Chapter 1")],
|
||||
sub_items: Vec::new(),
|
||||
};
|
||||
let should_be = BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: PathBuf::from("chapter_1.md"),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(nested.clone()),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(nested.clone()),
|
||||
],
|
||||
});
|
||||
|
||||
let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_a_book_with_a_single_chapter() {
|
||||
let (link, temp) = dummy_link();
|
||||
let summary = Summary {
|
||||
numbered_chapters: vec![SummaryItem::Link(link)],
|
||||
..Default::default()
|
||||
};
|
||||
let should_be = Book {
|
||||
sections: vec![BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
path: PathBuf::from("chapter_1.md"),
|
||||
..Default::default()
|
||||
})],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got = load_book_from_disk(&summary, temp.path()).unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn book_iter_iterates_over_sequential_items() {
|
||||
let book = Book {
|
||||
sections: vec![
|
||||
BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
..Default::default()
|
||||
}),
|
||||
BookItem::Separator,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let should_be: Vec<_> = book.sections.iter().collect();
|
||||
|
||||
let got: Vec<_> = book.iter().collect();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterate_over_nested_book_items() {
|
||||
let book = Book {
|
||||
sections: vec![
|
||||
BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: PathBuf::from("Chapter_1/index.md"),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Hello World",
|
||||
String::new(),
|
||||
"Chapter_1/hello.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Goodbye World",
|
||||
String::new(),
|
||||
"Chapter_1/goodbye.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
],
|
||||
}),
|
||||
BookItem::Separator,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got: Vec<_> = book.iter().collect();
|
||||
|
||||
assert_eq!(got.len(), 5);
|
||||
|
||||
// checking the chapter names are in the order should be sufficient here...
|
||||
let chapter_names: Vec<String> = got
|
||||
.into_iter()
|
||||
.filter_map(|i| match *i {
|
||||
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
|
||||
_ => None,
|
||||
}).collect();
|
||||
let should_be: Vec<_> = vec![
|
||||
String::from("Chapter 1"),
|
||||
String::from("Hello World"),
|
||||
String::from("Goodbye World"),
|
||||
];
|
||||
|
||||
assert_eq!(chapter_names, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_each_mut_visits_all_items() {
|
||||
let mut book = Book {
|
||||
sections: vec![
|
||||
BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: PathBuf::from("Chapter_1/index.md"),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Hello World",
|
||||
String::new(),
|
||||
"Chapter_1/hello.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Goodbye World",
|
||||
String::new(),
|
||||
"Chapter_1/goodbye.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
],
|
||||
}),
|
||||
BookItem::Separator,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let num_items = book.iter().count();
|
||||
let mut visited = 0;
|
||||
|
||||
book.for_each_mut(|_| visited += 1);
|
||||
|
||||
assert_eq!(visited, num_items);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_load_chapters_with_an_empty_path() {
|
||||
let (_, temp) = dummy_link();
|
||||
let summary = Summary {
|
||||
numbered_chapters: vec![SummaryItem::Link(Link {
|
||||
name: String::from("Empty"),
|
||||
location: PathBuf::from(""),
|
||||
..Default::default()
|
||||
})],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got = load_book_from_disk(&summary, temp.path());
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_load_chapters_when_the_link_is_a_directory() {
|
||||
let (_, temp) = dummy_link();
|
||||
let dir = temp.path().join("nested");
|
||||
fs::create_dir(&dir).unwrap();
|
||||
|
||||
let summary = Summary {
|
||||
numbered_chapters: vec![SummaryItem::Link(Link {
|
||||
name: String::from("nested"),
|
||||
location: dir,
|
||||
..Default::default()
|
||||
})],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got = load_book_from_disk(&summary, temp.path());
|
||||
assert!(got.is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,109 +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,
|
||||
pub description: 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(),
|
||||
description: 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, description
|
||||
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("\"", "") }
|
||||
if let Some(a) = config.find_path(&["description"]) { self.description = 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,89 +0,0 @@
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use self::rustc_serialize::json::{Json, ToJson};
|
||||
use std::path::PathBuf;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BookItem {
|
||||
Chapter(String, Chapter), // String = section
|
||||
Affix(Chapter),
|
||||
Spacer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Chapter {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub sub_items: Vec<BookItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BookItems<'a> {
|
||||
pub items: &'a [BookItem],
|
||||
pub current_index: usize,
|
||||
pub stack: Vec<(&'a [BookItem], usize)>,
|
||||
}
|
||||
|
||||
|
||||
impl Chapter {
|
||||
|
||||
pub fn new(name: String, path: PathBuf) -> Self {
|
||||
|
||||
Chapter {
|
||||
name: name,
|
||||
path: path,
|
||||
sub_items: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Shamelessly copied from Rustbook
|
||||
// (https://github.com/rust-lang/rust/blob/master/src/rustbook/book.rs)
|
||||
impl<'a> Iterator for BookItems<'a> {
|
||||
type Item = &'a BookItem;
|
||||
|
||||
fn next(&mut self) -> Option<&'a BookItem> {
|
||||
loop {
|
||||
if self.current_index >= self.items.len() {
|
||||
match self.stack.pop() {
|
||||
None => return None,
|
||||
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();
|
||||
|
||||
match *cur {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
200
src/book/init.rs
Normal file
200
src/book/init.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use toml;
|
||||
|
||||
use super::MDBook;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
use theme;
|
||||
|
||||
/// A helper for setting up a new book and its directory structure.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BookBuilder {
|
||||
root: PathBuf,
|
||||
create_gitignore: bool,
|
||||
config: Config,
|
||||
copy_theme: bool,
|
||||
}
|
||||
|
||||
impl BookBuilder {
|
||||
/// Create a new `BookBuilder` which will generate a book in the provided
|
||||
/// root directory.
|
||||
pub fn new<P: Into<PathBuf>>(root: P) -> BookBuilder {
|
||||
BookBuilder {
|
||||
root: root.into(),
|
||||
create_gitignore: false,
|
||||
config: Config::default(),
|
||||
copy_theme: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the `Config` to be used.
|
||||
pub fn with_config(&mut self, cfg: Config) -> &mut BookBuilder {
|
||||
self.config = cfg;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the config used by the `BookBuilder`.
|
||||
pub fn config(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Should the theme be copied into the generated book (so users can tweak
|
||||
/// it)?
|
||||
pub fn copy_theme(&mut self, copy: bool) -> &mut BookBuilder {
|
||||
self.copy_theme = copy;
|
||||
self
|
||||
}
|
||||
|
||||
/// Should we create a `.gitignore` file?
|
||||
pub fn create_gitignore(&mut self, create: bool) -> &mut BookBuilder {
|
||||
self.create_gitignore = create;
|
||||
self
|
||||
}
|
||||
|
||||
/// Generate the actual book. This will:
|
||||
///
|
||||
/// - Create the directory structure.
|
||||
/// - Stub out some dummy chapters and the `SUMMARY.md`.
|
||||
/// - Create a `.gitignore` (if applicable)
|
||||
/// - Create a themes directory and populate it (if applicable)
|
||||
/// - Generate a `book.toml` file,
|
||||
/// - Then load the book so we can build it or run tests.
|
||||
pub fn build(&self) -> Result<MDBook> {
|
||||
info!("Creating a new book with stub content");
|
||||
|
||||
self.create_directory_structure()
|
||||
.chain_err(|| "Unable to create directory structure")?;
|
||||
|
||||
self.create_stub_files()
|
||||
.chain_err(|| "Unable to create stub files")?;
|
||||
|
||||
if self.create_gitignore {
|
||||
self.build_gitignore()
|
||||
.chain_err(|| "Unable to create .gitignore")?;
|
||||
}
|
||||
|
||||
if self.copy_theme {
|
||||
self.copy_across_theme()
|
||||
.chain_err(|| "Unable to copy across the theme")?;
|
||||
}
|
||||
|
||||
self.write_book_toml()?;
|
||||
|
||||
match MDBook::load(&self.root) {
|
||||
Ok(book) => Ok(book),
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
|
||||
panic!(
|
||||
"The BookBuilder should always create a valid book. If you are seeing this it \
|
||||
is a bug and should be reported."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_book_toml(&self) -> Result<()> {
|
||||
debug!("Writing book.toml");
|
||||
let book_toml = self.root.join("book.toml");
|
||||
let cfg = toml::to_vec(&self.config).chain_err(|| "Unable to serialize the config")?;
|
||||
|
||||
File::create(book_toml)
|
||||
.chain_err(|| "Couldn't create book.toml")?
|
||||
.write_all(&cfg)
|
||||
.chain_err(|| "Unable to write config to book.toml")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_across_theme(&self) -> Result<()> {
|
||||
debug!("Copying theme");
|
||||
|
||||
let themedir = self
|
||||
.config
|
||||
.html_config()
|
||||
.and_then(|html| html.theme)
|
||||
.unwrap_or_else(|| self.config.book.src.join("theme"));
|
||||
let themedir = self.root.join(themedir);
|
||||
|
||||
if !themedir.exists() {
|
||||
debug!(
|
||||
"{} does not exist, creating the directory",
|
||||
themedir.display()
|
||||
);
|
||||
fs::create_dir(&themedir)?;
|
||||
}
|
||||
|
||||
let mut index = File::create(themedir.join("index.hbs"))?;
|
||||
index.write_all(theme::INDEX)?;
|
||||
|
||||
let cssdir = themedir.join("css");
|
||||
fs::create_dir(&cssdir)?;
|
||||
|
||||
let mut general_css = File::create(cssdir.join("general.css"))?;
|
||||
general_css.write_all(theme::GENERAL_CSS)?;
|
||||
|
||||
let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
|
||||
chrome_css.write_all(theme::CHROME_CSS)?;
|
||||
|
||||
let mut print_css = File::create(cssdir.join("print.css"))?;
|
||||
print_css.write_all(theme::PRINT_CSS)?;
|
||||
|
||||
let mut variables_css = File::create(cssdir.join("variables.css"))?;
|
||||
variables_css.write_all(theme::VARIABLES_CSS)?;
|
||||
|
||||
let mut favicon = File::create(themedir.join("favicon.png"))?;
|
||||
favicon.write_all(theme::FAVICON)?;
|
||||
|
||||
let mut js = File::create(themedir.join("book.js"))?;
|
||||
js.write_all(theme::JS)?;
|
||||
|
||||
let mut highlight_css = File::create(themedir.join("highlight.css"))?;
|
||||
highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
|
||||
|
||||
let mut highlight_js = File::create(themedir.join("highlight.js"))?;
|
||||
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_gitignore(&self) -> Result<()> {
|
||||
debug!("Creating .gitignore");
|
||||
|
||||
let mut f = File::create(self.root.join(".gitignore"))?;
|
||||
|
||||
writeln!(f, "{}", self.config.build.build_dir.display())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_stub_files(&self) -> Result<()> {
|
||||
debug!("Creating example book contents");
|
||||
let src_dir = self.root.join(&self.config.book.src);
|
||||
|
||||
let summary = src_dir.join("SUMMARY.md");
|
||||
let mut f = File::create(&summary).chain_err(|| "Unable to create SUMMARY.md")?;
|
||||
writeln!(f, "# Summary")?;
|
||||
writeln!(f, "")?;
|
||||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||
|
||||
let chapter_1 = src_dir.join("chapter_1.md");
|
||||
let mut f = File::create(&chapter_1).chain_err(|| "Unable to create chapter_1.md")?;
|
||||
writeln!(f, "# Chapter 1")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_directory_structure(&self) -> Result<()> {
|
||||
debug!("Creating directory tree");
|
||||
fs::create_dir_all(&self.root)?;
|
||||
|
||||
let src = self.root.join(&self.config.book.src);
|
||||
fs::create_dir_all(&src)?;
|
||||
|
||||
let build = self.root.join(&self.config.build.build_dir);
|
||||
fs::create_dir_all(&build)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,406 +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};
|
||||
|
||||
|
||||
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() {
|
||||
fs::create_dir_all(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(())
|
||||
}
|
||||
|
||||
pub fn create_gitignore(&self) {
|
||||
let gitignore = self.get_gitignore();
|
||||
|
||||
if !gitignore.exists() {
|
||||
// Gitignore does not exist, create it
|
||||
|
||||
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`. If it
|
||||
// is not, `strip_prefix` will return an Error.
|
||||
if !self.get_dest().starts_with(self.get_root()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let relative = self.get_dest().strip_prefix(self.get_root())
|
||||
.expect("Destination is not relative to root.");
|
||||
let relative = relative.to_str()
|
||||
.expect("Path could not be yielded into a string slice.");
|
||||
|
||||
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<(), 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 get_gitignore(&self) -> PathBuf {
|
||||
self.config.get_root().join(".gitignore")
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
// favicon.png
|
||||
let mut favicon = try!(File::create(&theme_dir.join("favicon.png")));
|
||||
try!(favicon.write_all(theme::FAVICON));
|
||||
|
||||
// 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 get_root(&self) -> &Path {
|
||||
self.config.get_root()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
pub fn set_description(mut self, description: &str) -> Self {
|
||||
self.config.description = description.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_description(&self) -> &str {
|
||||
&self.config.description
|
||||
}
|
||||
|
||||
// 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(())
|
||||
}
|
||||
|
||||
}
|
||||
574
src/book/mod.rs
574
src/book/mod.rs
@@ -1,7 +1,569 @@
|
||||
pub mod mdbook;
|
||||
pub mod bookitem;
|
||||
pub mod bookconfig;
|
||||
//! The internal representation of a book and infrastructure for loading it from
|
||||
//! disk and building it.
|
||||
//!
|
||||
//! For examples on using `MDBook`, consult the [top-level documentation][1].
|
||||
//!
|
||||
//! [1]: ../index.html
|
||||
|
||||
pub use self::bookitem::{BookItem, BookItems};
|
||||
pub use self::bookconfig::BookConfig;
|
||||
pub use self::mdbook::MDBook;
|
||||
mod book;
|
||||
mod init;
|
||||
mod summary;
|
||||
|
||||
pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
|
||||
pub use self::init::BookBuilder;
|
||||
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use toml::Value;
|
||||
|
||||
use errors::*;
|
||||
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
||||
use utils;
|
||||
|
||||
use config::Config;
|
||||
|
||||
/// The object used to manage and build a book.
|
||||
pub struct MDBook {
|
||||
/// The book's root directory.
|
||||
pub root: PathBuf,
|
||||
/// The configuration used to tweak now a book is built.
|
||||
pub config: Config,
|
||||
/// A representation of the book's contents in memory.
|
||||
pub book: Book,
|
||||
renderers: Vec<Box<Renderer>>,
|
||||
|
||||
/// List of pre-processors to be run on the book
|
||||
preprocessors: Vec<Box<Preprocessor>>,
|
||||
}
|
||||
|
||||
impl MDBook {
|
||||
/// Load a book from its root directory on disk.
|
||||
pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
|
||||
let book_root = book_root.into();
|
||||
let config_location = book_root.join("book.toml");
|
||||
|
||||
// the book.json file is no longer used, so we should emit a warning to
|
||||
// let people know to migrate to book.toml
|
||||
if book_root.join("book.json").exists() {
|
||||
warn!("It appears you are still using book.json for configuration.");
|
||||
warn!("This format is no longer used, so you should migrate to the");
|
||||
warn!("book.toml format.");
|
||||
warn!("Check the user guide for migration information:");
|
||||
warn!("\thttps://rust-lang-nursery.github.io/mdBook/format/config.html");
|
||||
}
|
||||
|
||||
let mut config = if config_location.exists() {
|
||||
debug!("Loading config from {}", config_location.display());
|
||||
Config::from_disk(&config_location)?
|
||||
} else {
|
||||
Config::default()
|
||||
};
|
||||
|
||||
config.update_from_env();
|
||||
|
||||
if log_enabled!(::log::Level::Trace) {
|
||||
for line in format!("Config: {:#?}", config).lines() {
|
||||
trace!("{}", line);
|
||||
}
|
||||
}
|
||||
|
||||
MDBook::load_with_config(book_root, config)
|
||||
}
|
||||
|
||||
/// Load a book from its root directory using a custom config.
|
||||
pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
|
||||
let root = book_root.into();
|
||||
|
||||
let src_dir = root.join(&config.book.src);
|
||||
let book = book::load_book(&src_dir, &config.build)?;
|
||||
|
||||
let renderers = determine_renderers(&config);
|
||||
let preprocessors = determine_preprocessors(&config)?;
|
||||
|
||||
Ok(MDBook {
|
||||
root,
|
||||
config,
|
||||
book,
|
||||
renderers,
|
||||
preprocessors,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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::book::BookItem;
|
||||
/// # #[allow(unused_variables)]
|
||||
/// # fn main() {
|
||||
/// # let book = MDBook::load("mybook").unwrap();
|
||||
/// for item in book.iter() {
|
||||
/// match *item {
|
||||
/// BookItem::Chapter(ref chapter) => {},
|
||||
/// BookItem::Separator => {},
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // 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 {
|
||||
self.book.iter()
|
||||
}
|
||||
|
||||
/// `init()` gives you a `BookBuilder` which you can use to setup a new book
|
||||
/// and its accompanying directory structure.
|
||||
///
|
||||
/// The `BookBuilder` 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 path provided as the root directory for your book, then adds
|
||||
/// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
|
||||
/// to get you started.
|
||||
pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
|
||||
BookBuilder::new(book_root)
|
||||
}
|
||||
|
||||
/// Tells the renderer to build our book and put it in the build directory.
|
||||
pub fn build(&self) -> Result<()> {
|
||||
info!("Book building has started");
|
||||
|
||||
for renderer in &self.renderers {
|
||||
self.execute_build_process(&**renderer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run the entire build process for a particular `Renderer`.
|
||||
fn execute_build_process(&self, renderer: &Renderer) -> Result<()> {
|
||||
let mut preprocessed_book = self.book.clone();
|
||||
let preprocess_ctx = PreprocessorContext::new(self.root.clone(),
|
||||
self.config.clone(),
|
||||
renderer.name().to_string());
|
||||
|
||||
for preprocessor in &self.preprocessors {
|
||||
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
|
||||
debug!("Running the {} preprocessor.", preprocessor.name());
|
||||
preprocessed_book =
|
||||
preprocessor.run(&preprocess_ctx, preprocessed_book)?;
|
||||
}
|
||||
}
|
||||
|
||||
info!("Running the {} backend", renderer.name());
|
||||
self.render(&preprocessed_book, renderer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
preprocessed_book: &Book,
|
||||
renderer: &Renderer,
|
||||
) -> Result<()> {
|
||||
let name = renderer.name();
|
||||
let build_dir = self.build_dir_for(name);
|
||||
if build_dir.exists() {
|
||||
debug!(
|
||||
"Cleaning build dir for the \"{}\" renderer ({})",
|
||||
name,
|
||||
build_dir.display()
|
||||
);
|
||||
|
||||
utils::fs::remove_dir_content(&build_dir)
|
||||
.chain_err(|| "Unable to clear output directory")?;
|
||||
}
|
||||
|
||||
let render_context = RenderContext::new(
|
||||
self.root.clone(),
|
||||
preprocessed_book.clone(),
|
||||
self.config.clone(),
|
||||
build_dir,
|
||||
);
|
||||
|
||||
renderer
|
||||
.render(&render_context)
|
||||
.chain_err(|| "Rendering failed")
|
||||
}
|
||||
|
||||
/// 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/trait.Renderer.html)
|
||||
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
|
||||
self.renderers.push(Box::new(renderer));
|
||||
self
|
||||
}
|
||||
|
||||
/// Register a [`Preprocessor`](../preprocess/trait.Preprocessor.html) to be used when rendering the book.
|
||||
pub fn with_preprecessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
||||
self.preprocessors.push(Box::new(preprocessor));
|
||||
self
|
||||
}
|
||||
|
||||
/// Run `rustdoc` tests on the book, linking against the provided libraries.
|
||||
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
|
||||
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 = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
||||
|
||||
// FIXME: Is "test" the proper renderer name to use here?
|
||||
let preprocess_context = PreprocessorContext::new(self.root.clone(),
|
||||
self.config.clone(),
|
||||
"test".to_string());
|
||||
|
||||
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
|
||||
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
||||
// actual markdown files.
|
||||
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
if !ch.path.as_os_str().is_empty() {
|
||||
let path = self.source_dir().join(&ch.path);
|
||||
let content = utils::fs::file_to_string(&path)?;
|
||||
info!("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(())
|
||||
}
|
||||
|
||||
/// The logic for determining where a backend should put its build
|
||||
/// artefacts.
|
||||
///
|
||||
/// If there is only 1 renderer, put it in the directory pointed to by the
|
||||
/// `build.build_dir` key in `Config`. If there is more than one then the
|
||||
/// renderer gets its own directory within the main build dir.
|
||||
///
|
||||
/// i.e. If there were only one renderer (in this case, the HTML renderer):
|
||||
///
|
||||
/// - build/
|
||||
/// - index.html
|
||||
/// - ...
|
||||
///
|
||||
/// Otherwise if there are multiple:
|
||||
///
|
||||
/// - build/
|
||||
/// - epub/
|
||||
/// - my_awesome_book.epub
|
||||
/// - html/
|
||||
/// - index.html
|
||||
/// - ...
|
||||
/// - latex/
|
||||
/// - my_awesome_book.tex
|
||||
///
|
||||
pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
|
||||
let build_dir = self.root.join(&self.config.build.build_dir);
|
||||
|
||||
if self.renderers.len() <= 1 {
|
||||
build_dir
|
||||
} else {
|
||||
build_dir.join(backend_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the directory containing this book's source files.
|
||||
pub fn source_dir(&self) -> PathBuf {
|
||||
self.root.join(&self.config.book.src)
|
||||
}
|
||||
|
||||
/// Get the directory containing the theme resources for the book.
|
||||
pub fn theme_dir(&self) -> PathBuf {
|
||||
self.config
|
||||
.html_config()
|
||||
.unwrap_or_default()
|
||||
.theme_dir(&self.root)
|
||||
}
|
||||
}
|
||||
|
||||
/// Look at the `Config` and try to figure out what renderers to use.
|
||||
fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
|
||||
let mut renderers: Vec<Box<Renderer>> = Vec::new();
|
||||
|
||||
if let Some(output_table) = config.get("output").and_then(|o| o.as_table()) {
|
||||
for (key, table) in output_table.iter() {
|
||||
// the "html" backend has its own Renderer
|
||||
if key == "html" {
|
||||
renderers.push(Box::new(HtmlHandlebars::new()));
|
||||
} else {
|
||||
let renderer = interpret_custom_renderer(key, table);
|
||||
renderers.push(renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we couldn't find anything, add the HTML renderer as a default
|
||||
if renderers.is_empty() {
|
||||
renderers.push(Box::new(HtmlHandlebars::new()));
|
||||
}
|
||||
|
||||
renderers
|
||||
}
|
||||
|
||||
fn default_preprocessors() -> Vec<Box<Preprocessor>> {
|
||||
vec![
|
||||
Box::new(LinkPreprocessor::new()),
|
||||
Box::new(IndexPreprocessor::new()),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_default_preprocessor(pre: &Preprocessor) -> bool {
|
||||
let name = pre.name();
|
||||
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
|
||||
}
|
||||
|
||||
/// Look at the `MDBook` and try to figure out what preprocessors to run.
|
||||
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
|
||||
let preprocessor_keys = config.get("preprocessor")
|
||||
.and_then(|value| value.as_table())
|
||||
.map(|table| table.keys());
|
||||
|
||||
let mut preprocessors = if config.build.use_default_preprocessors {
|
||||
default_preprocessors()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let preprocessor_keys = match preprocessor_keys {
|
||||
Some(keys) => keys,
|
||||
// If no preprocessor field is set, default to the LinkPreprocessor and
|
||||
// IndexPreprocessor. This allows you to disable default preprocessors
|
||||
// by setting "preprocess" to an empty list.
|
||||
None => return Ok(preprocessors),
|
||||
};
|
||||
|
||||
for key in preprocessor_keys {
|
||||
match key.as_ref() {
|
||||
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
|
||||
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
|
||||
_ => bail!("{:?} is not a recognised preprocessor", key),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(preprocessors)
|
||||
}
|
||||
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
|
||||
// look for the `command` field, falling back to using the key
|
||||
// prepended by "mdbook-"
|
||||
let table_dot_command = table
|
||||
.get("command")
|
||||
.and_then(|c| c.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
|
||||
Box::new(CmdRenderer::new(key.to_string(), command.to_string()))
|
||||
}
|
||||
|
||||
/// Check whether we should run a particular `Preprocessor` in combination
|
||||
/// with the renderer, falling back to `Preprocessor::supports_renderer()`
|
||||
/// method if the user doesn't say anything.
|
||||
///
|
||||
/// The `build.use-default-preprocessors` config option can be used to ensure
|
||||
/// default preprocessors always run if they support the renderer.
|
||||
fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg: &Config) -> bool {
|
||||
// default preprocessors should be run by default (if supported)
|
||||
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
|
||||
return preprocessor.supports_renderer(renderer.name());
|
||||
}
|
||||
|
||||
let key = format!("preprocessor.{}.renderers", preprocessor.name());
|
||||
let renderer_name = renderer.name();
|
||||
|
||||
if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
|
||||
return explicit_renderers.into_iter()
|
||||
.filter_map(|val| val.as_str())
|
||||
.any(|name| name == renderer_name);
|
||||
}
|
||||
|
||||
preprocessor.supports_renderer(renderer_name)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use toml::value::{Table, Value};
|
||||
|
||||
#[test]
|
||||
fn config_defaults_to_html_renderer_if_empty() {
|
||||
let cfg = Config::default();
|
||||
|
||||
// make sure we haven't got anything in the `output` table
|
||||
assert!(cfg.get("output").is_none());
|
||||
|
||||
let got = determine_renderers(&cfg);
|
||||
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].name(), "html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_a_random_renderer_to_the_config() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.set("output.random", Table::new()).unwrap();
|
||||
|
||||
let got = determine_renderers(&cfg);
|
||||
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].name(), "random");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_a_random_renderer_with_custom_command_to_the_config() {
|
||||
let mut cfg = Config::default();
|
||||
|
||||
let mut table = Table::new();
|
||||
table.insert("command".to_string(), Value::String("false".to_string()));
|
||||
cfg.set("output.random", table).unwrap();
|
||||
|
||||
let got = determine_renderers(&cfg);
|
||||
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].name(), "random");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
|
||||
let cfg = Config::default();
|
||||
|
||||
// make sure we haven't got anything in the `preprocessor` table
|
||||
assert!(cfg.get("preprocessor").is_none());
|
||||
|
||||
let got = determine_preprocessors(&cfg);
|
||||
|
||||
assert!(got.is_ok());
|
||||
assert_eq!(got.as_ref().unwrap().len(), 2);
|
||||
assert_eq!(got.as_ref().unwrap()[0].name(), "links");
|
||||
assert_eq!(got.as_ref().unwrap()[1].name(), "index");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_default_preprocessors_works() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.build.use_default_preprocessors = false;
|
||||
|
||||
let got = determine_preprocessors(&cfg).unwrap();
|
||||
|
||||
assert_eq!(got.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_complains_if_unimplemented_preprocessor() {
|
||||
let cfg_str: &'static str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
|
||||
[preprocessor.random]
|
||||
|
||||
[build]
|
||||
build-dir = "outputs"
|
||||
create-missing = false
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// make sure the `preprocessor.random` table exists
|
||||
assert!(cfg.get_preprocessor("random").is_some());
|
||||
|
||||
let got = determine_preprocessors(&cfg);
|
||||
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_respects_preprocessor_selection() {
|
||||
let cfg_str: &'static str = r#"
|
||||
[preprocessor.links]
|
||||
renderers = ["html"]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// double-check that we can access preprocessor.links.renderers[0]
|
||||
let html = cfg.get_preprocessor("links")
|
||||
.and_then(|links| links.get("renderers"))
|
||||
.and_then(|renderers| renderers.as_array())
|
||||
.and_then(|renderers| renderers.get(0))
|
||||
.and_then(|renderer| renderer.as_str())
|
||||
.unwrap();
|
||||
assert_eq!(html, "html");
|
||||
let html_renderer = HtmlHandlebars::default();
|
||||
let pre = LinkPreprocessor::new();
|
||||
|
||||
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
|
||||
assert!(should_run);
|
||||
}
|
||||
|
||||
struct BoolPreprocessor(bool);
|
||||
impl Preprocessor for BoolPreprocessor {
|
||||
fn name(&self) -> &str {
|
||||
"bool-preprocessor"
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, _renderer: &str) -> bool {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
|
||||
let cfg = Config::default();
|
||||
let html = HtmlHandlebars::new();
|
||||
|
||||
let should_be = true;
|
||||
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
let should_be = false;
|
||||
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
727
src/book/summary.rs
Normal file
727
src/book/summary.rs
Normal file
@@ -0,0 +1,727 @@
|
||||
use errors::*;
|
||||
use memchr::{self, Memchr};
|
||||
use pulldown_cmark::{self, Event, Tag};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::iter::FromIterator;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be
|
||||
/// used when loading a book from disk.
|
||||
///
|
||||
/// # Summary Format
|
||||
///
|
||||
/// **Title:** It's common practice to begin with a title, generally
|
||||
/// "# Summary". It's not mandatory and the parser (currently) ignores it, so
|
||||
/// you can too if you feel like it.
|
||||
///
|
||||
/// **Prefix Chapter:** Before the main numbered chapters you can add a couple
|
||||
/// of elements that will not be numbered. This is useful for forewords,
|
||||
/// introductions, etc. There are however some constraints. You can not nest
|
||||
/// prefix chapters, they should all be on the root level. And you can not add
|
||||
/// prefix chapters once you have added numbered chapters.
|
||||
///
|
||||
/// ```markdown
|
||||
/// [Title of prefix element](relative/path/to/markdown.md)
|
||||
/// ```
|
||||
///
|
||||
/// **Numbered Chapter:** Numbered chapters are the main content of the book,
|
||||
/// they
|
||||
/// will be numbered and can be nested, resulting in a nice hierarchy (chapters,
|
||||
/// sub-chapters, etc.)
|
||||
///
|
||||
/// ```markdown
|
||||
/// - [Title of the Chapter](relative/path/to/markdown.md)
|
||||
/// ```
|
||||
///
|
||||
/// You can either use - or * to indicate a numbered chapter, the parser doesn't
|
||||
/// care but you'll probably want to stay consistent.
|
||||
///
|
||||
/// **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.
|
||||
pub fn parse_summary(summary: &str) -> Result<Summary> {
|
||||
let parser = SummaryParser::new(summary);
|
||||
parser.parse()
|
||||
}
|
||||
|
||||
/// The parsed `SUMMARY.md`, specifying how the book should be laid out.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Summary {
|
||||
/// An optional title for the `SUMMARY.md`, currently just ignored.
|
||||
pub title: Option<String>,
|
||||
/// Chapters before the main text (e.g. an introduction).
|
||||
pub prefix_chapters: Vec<SummaryItem>,
|
||||
/// The main chapters in the document.
|
||||
pub numbered_chapters: Vec<SummaryItem>,
|
||||
/// Items which come after the main document (e.g. a conclusion).
|
||||
pub suffix_chapters: Vec<SummaryItem>,
|
||||
}
|
||||
|
||||
/// A struct representing an entry in the `SUMMARY.md`, possibly with nested
|
||||
/// entries.
|
||||
///
|
||||
/// This is roughly the equivalent of `[Some section](./path/to/file.md)`.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Link {
|
||||
/// The name of the chapter.
|
||||
pub name: String,
|
||||
/// The location of the chapter's source file, taking the book's `src`
|
||||
/// directory as the root.
|
||||
pub location: PathBuf,
|
||||
/// The section number, if this chapter is in the numbered section.
|
||||
pub number: Option<SectionNumber>,
|
||||
/// Any nested items this chapter may contain.
|
||||
pub nested_items: Vec<SummaryItem>,
|
||||
}
|
||||
|
||||
impl Link {
|
||||
/// Create a new link with no nested items.
|
||||
pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link {
|
||||
Link {
|
||||
name: name.into(),
|
||||
location: location.as_ref().to_path_buf(),
|
||||
number: None,
|
||||
nested_items: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Link {
|
||||
fn default() -> Self {
|
||||
Link {
|
||||
name: String::new(),
|
||||
location: PathBuf::new(),
|
||||
number: None,
|
||||
nested_items: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An item in `SUMMARY.md` which could be either a separator or a `Link`.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SummaryItem {
|
||||
/// A link to a chapter.
|
||||
Link(Link),
|
||||
/// A separator (`---`).
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl SummaryItem {
|
||||
fn maybe_link_mut(&mut self) -> Option<&mut Link> {
|
||||
match *self {
|
||||
SummaryItem::Link(ref mut l) => Some(l),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Link> for SummaryItem {
|
||||
fn from(other: Link) -> SummaryItem {
|
||||
SummaryItem::Link(other)
|
||||
}
|
||||
}
|
||||
|
||||
/// A recursive descent (-ish) parser for a `SUMMARY.md`.
|
||||
///
|
||||
///
|
||||
/// # Grammar
|
||||
///
|
||||
/// The `SUMMARY.md` file has a grammar which looks something like this:
|
||||
///
|
||||
/// ```text
|
||||
/// summary ::= title prefix_chapters numbered_chapters
|
||||
/// suffix_chapters
|
||||
/// title ::= "# " TEXT
|
||||
/// | EPSILON
|
||||
/// prefix_chapters ::= item*
|
||||
/// suffix_chapters ::= item*
|
||||
/// numbered_chapters ::= dotted_item+
|
||||
/// dotted_item ::= INDENT* DOT_POINT item
|
||||
/// item ::= link
|
||||
/// | separator
|
||||
/// separator ::= "---"
|
||||
/// link ::= "[" TEXT "]" "(" TEXT ")"
|
||||
/// DOT_POINT ::= "-"
|
||||
/// | "*"
|
||||
/// ```
|
||||
///
|
||||
/// > **Note:** the `TEXT` terminal is "normal" text, and should (roughly)
|
||||
/// > match the following regex: "[^<>\n[]]+".
|
||||
struct SummaryParser<'a> {
|
||||
src: &'a str,
|
||||
stream: pulldown_cmark::Parser<'a>,
|
||||
}
|
||||
|
||||
/// Reads `Events` from the provided stream until the corresponding
|
||||
/// `Event::End` is encountered which matches the `$delimiter` pattern.
|
||||
///
|
||||
/// This is the equivalent of doing
|
||||
/// `$stream.take_while(|e| e != $delimeter).collect()` but it allows you to
|
||||
/// use pattern matching and you won't get errors because `take_while()`
|
||||
/// moves `$stream` out of self.
|
||||
macro_rules! collect_events {
|
||||
($stream:expr,start $delimiter:pat) => {
|
||||
collect_events!($stream, Event::Start($delimiter))
|
||||
};
|
||||
($stream:expr,end $delimiter:pat) => {
|
||||
collect_events!($stream, Event::End($delimiter))
|
||||
};
|
||||
($stream:expr, $delimiter:pat) => {{
|
||||
let mut events = Vec::new();
|
||||
|
||||
loop {
|
||||
let event = $stream.next();
|
||||
trace!("Next event: {:?}", event);
|
||||
|
||||
match event {
|
||||
Some($delimiter) => break,
|
||||
Some(other) => events.push(other),
|
||||
None => {
|
||||
debug!(
|
||||
"Reached end of stream without finding the closing pattern, {}",
|
||||
stringify!($delimiter)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
events
|
||||
}};
|
||||
}
|
||||
|
||||
impl<'a> SummaryParser<'a> {
|
||||
fn new(text: &str) -> SummaryParser {
|
||||
let pulldown_parser = pulldown_cmark::Parser::new(text);
|
||||
|
||||
SummaryParser {
|
||||
src: text,
|
||||
stream: pulldown_parser,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current line and column to give the user more useful error
|
||||
/// messages.
|
||||
fn current_location(&self) -> (usize, usize) {
|
||||
let byte_offset = self.stream.get_offset();
|
||||
|
||||
let previous_text = self.src[..byte_offset].as_bytes();
|
||||
let line = Memchr::new(b'\n', previous_text).count() + 1;
|
||||
let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0);
|
||||
let col = self.src[start_of_line..byte_offset].chars().count();
|
||||
|
||||
(line, col)
|
||||
}
|
||||
|
||||
/// Parse the text the `SummaryParser` was created with.
|
||||
fn parse(mut self) -> Result<Summary> {
|
||||
let title = self.parse_title();
|
||||
|
||||
let prefix_chapters = self
|
||||
.parse_affix(true)
|
||||
.chain_err(|| "There was an error parsing the prefix chapters")?;
|
||||
let numbered_chapters = self
|
||||
.parse_numbered()
|
||||
.chain_err(|| "There was an error parsing the numbered chapters")?;
|
||||
let suffix_chapters = self
|
||||
.parse_affix(false)
|
||||
.chain_err(|| "There was an error parsing the suffix chapters")?;
|
||||
|
||||
Ok(Summary {
|
||||
title,
|
||||
prefix_chapters,
|
||||
numbered_chapters,
|
||||
suffix_chapters,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the affix chapters. This expects the first event (start of
|
||||
/// paragraph) to have already been consumed by the previous parser.
|
||||
fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
|
||||
let mut items = Vec::new();
|
||||
debug!(
|
||||
"Parsing {} items",
|
||||
if is_prefix { "prefix" } else { "suffix" }
|
||||
);
|
||||
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::List(..))) => {
|
||||
if is_prefix {
|
||||
// we've finished prefix chapters and are at the start
|
||||
// of the numbered section.
|
||||
break;
|
||||
} else {
|
||||
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
|
||||
}
|
||||
}
|
||||
Some(Event::Start(Tag::Link(href, _))) => {
|
||||
let link = self.parse_link(href.to_string())?;
|
||||
items.push(SummaryItem::Link(link));
|
||||
}
|
||||
Some(Event::Start(Tag::Rule)) => items.push(SummaryItem::Separator),
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn parse_link(&mut self, href: String) -> Result<Link> {
|
||||
let link_content = collect_events!(self.stream, end Tag::Link(..));
|
||||
let name = stringify_events(link_content);
|
||||
|
||||
if href.is_empty() {
|
||||
Err(self.parse_error("You can't have an empty link."))
|
||||
} else {
|
||||
Ok(Link {
|
||||
name: name,
|
||||
location: PathBuf::from(href.to_string()),
|
||||
number: None,
|
||||
nested_items: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the numbered chapters. This assumes the opening list tag has
|
||||
/// already been consumed by a previous parser.
|
||||
fn parse_numbered(&mut self) -> Result<Vec<SummaryItem>> {
|
||||
let mut items = Vec::new();
|
||||
let root_number = SectionNumber::default();
|
||||
|
||||
// we need to do this funny loop-match-if-let dance because a rule will
|
||||
// close off any currently running list. Therefore we try to read the
|
||||
// list items before the rule, then if we encounter a rule we'll add a
|
||||
// separator and try to resume parsing numbered chapters if we start a
|
||||
// list immediately afterwards.
|
||||
//
|
||||
// If you can think of a better way to do this then please make a PR :)
|
||||
|
||||
loop {
|
||||
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?;
|
||||
|
||||
// if we've resumed after something like a rule the root sections
|
||||
// will be numbered from 1. We need to manually go back and update
|
||||
// them
|
||||
update_section_numbers(&mut bunch_of_items, 0, items.len() as u32);
|
||||
items.extend(bunch_of_items);
|
||||
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Paragraph)) => {
|
||||
// we're starting the suffix chapters
|
||||
break;
|
||||
}
|
||||
Some(Event::Start(other_tag)) => {
|
||||
if other_tag == Tag::Rule {
|
||||
items.push(SummaryItem::Separator);
|
||||
}
|
||||
trace!("Skipping contents of {:?}", other_tag);
|
||||
|
||||
// Skip over the contents of this tag
|
||||
while let Some(event) = self.next_event() {
|
||||
if event == Event::End(other_tag.clone()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Event::Start(Tag::List(..))) = self.next_event() {
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
// something else... ignore
|
||||
continue;
|
||||
}
|
||||
None => {
|
||||
// EOF, bail...
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn next_event(&mut self) -> Option<Event<'a>> {
|
||||
let next = self.stream.next();
|
||||
trace!("Next event: {:?}", next);
|
||||
|
||||
next
|
||||
}
|
||||
|
||||
fn parse_nested_numbered(&mut self, parent: &SectionNumber) -> Result<Vec<SummaryItem>> {
|
||||
debug!("Parsing numbered chapters at level {}", parent);
|
||||
let mut items = Vec::new();
|
||||
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Item)) => {
|
||||
let item = self.parse_nested_item(parent, items.len())?;
|
||||
items.push(item);
|
||||
}
|
||||
Some(Event::Start(Tag::List(..))) => {
|
||||
// recurse to parse the nested list
|
||||
let (_, last_item) = get_last_link(&mut items)?;
|
||||
let last_item_number = last_item
|
||||
.number
|
||||
.as_ref()
|
||||
.expect("All numbered chapters have numbers");
|
||||
|
||||
let sub_items = self.parse_nested_numbered(last_item_number)?;
|
||||
|
||||
last_item.nested_items = sub_items;
|
||||
}
|
||||
Some(Event::End(Tag::List(..))) => break,
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn parse_nested_item(
|
||||
&mut self,
|
||||
parent: &SectionNumber,
|
||||
num_existing_items: usize,
|
||||
) -> Result<SummaryItem> {
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Paragraph)) => continue,
|
||||
Some(Event::Start(Tag::Link(href, _))) => {
|
||||
let mut link = self.parse_link(href.to_string())?;
|
||||
|
||||
let mut number = parent.clone();
|
||||
number.0.push(num_existing_items as u32 + 1);
|
||||
trace!(
|
||||
"Found chapter: {} {} ({})",
|
||||
number,
|
||||
link.name,
|
||||
link.location.display()
|
||||
);
|
||||
|
||||
link.number = Some(number);
|
||||
|
||||
return Ok(SummaryItem::Link(link));
|
||||
}
|
||||
other => {
|
||||
warn!("Expected a start of a link, actually got {:?}", other);
|
||||
bail!(self.parse_error(
|
||||
"The link items for nested chapters must only contain a hyperlink"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_error<D: Display>(&self, msg: D) -> Error {
|
||||
let (line, col) = self.current_location();
|
||||
|
||||
ErrorKind::ParseError(line, col, msg.to_string()).into()
|
||||
}
|
||||
|
||||
/// Try to parse the title line.
|
||||
fn parse_title(&mut self) -> Option<String> {
|
||||
if let Some(Event::Start(Tag::Header(1))) = self.next_event() {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Header(1));
|
||||
Some(stringify_events(tags))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_section_numbers(sections: &mut [SummaryItem], level: usize, by: u32) {
|
||||
for section in sections {
|
||||
if let SummaryItem::Link(ref mut link) = *section {
|
||||
if let Some(ref mut number) = link.number {
|
||||
number.0[level] += by;
|
||||
}
|
||||
|
||||
update_section_numbers(&mut link.nested_items, level, by);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a pointer to the last `Link` in a list of `SummaryItem`s, and its
|
||||
/// index.
|
||||
fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
|
||||
links
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
|
||||
.rev()
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
"Unable to get last link because the list of SummaryItems doesn't contain any Links"
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes the styling from a list of Markdown events and returns just the
|
||||
/// plain text.
|
||||
fn stringify_events(events: Vec<Event>) -> String {
|
||||
events
|
||||
.into_iter()
|
||||
.filter_map(|t| match t {
|
||||
Event::Text(text) => Some(text.into_owned()),
|
||||
_ => None,
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
|
||||
/// a pretty `Display` impl.
|
||||
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SectionNumber(pub Vec<u32>);
|
||||
|
||||
impl Display for SectionNumber {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
if self.0.is_empty() {
|
||||
write!(f, "0")
|
||||
} else {
|
||||
for item in &self.0 {
|
||||
write!(f, "{}.", item)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SectionNumber {
|
||||
type Target = Vec<u32>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for SectionNumber {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<u32> for SectionNumber {
|
||||
fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self {
|
||||
SectionNumber(it.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn section_number_has_correct_dotted_representation() {
|
||||
let inputs = vec![
|
||||
(vec![0], "0."),
|
||||
(vec![1, 3], "1.3."),
|
||||
(vec![1, 2, 3], "1.2.3."),
|
||||
];
|
||||
|
||||
for (input, should_be) in inputs {
|
||||
let section_number = SectionNumber(input).to_string();
|
||||
assert_eq!(section_number, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_initial_title() {
|
||||
let src = "# Summary";
|
||||
let should_be = String::from("Summary");
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser.parse_title().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_title_with_styling() {
|
||||
let src = "# My **Awesome** Summary";
|
||||
let should_be = String::from("My Awesome Summary");
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser.parse_title().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_markdown_events_to_a_string() {
|
||||
let src = "Hello *World*, `this` is some text [and a link](./path/to/link)";
|
||||
let should_be = "Hello World, this is some text and a link";
|
||||
|
||||
let events = pulldown_cmark::Parser::new(src).collect();
|
||||
let got = stringify_events(events);
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_some_prefix_items() {
|
||||
let src = "[First](./first.md)\n[Second](./second.md)\n";
|
||||
let mut parser = SummaryParser::new(src);
|
||||
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
..Default::default()
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: PathBuf::from("./second.md"),
|
||||
..Default::default()
|
||||
}),
|
||||
];
|
||||
|
||||
let _ = parser.stream.next(); // step past first event
|
||||
let got = parser.parse_affix(true).unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_prefix_items_with_a_separator() {
|
||||
let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n";
|
||||
let mut parser = SummaryParser::new(src);
|
||||
|
||||
let _ = parser.stream.next(); // step past first event
|
||||
let got = parser.parse_affix(true).unwrap();
|
||||
|
||||
assert_eq!(got.len(), 3);
|
||||
assert_eq!(got[1], SummaryItem::Separator);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suffix_items_cannot_be_followed_by_a_list() {
|
||||
let src = "[First](./first.md)\n- [Second](./second.md)\n";
|
||||
let mut parser = SummaryParser::new(src);
|
||||
|
||||
let _ = parser.stream.next(); // step past first event
|
||||
let got = parser.parse_affix(false);
|
||||
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_a_link() {
|
||||
let src = "[First](./first.md)";
|
||||
let should_be = Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let _ = parser.stream.next(); // skip past start of paragraph
|
||||
|
||||
let href = match parser.stream.next() {
|
||||
Some(Event::Start(Tag::Link(href, _))) => href.to_string(),
|
||||
other => panic!("Unreachable, {:?}", other),
|
||||
};
|
||||
|
||||
let got = parser.parse_link(href).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_a_numbered_chapter() {
|
||||
let src = "- [First](./first.md)\n";
|
||||
let link = Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
..Default::default()
|
||||
};
|
||||
let should_be = vec![SummaryItem::Link(link)];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let _ = parser.stream.next();
|
||||
|
||||
let got = parser.parse_numbered().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_nested_numbered_chapters() {
|
||||
let src = "- [First](./first.md)\n - [Nested](./nested.md)\n- [Second](./second.md)";
|
||||
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: vec![SummaryItem::Link(Link {
|
||||
name: String::from("Nested"),
|
||||
location: PathBuf::from("./nested.md"),
|
||||
number: Some(SectionNumber(vec![1, 1])),
|
||||
nested_items: Vec::new(),
|
||||
})],
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: PathBuf::from("./second.md"),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let _ = parser.stream.next();
|
||||
|
||||
let got = parser.parse_numbered().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
/// This test ensures the book will continue to pass because it breaks the
|
||||
/// `SUMMARY.md` up using level 2 headers ([example]).
|
||||
///
|
||||
/// [example]: https://github.com/rust-lang/book/blob/2c942dc094f4ddcdc7aba7564f80782801197c99/second-edition/src/SUMMARY.md#basic-rust-literacy
|
||||
#[test]
|
||||
fn can_have_a_subheader_between_nested_items() {
|
||||
let src = "- [First](./first.md)\n\n## Subheading\n\n- [Second](./second.md)\n";
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: PathBuf::from("./second.md"),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let _ = parser.stream.next();
|
||||
|
||||
let got = parser.parse_numbered().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_link_location_is_an_error() {
|
||||
let src = "- [Empty]()\n";
|
||||
let mut parser = SummaryParser::new(src);
|
||||
parser.stream.next();
|
||||
|
||||
let got = parser.parse_numbered();
|
||||
assert!(got.is_err());
|
||||
}
|
||||
}
|
||||
36
src/cmd/build.rs
Normal file
36
src/cmd/build.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
use {get_book_dir, open};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("build")
|
||||
.about("Builds a book from its markdown files")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
).arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
).arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
|
||||
}
|
||||
|
||||
// Build command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
|
||||
book.build()?;
|
||||
|
||||
if args.is_present("open") {
|
||||
// FIXME: What's the right behaviour if we don't use the HTML renderer?
|
||||
open(book.build_dir_for("html").join("index.html"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
32
src/cmd/clean.rs
Normal file
32
src/cmd/clean.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::MDBook;
|
||||
use std::fs;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("clean")
|
||||
.about("Deletes a built book")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
).arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
}
|
||||
|
||||
// Clean command implementation
|
||||
pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::load(&book_dir)?;
|
||||
|
||||
let dir_to_remove = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => dest_dir.into(),
|
||||
None => book.root.join(&book.config.build.build_dir),
|
||||
};
|
||||
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
107
src/cmd/init.rs
Normal file
107
src/cmd/init.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::config;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("init")
|
||||
.about("Creates the boilerplate structure and files for a new book")
|
||||
// the {n} denotes a newline which will properly aligned in all help messages
|
||||
.arg_from_usage("[dir] 'Directory to create the book in{n}\
|
||||
(Defaults to the Current Directory when omitted)'")
|
||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
||||
.arg_from_usage("--force 'Skips confirmation prompts'")
|
||||
}
|
||||
|
||||
// Init command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut builder = MDBook::init(&book_dir);
|
||||
let mut config = config::Config::default();
|
||||
|
||||
// If flag `--theme` is present, copy theme to src
|
||||
if args.is_present("theme") {
|
||||
config.set("output.html.theme", "src/theme")?;
|
||||
// Skip this if `--force` is present
|
||||
if !args.is_present("force") {
|
||||
// Print warning
|
||||
println!();
|
||||
println!(
|
||||
"Copying the default theme to {}",
|
||||
builder.config().book.src.display()
|
||||
);
|
||||
println!("This 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() {
|
||||
builder.copy_theme(true);
|
||||
}
|
||||
} else {
|
||||
builder.copy_theme(true);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||
|
||||
if confirm() {
|
||||
builder.create_gitignore(true);
|
||||
}
|
||||
|
||||
config.book.title = request_book_title();
|
||||
|
||||
if let Some(author) = get_author_name() {
|
||||
debug!("Obtained user name from gitconfig: {:?}", author);
|
||||
config.book.authors.push(author);
|
||||
builder.with_config(config);
|
||||
}
|
||||
|
||||
builder.build()?;
|
||||
println!("\nAll done, no errors...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Obtains author name from git config file by running the `git config` command.
|
||||
fn get_author_name() -> Option<String> {
|
||||
let output = Command::new("git")
|
||||
.args(&["config", "--get", "user.name"])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if output.status.success() {
|
||||
Some(String::from_utf8_lossy(&output.stdout).trim().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Request book title from user and return if provided.
|
||||
fn request_book_title() -> Option<String> {
|
||||
println!("What title would you like to give the book? ");
|
||||
io::stdout().flush().unwrap();
|
||||
let mut resp = String::new();
|
||||
io::stdin().read_line(&mut resp).unwrap();
|
||||
let resp = resp.trim();
|
||||
if resp.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(resp.into())
|
||||
}
|
||||
}
|
||||
|
||||
// Simple function for user confirmation
|
||||
fn confirm() -> bool {
|
||||
io::stdout().flush().unwrap();
|
||||
let mut s = String::new();
|
||||
io::stdin().read_line(&mut s).ok();
|
||||
match &*s.trim() {
|
||||
"Y" | "y" | "yes" | "Yes" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
10
src/cmd/mod.rs
Normal file
10
src/cmd/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! Subcommand modules for the `mdbook` binary.
|
||||
|
||||
pub mod build;
|
||||
pub mod clean;
|
||||
pub mod init;
|
||||
#[cfg(feature = "serve")]
|
||||
pub mod serve;
|
||||
pub mod test;
|
||||
#[cfg(feature = "watch")]
|
||||
pub mod watch;
|
||||
149
src/cmd/serve.rs
Normal file
149
src/cmd/serve.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
extern crate iron;
|
||||
extern crate staticfile;
|
||||
extern crate ws;
|
||||
|
||||
use self::iron::{
|
||||
status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set,
|
||||
};
|
||||
#[cfg(feature = "watch")]
|
||||
use super::watch;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
use std;
|
||||
use {get_book_dir, open};
|
||||
|
||||
struct ErrorRecover;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("serve")
|
||||
.about("Serves a book at http://localhost:3000, and rebuilds it on changes")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("hostname")
|
||||
.short("n")
|
||||
.long("hostname")
|
||||
.takes_value(true)
|
||||
.default_value("localhost")
|
||||
.empty_values(false)
|
||||
.help("Hostname to listen on for HTTP connections"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("port")
|
||||
.short("p")
|
||||
.long("port")
|
||||
.takes_value(true)
|
||||
.default_value("3000")
|
||||
.empty_values(false)
|
||||
.help("Port to use for HTTP connections"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("websocket-hostname")
|
||||
.long("websocket-hostname")
|
||||
.takes_value(true)
|
||||
.empty_values(false)
|
||||
.help(
|
||||
"Hostname to connect to for WebSockets connections (Defaults to the HTTP hostname)",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("websocket-port")
|
||||
.short("w")
|
||||
.long("websocket-port")
|
||||
.takes_value(true)
|
||||
.default_value("3001")
|
||||
.empty_values(false)
|
||||
.help("Port to use for WebSockets livereload connections"),
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Opens the book server in a web browser'")
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
let port = args.value_of("port").unwrap();
|
||||
let ws_port = args.value_of("websocket-port").unwrap();
|
||||
let hostname = args.value_of("hostname").unwrap();
|
||||
let public_address = args.value_of("websocket-address").unwrap_or(hostname);
|
||||
let open_browser = args.is_present("open");
|
||||
|
||||
let address = format!("{}:{}", hostname, port);
|
||||
let ws_address = format!("{}:{}", hostname, ws_port);
|
||||
|
||||
let livereload_url = format!("ws://{}:{}", public_address, ws_port);
|
||||
book.config
|
||||
.set("output.html.livereload-url", &livereload_url)?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
|
||||
book.build()?;
|
||||
|
||||
let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html")));
|
||||
chain.link_after(ErrorRecover);
|
||||
let _iron = Iron::new(chain)
|
||||
.http(&*address)
|
||||
.chain_err(|| "Unable to launch the server")?;
|
||||
|
||||
let ws_server =
|
||||
ws::WebSocket::new(|_| |_| Ok(())).chain_err(|| "Unable to start the websocket")?;
|
||||
|
||||
let broadcaster = ws_server.broadcaster();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
ws_server.listen(&*ws_address).unwrap();
|
||||
});
|
||||
|
||||
let serving_url = format!("http://{}", address);
|
||||
info!("Serving on: {}", serving_url);
|
||||
|
||||
if open_browser {
|
||||
open(serving_url);
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
watch::trigger_on_change(&mut book, move |path, book_dir| {
|
||||
info!("File changed: {:?}", path);
|
||||
info!("Building book...");
|
||||
|
||||
// FIXME: This area is really ugly because we need to re-set livereload :(
|
||||
|
||||
let result = MDBook::load(&book_dir)
|
||||
.and_then(|mut b| {
|
||||
b.config
|
||||
.set("output.html.livereload-url", &livereload_url)?;
|
||||
Ok(b)
|
||||
}).and_then(|b| b.build());
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to load the book");
|
||||
utils::log_backtrace(&e);
|
||||
} else {
|
||||
let _ = broadcaster.send("reload");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl AfterMiddleware for ErrorRecover {
|
||||
fn catch(&self, _: &mut Request, err: IronError) -> IronResult<Response> {
|
||||
match err.response.status {
|
||||
// each error will result in 404 response
|
||||
Some(_) => Ok(err.response.set(status::NotFound)),
|
||||
_ => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/cmd/test.rs
Normal file
45
src/cmd/test.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("test")
|
||||
.about("Tests that a book's Rust code samples compile")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg(Arg::with_name("library-path")
|
||||
.short("L")
|
||||
.long("library-path")
|
||||
.value_name("dir")
|
||||
.takes_value(true)
|
||||
.require_delimiter(true)
|
||||
.multiple(true)
|
||||
.empty_values(false)
|
||||
.help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
|
||||
}
|
||||
|
||||
// test command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let library_paths: Vec<&str> = args
|
||||
.values_of("library-path")
|
||||
.map(|v| v.collect())
|
||||
.unwrap_or_default();
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
|
||||
book.test(library_paths)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
90
src/cmd/watch.rs
Normal file
90
src/cmd/watch.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
extern crate notify;
|
||||
|
||||
use self::notify::Watcher;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::time::Duration;
|
||||
use {get_book_dir, open};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("watch")
|
||||
.about("Watches a book's files and rebuilds it on changes")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
).arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
).arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::load(&book_dir)?;
|
||||
|
||||
if args.is_present("open") {
|
||||
book.build()?;
|
||||
open(book.build_dir_for("html").join("index.html"));
|
||||
}
|
||||
|
||||
trigger_on_change(&book, |path, book_dir| {
|
||||
info!("File changed: {:?}\nBuilding book...\n", path);
|
||||
let result = MDBook::load(&book_dir).and_then(|b| b.build());
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to build the book");
|
||||
utils::log_backtrace(&e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calls the closure when a book source file is changed, blocking indefinitely.
|
||||
pub fn trigger_on_change<F>(book: &MDBook, closure: F)
|
||||
where
|
||||
F: Fn(&Path, &Path),
|
||||
{
|
||||
use self::notify::DebouncedEvent::*;
|
||||
use self::notify::RecursiveMode::*;
|
||||
|
||||
// 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) => {
|
||||
error!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
::std::process::exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.source_dir(), Recursive) {
|
||||
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
|
||||
::std::process::exit(1);
|
||||
};
|
||||
|
||||
let _ = watcher.watch(book.theme_dir(), Recursive);
|
||||
|
||||
// Add the book.toml file to the watcher if it exists
|
||||
let _ = watcher.watch(book.root.join("book.toml"), NonRecursive);
|
||||
|
||||
info!("Listening for changes...");
|
||||
|
||||
for event in rx.iter() {
|
||||
debug!("Received filesystem event: {:?}", event);
|
||||
match event {
|
||||
Create(path) | Write(path) | Remove(path) | Rename(_, path) => {
|
||||
closure(&path, &book.root);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
804
src/config.rs
Normal file
804
src/config.rs
Normal file
@@ -0,0 +1,804 @@
|
||||
//! Mdbook's configuration system.
|
||||
//!
|
||||
//! The main entrypoint of the `config` module is the `Config` struct. This acts
|
||||
//! essentially as a bag of configuration information, with a couple
|
||||
//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support
|
||||
//! for arbitrary data which is exposed to plugins and alternate backends.
|
||||
//!
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! # extern crate mdbook;
|
||||
//! # use mdbook::errors::*;
|
||||
//! # extern crate toml;
|
||||
//! use std::path::PathBuf;
|
||||
//! use mdbook::Config;
|
||||
//! use toml::Value;
|
||||
//!
|
||||
//! # fn run() -> Result<()> {
|
||||
//! let src = r#"
|
||||
//! [book]
|
||||
//! title = "My Book"
|
||||
//! authors = ["Michael-F-Bryan"]
|
||||
//!
|
||||
//! [build]
|
||||
//! src = "out"
|
||||
//!
|
||||
//! [other-table.foo]
|
||||
//! bar = 123
|
||||
//! "#;
|
||||
//!
|
||||
//! // load the `Config` from a toml string
|
||||
//! let mut cfg = Config::from_str(src)?;
|
||||
//!
|
||||
//! // retrieve a nested value
|
||||
//! let bar = cfg.get("other-table.foo.bar").cloned();
|
||||
//! assert_eq!(bar, Some(Value::Integer(123)));
|
||||
//!
|
||||
//! // Set the `output.html.theme` directory
|
||||
//! assert!(cfg.get("output.html").is_none());
|
||||
//! cfg.set("output.html.theme", "./themes");
|
||||
//!
|
||||
//! // then load it again, automatically deserializing to a `PathBuf`.
|
||||
//! let got: PathBuf = cfg.get_deserialized("output.html.theme")?;
|
||||
//! assert_eq!(got, PathBuf::from("./themes"));
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # fn main() { run().unwrap() }
|
||||
//! ```
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use toml::value::Table;
|
||||
use toml::{self, Value};
|
||||
use toml_query::delete::TomlValueDeleteExt;
|
||||
use toml_query::insert::TomlValueInsertExt;
|
||||
use toml_query::read::TomlValueReadExt;
|
||||
|
||||
use errors::*;
|
||||
|
||||
/// The overall configuration object for MDBook, essentially an in-memory
|
||||
/// representation of `book.toml`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
/// Metadata about the book.
|
||||
pub book: BookConfig,
|
||||
/// Information about the build environment.
|
||||
pub build: BuildConfig,
|
||||
rest: Value,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load a `Config` from some string.
|
||||
pub fn from_str(src: &str) -> Result<Config> {
|
||||
toml::from_str(src).chain_err(|| Error::from("Invalid configuration file"))
|
||||
}
|
||||
|
||||
/// Load the configuration file from disk.
|
||||
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
|
||||
let mut buffer = String::new();
|
||||
File::open(config_file)
|
||||
.chain_err(|| "Unable to open the configuration file")?
|
||||
.read_to_string(&mut buffer)
|
||||
.chain_err(|| "Couldn't read the file")?;
|
||||
|
||||
Config::from_str(&buffer)
|
||||
}
|
||||
|
||||
/// Updates the `Config` from the available environment variables.
|
||||
///
|
||||
/// Variables starting with `MDBOOK_` are used for configuration. The key is
|
||||
/// created by removing the `MDBOOK_` prefix and turning the resulting
|
||||
/// string into `kebab-case`. Double underscores (`__`) separate nested
|
||||
/// keys, while a single underscore (`_`) is replaced with a dash (`-`).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// - `MDBOOK_foo` -> `foo`
|
||||
/// - `MDBOOK_FOO` -> `foo`
|
||||
/// - `MDBOOK_FOO__BAR` -> `foo.bar`
|
||||
/// - `MDBOOK_FOO_BAR` -> `foo-bar`
|
||||
/// - `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
|
||||
///
|
||||
/// So by setting the `MDBOOK_BOOK__TITLE` environment variable you can
|
||||
/// override the book's title without needing to touch your `book.toml`.
|
||||
///
|
||||
/// > **Note:** To facilitate setting more complex config items, the value
|
||||
/// > of an environment variable is first parsed as JSON, falling back to a
|
||||
/// > string if the parse fails.
|
||||
/// >
|
||||
/// > This means, if you so desired, you could override all book metadata
|
||||
/// > when building the book with something like
|
||||
/// >
|
||||
/// > ```text
|
||||
/// > $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
|
||||
/// > $ mdbook build
|
||||
/// > ```
|
||||
///
|
||||
/// The latter case may be useful in situations where `mdbook` is invoked
|
||||
/// from a script or CI, where it sometimes isn't possible to update the
|
||||
/// `book.toml` before building.
|
||||
pub fn update_from_env(&mut self) {
|
||||
debug!("Updating the config from environment variables");
|
||||
|
||||
let overrides = env::vars().filter_map(|(key, value)| match parse_env(&key) {
|
||||
Some(index) => Some((index, value)),
|
||||
None => None,
|
||||
});
|
||||
|
||||
for (key, value) in overrides {
|
||||
trace!("{} => {}", key, value);
|
||||
let parsed_value = serde_json::from_str(&value)
|
||||
.unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
|
||||
|
||||
self.set(key, parsed_value).expect("unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch an arbitrary item from the `Config` as a `toml::Value`.
|
||||
///
|
||||
/// You can use dotted indices to access nested items (e.g.
|
||||
/// `output.html.playpen` will fetch the "playpen" out of the html output
|
||||
/// table).
|
||||
pub fn get(&self, key: &str) -> Option<&Value> {
|
||||
match self.rest.read(key) {
|
||||
Ok(inner) => inner,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a value from the `Config` so you can mutate it.
|
||||
pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Value> {
|
||||
match self.rest.read_mut(key) {
|
||||
Ok(inner) => inner,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience method for getting the html renderer's configuration.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is for compatibility only. It will be removed completely once the
|
||||
/// HTML renderer is refactored to be less coupled to `mdbook` internals.
|
||||
#[doc(hidden)]
|
||||
pub fn html_config(&self) -> Option<HtmlConfig> {
|
||||
self.get_deserialized("output.html").ok()
|
||||
}
|
||||
|
||||
/// Convenience function to fetch a value from the config and deserialize it
|
||||
/// into some arbitrary type.
|
||||
pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
|
||||
let name = name.as_ref();
|
||||
|
||||
if let Some(value) = self.get(name) {
|
||||
value
|
||||
.clone()
|
||||
.try_into()
|
||||
.chain_err(|| "Couldn't deserialize the value")
|
||||
} else {
|
||||
bail!("Key not found, {:?}", name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a config key, clobbering any existing values along the way.
|
||||
///
|
||||
/// The only way this can fail is if we can't serialize `value` into a
|
||||
/// `toml::Value`.
|
||||
pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
|
||||
let index = index.as_ref();
|
||||
|
||||
let value =
|
||||
Value::try_from(value).chain_err(|| "Unable to represent the item as a JSON Value")?;
|
||||
|
||||
if index.starts_with("book.") {
|
||||
self.book.update_value(&index[5..], value);
|
||||
} else if index.starts_with("build.") {
|
||||
self.build.update_value(&index[6..], value);
|
||||
} else {
|
||||
self.rest.insert(index, value)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the table associated with a particular renderer.
|
||||
pub fn get_renderer<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
|
||||
let key = format!("output.{}", index.as_ref());
|
||||
self.get(&key).and_then(|v| v.as_table())
|
||||
}
|
||||
|
||||
/// Get the table associated with a particular preprocessor.
|
||||
pub fn get_preprocessor<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
|
||||
let key = format!("preprocessor.{}", index.as_ref());
|
||||
self.get(&key).and_then(|v| v.as_table())
|
||||
}
|
||||
|
||||
fn from_legacy(mut table: Value) -> Config {
|
||||
let mut cfg = Config::default();
|
||||
|
||||
// we use a macro here instead of a normal loop because the $out
|
||||
// variable can be different types. This way we can make type inference
|
||||
// figure out what try_into() deserializes to.
|
||||
macro_rules! get_and_insert {
|
||||
($table:expr, $key:expr => $out:expr) => {
|
||||
let got = $table
|
||||
.as_table_mut()
|
||||
.and_then(|t| t.remove($key))
|
||||
.and_then(|v| v.try_into().ok());
|
||||
if let Some(value) = got {
|
||||
$out = value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get_and_insert!(table, "title" => cfg.book.title);
|
||||
get_and_insert!(table, "authors" => cfg.book.authors);
|
||||
get_and_insert!(table, "source" => cfg.book.src);
|
||||
get_and_insert!(table, "description" => cfg.book.description);
|
||||
|
||||
if let Ok(Some(dest)) = table.delete("output.html.destination") {
|
||||
if let Ok(destination) = dest.try_into() {
|
||||
cfg.build.build_dir = destination;
|
||||
}
|
||||
}
|
||||
|
||||
cfg.rest = table;
|
||||
cfg
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
book: BookConfig::default(),
|
||||
build: BuildConfig::default(),
|
||||
rest: Value::Table(Table::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for Config {
|
||||
fn deserialize<D: Deserializer<'de>>(de: D) -> ::std::result::Result<Self, D::Error> {
|
||||
let raw = Value::deserialize(de)?;
|
||||
|
||||
if is_legacy_format(&raw) {
|
||||
warn!("It looks like you are using the legacy book.toml format.");
|
||||
warn!("We'll parse it for now, but you should probably convert to the new format.");
|
||||
warn!("See the mdbook documentation for more details, although as a rule of thumb");
|
||||
warn!("just move all top level configuration entries like `title`, `author` and");
|
||||
warn!("`description` under a table called `[book]`, move the `destination` entry");
|
||||
warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
|
||||
warn!("`[build]`, and it should all work.");
|
||||
warn!("Documentation: http://rust-lang-nursery.github.io/mdBook/format/config.html");
|
||||
return Ok(Config::from_legacy(raw));
|
||||
}
|
||||
|
||||
let mut table = match raw {
|
||||
Value::Table(t) => t,
|
||||
_ => {
|
||||
use serde::de::Error;
|
||||
return Err(D::Error::custom(
|
||||
"A config file should always be a toml table",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let book: BookConfig = table
|
||||
.remove("book")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let build: BuildConfig = table
|
||||
.remove("build")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Config {
|
||||
book: book,
|
||||
build: build,
|
||||
rest: Value::Table(table),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Config {
|
||||
fn serialize<S: Serializer>(&self, s: S) -> ::std::result::Result<S::Ok, S::Error> {
|
||||
use serde::ser::Error;
|
||||
|
||||
let mut table = self.rest.clone();
|
||||
|
||||
let book_config = match Value::try_from(self.book.clone()) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(_) => {
|
||||
return Err(S::Error::custom("Unable to serialize the BookConfig"));
|
||||
}
|
||||
};
|
||||
|
||||
table.insert("book", book_config).expect("unreachable");
|
||||
table.serialize(s)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_env(key: &str) -> Option<String> {
|
||||
const PREFIX: &str = "MDBOOK_";
|
||||
|
||||
if key.starts_with(PREFIX) {
|
||||
let key = &key[PREFIX.len()..];
|
||||
|
||||
Some(key.to_lowercase().replace("__", ".").replace("_", "-"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_legacy_format(table: &Value) -> bool {
|
||||
let legacy_items = [
|
||||
"title",
|
||||
"authors",
|
||||
"source",
|
||||
"description",
|
||||
"output.html.destination",
|
||||
];
|
||||
|
||||
for item in &legacy_items {
|
||||
if let Ok(Some(_)) = table.read(item) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Configuration options which are specific to the book and required for
|
||||
/// loading it from disk.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct BookConfig {
|
||||
/// The book's title.
|
||||
pub title: Option<String>,
|
||||
/// The book's authors.
|
||||
pub authors: Vec<String>,
|
||||
/// An optional description for the book.
|
||||
pub description: Option<String>,
|
||||
/// Location of the book source relative to the book's root directory.
|
||||
pub src: PathBuf,
|
||||
/// Does this book support more than one language?
|
||||
pub multilingual: bool,
|
||||
}
|
||||
|
||||
impl Default for BookConfig {
|
||||
fn default() -> BookConfig {
|
||||
BookConfig {
|
||||
title: None,
|
||||
authors: Vec::new(),
|
||||
description: None,
|
||||
src: PathBuf::from("src"),
|
||||
multilingual: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the build procedure.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct BuildConfig {
|
||||
/// Where to put built artefacts relative to the book's root directory.
|
||||
pub build_dir: PathBuf,
|
||||
/// Should non-existent markdown files specified in `SETTINGS.md` be created
|
||||
/// if they don't exist?
|
||||
pub create_missing: bool,
|
||||
/// Should the default preprocessors always be used when they are
|
||||
/// compatible with the renderer?
|
||||
pub use_default_preprocessors: bool,
|
||||
}
|
||||
|
||||
impl Default for BuildConfig {
|
||||
fn default() -> BuildConfig {
|
||||
BuildConfig {
|
||||
build_dir: PathBuf::from("book"),
|
||||
create_missing: true,
|
||||
use_default_preprocessors: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the HTML renderer.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct HtmlConfig {
|
||||
/// The theme directory, if specified.
|
||||
pub theme: Option<PathBuf>,
|
||||
/// Use "smart quotes" instead of the usual `"` character.
|
||||
pub curly_quotes: bool,
|
||||
/// Should mathjax be enabled?
|
||||
pub mathjax_support: bool,
|
||||
/// An optional google analytics code.
|
||||
pub google_analytics: Option<String>,
|
||||
/// Additional CSS stylesheets to include in the rendered page's `<head>`.
|
||||
pub additional_css: Vec<PathBuf>,
|
||||
/// Additional JS scripts to include at the bottom of the rendered page's
|
||||
/// `<body>`.
|
||||
pub additional_js: Vec<PathBuf>,
|
||||
/// Playpen settings.
|
||||
pub playpen: Playpen,
|
||||
/// This is used as a bit of a workaround for the `mdbook serve` command.
|
||||
/// Basically, because you set the websocket port from the command line, the
|
||||
/// `mdbook serve` command needs a way to let the HTML renderer know where
|
||||
/// to point livereloading at, if it has been enabled.
|
||||
///
|
||||
/// This config item *should not be edited* by the end user.
|
||||
#[doc(hidden)]
|
||||
pub livereload_url: Option<String>,
|
||||
/// Should section labels be rendered?
|
||||
pub no_section_label: bool,
|
||||
/// Search settings. If `None`, the default will be used.
|
||||
pub search: Option<Search>,
|
||||
}
|
||||
|
||||
impl HtmlConfig {
|
||||
/// Returns the directory of theme from the provided root directory. If the
|
||||
/// directory is not present it will append the default directory of "theme"
|
||||
pub fn theme_dir(&self, root: &PathBuf) -> PathBuf {
|
||||
match self.theme {
|
||||
Some(ref d) => root.join(d),
|
||||
None => root.join("theme"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the the HTML renderer handles the playpen.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Playpen {
|
||||
/// Should playpen snippets be editable? Default: `false`.
|
||||
pub editable: bool,
|
||||
/// Copy JavaScript files for the editor to the output directory?
|
||||
/// Default: `true`.
|
||||
pub copy_js: bool,
|
||||
}
|
||||
|
||||
impl Default for Playpen {
|
||||
fn default() -> Playpen {
|
||||
Playpen {
|
||||
editable: false,
|
||||
copy_js: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration of the search functionality of the HTML renderer.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Search {
|
||||
/// Enable the search feature. Default: `true`.
|
||||
pub enable: bool,
|
||||
/// Maximum number of visible results. Default: `30`.
|
||||
pub limit_results: u32,
|
||||
/// The number of words used for a search result teaser. Default: `30`.
|
||||
pub teaser_word_count: u32,
|
||||
/// Define the logical link between multiple search words.
|
||||
/// If true, all search words must appear in each result. Default: `true`.
|
||||
pub use_boolean_and: bool,
|
||||
/// Boost factor for the search result score if a search word appears in the header.
|
||||
/// Default: `2`.
|
||||
pub boost_title: u8,
|
||||
/// Boost factor for the search result score if a search word appears in the hierarchy.
|
||||
/// The hierarchy contains all titles of the parent documents and all parent headings.
|
||||
/// Default: `1`.
|
||||
pub boost_hierarchy: u8,
|
||||
/// Boost factor for the search result score if a search word appears in the text.
|
||||
/// Default: `1`.
|
||||
pub boost_paragraph: u8,
|
||||
/// True if the searchword `micro` should match `microwave`. Default: `true`.
|
||||
pub expand: bool,
|
||||
/// Documents are split into smaller parts, seperated by headings. This defines, until which
|
||||
/// level of heading documents should be split. Default: `3`. (`### This is a level 3 heading`)
|
||||
pub heading_split_level: u8,
|
||||
/// Copy JavaScript files for the search functionality to the output directory?
|
||||
/// Default: `true`.
|
||||
pub copy_js: bool,
|
||||
}
|
||||
|
||||
impl Default for Search {
|
||||
fn default() -> Search {
|
||||
// Please update the documentation of `Search` when changing values!
|
||||
Search {
|
||||
enable: true,
|
||||
limit_results: 30,
|
||||
teaser_word_count: 30,
|
||||
use_boolean_and: false,
|
||||
boost_title: 2,
|
||||
boost_hierarchy: 1,
|
||||
boost_paragraph: 1,
|
||||
expand: true,
|
||||
heading_split_level: 3,
|
||||
copy_js: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows you to "update" any arbitrary field in a struct by round-tripping via
|
||||
/// a `toml::Value`.
|
||||
///
|
||||
/// This is definitely not the most performant way to do things, which means you
|
||||
/// should probably keep it away from tight loops...
|
||||
trait Updateable<'de>: Serialize + Deserialize<'de> {
|
||||
fn update_value<S: Serialize>(&mut self, key: &str, value: S) {
|
||||
let mut raw = Value::try_from(&self).expect("unreachable");
|
||||
|
||||
{
|
||||
if let Ok(value) = Value::try_from(value) {
|
||||
let _ = raw.insert(key, value);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(updated) = raw.try_into() {
|
||||
*self = updated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const COMPLEX_CONFIG: &'static str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
|
||||
description = "A completely useless book"
|
||||
multilingual = true
|
||||
src = "source"
|
||||
|
||||
[build]
|
||||
build-dir = "outputs"
|
||||
create-missing = false
|
||||
use-default-preprocessors = true
|
||||
|
||||
[output.html]
|
||||
theme = "./themedir"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["./foo/bar/baz.css"]
|
||||
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
editor = "ace"
|
||||
|
||||
[preprocess.first]
|
||||
|
||||
[preprocess.second]
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn load_a_complex_config_file() {
|
||||
let src = COMPLEX_CONFIG;
|
||||
|
||||
let book_should_be = BookConfig {
|
||||
title: Some(String::from("Some Book")),
|
||||
authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
|
||||
description: Some(String::from("A completely useless book")),
|
||||
multilingual: true,
|
||||
src: PathBuf::from("source"),
|
||||
..Default::default()
|
||||
};
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("outputs"),
|
||||
create_missing: false,
|
||||
use_default_preprocessors: true,
|
||||
};
|
||||
let playpen_should_be = Playpen {
|
||||
editable: true,
|
||||
copy_js: true,
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
|
||||
theme: Some(PathBuf::from("./themedir")),
|
||||
playpen: playpen_should_be,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
|
||||
assert_eq!(got.book, book_should_be);
|
||||
assert_eq!(got.build, build_should_be);
|
||||
assert_eq!(got.html_config().unwrap(), html_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_arbitrary_output_type() {
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
struct RandomOutput {
|
||||
foo: u32,
|
||||
bar: String,
|
||||
baz: Vec<bool>,
|
||||
}
|
||||
|
||||
let src = r#"
|
||||
[output.random]
|
||||
foo = 5
|
||||
bar = "Hello World"
|
||||
baz = [true, true, false]
|
||||
"#;
|
||||
|
||||
let should_be = RandomOutput {
|
||||
foo: 5,
|
||||
bar: String::from("Hello World"),
|
||||
baz: vec![true, true, false],
|
||||
};
|
||||
|
||||
let cfg = Config::from_str(src).unwrap();
|
||||
let got: RandomOutput = cfg.get_deserialized("output.random").unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
let baz: Vec<bool> = cfg.get_deserialized("output.random.baz").unwrap();
|
||||
let baz_should_be = vec![true, true, false];
|
||||
|
||||
assert_eq!(baz, baz_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutate_some_stuff() {
|
||||
// really this is just a sanity check to make sure the borrow checker
|
||||
// is happy...
|
||||
let src = COMPLEX_CONFIG;
|
||||
let mut config = Config::from_str(src).unwrap();
|
||||
let key = "output.html.playpen.editable";
|
||||
|
||||
assert_eq!(config.get(key).unwrap(), &Value::Boolean(true));
|
||||
*config.get_mut(key).unwrap() = Value::Boolean(false);
|
||||
assert_eq!(config.get(key).unwrap(), &Value::Boolean(false));
|
||||
}
|
||||
|
||||
/// The config file format has slightly changed (metadata stuff is now under
|
||||
/// the `book` table instead of being at the top level) so we're adding a
|
||||
/// **temporary** compatibility check. You should be able to still load the
|
||||
/// old format, emitting a warning.
|
||||
#[test]
|
||||
fn can_still_load_the_previous_format() {
|
||||
let src = r#"
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
source = "./source"
|
||||
|
||||
[output.html]
|
||||
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
|
||||
theme = "my-theme"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
"#;
|
||||
|
||||
let book_should_be = BookConfig {
|
||||
title: Some(String::from("mdBook Documentation")),
|
||||
description: Some(String::from(
|
||||
"Create book from markdown files. Like Gitbook but implemented in Rust",
|
||||
)),
|
||||
authors: vec![String::from("Mathieu David")],
|
||||
src: PathBuf::from("./source"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("my-book"),
|
||||
create_missing: true,
|
||||
use_default_preprocessors: true,
|
||||
};
|
||||
|
||||
let html_should_be = HtmlConfig {
|
||||
theme: Some(PathBuf::from("my-theme")),
|
||||
curly_quotes: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")],
|
||||
additional_js: vec![PathBuf::from("custom.js")],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.book, book_should_be);
|
||||
assert_eq!(got.build, build_should_be);
|
||||
assert_eq!(got.html_config().unwrap(), html_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_a_config_item() {
|
||||
let mut cfg = Config::default();
|
||||
let key = "foo.bar.baz";
|
||||
let value = "Something Interesting";
|
||||
|
||||
assert!(cfg.get(key).is_none());
|
||||
cfg.set(key, value).unwrap();
|
||||
|
||||
let got: String = cfg.get_deserialized(key).unwrap();
|
||||
assert_eq!(got, value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_env_vars() {
|
||||
let inputs = vec![
|
||||
("FOO", None),
|
||||
("MDBOOK_foo", Some("foo")),
|
||||
("MDBOOK_FOO__bar__baz", Some("foo.bar.baz")),
|
||||
("MDBOOK_FOO_bar__baz", Some("foo-bar.baz")),
|
||||
];
|
||||
|
||||
for (src, should_be) in inputs {
|
||||
let got = parse_env(src);
|
||||
let should_be = should_be.map(|s| s.to_string());
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_env_var(key: &str) -> String {
|
||||
format!(
|
||||
"MDBOOK_{}",
|
||||
key.to_uppercase().replace('.', "__").replace("-", "_")
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_config_using_env_var() {
|
||||
let mut cfg = Config::default();
|
||||
let key = "foo.bar";
|
||||
let value = "baz";
|
||||
|
||||
assert!(cfg.get(key).is_none());
|
||||
|
||||
let encoded_key = encode_env_var(key);
|
||||
env::set_var(encoded_key, value);
|
||||
|
||||
cfg.update_from_env();
|
||||
|
||||
assert_eq!(cfg.get_deserialized::<String, _>(key).unwrap(), value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_config_using_env_var_and_complex_value() {
|
||||
let mut cfg = Config::default();
|
||||
let key = "foo-bar.baz";
|
||||
let value = json!({"array": [1, 2, 3], "number": 3.14});
|
||||
let value_str = serde_json::to_string(&value).unwrap();
|
||||
|
||||
assert!(cfg.get(key).is_none());
|
||||
|
||||
let encoded_key = encode_env_var(key);
|
||||
env::set_var(encoded_key, value_str);
|
||||
|
||||
cfg.update_from_env();
|
||||
|
||||
assert_eq!(
|
||||
cfg.get_deserialized::<serde_json::Value, _>(key).unwrap(),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_book_title_via_env() {
|
||||
let mut cfg = Config::default();
|
||||
let should_be = "Something else".to_string();
|
||||
|
||||
assert_ne!(cfg.book.title, Some(should_be.clone()));
|
||||
|
||||
env::set_var("MDBOOK_BOOK__TITLE", &should_be);
|
||||
cfg.update_from_env();
|
||||
|
||||
assert_eq!(cfg.book.title, Some(should_be));
|
||||
}
|
||||
}
|
||||
183
src/lib.rs
183
src/lib.rs
@@ -1,83 +1,172 @@
|
||||
//! # mdBook
|
||||
//!
|
||||
//! **mdBook** is similar to Gitbook but implemented in Rust.
|
||||
//! It offers a command line interface, but can also be used as a regular crate.
|
||||
//! **mdBook** is a tool for rendering a collection of markdown documents into
|
||||
//! a form more suitable for end users like HTML or EPUB. It offers a command
|
||||
//! line interface, but this crate can be used if more control is required.
|
||||
//!
|
||||
//! This is the API doc, but you can find a [less "low-level" documentation here](../index.html) that
|
||||
//! contains information about the command line tool, format, structure etc.
|
||||
//! It is also rendered with mdBook to showcase the features and default theme.
|
||||
//! This is the API doc, the [user guide] is also available if you want
|
||||
//! information about the command line tool, format, structure etc. It is also
|
||||
//! rendered with mdBook to showcase the features and default theme.
|
||||
//!
|
||||
//! Some reasons why you would want to use the crate (over the cli):
|
||||
//!
|
||||
//! - Integrate mdbook in a current project
|
||||
//! - Extend the capabilities of mdBook
|
||||
//! - Do some processing or test before building your book
|
||||
//! - Write a new Renderer
|
||||
//! - Accessing the public API to help create a new Renderer
|
||||
//! - ...
|
||||
//!
|
||||
//! ## Example
|
||||
//! > **Note:** While we try to ensure `mdbook`'s command-line interface and
|
||||
//! > behaviour are backwards compatible, the tool's internals are still
|
||||
//! > evolving and being iterated on. If you wish to prevent accidental
|
||||
//! > breakages it is recommended to pin any tools building on top of the
|
||||
//! > `mdbook` crate to a specific release.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! extern crate mdbook;
|
||||
//! # Examples
|
||||
//!
|
||||
//! If creating a new book from scratch, you'll want to get a `BookBuilder` via
|
||||
//! the `MDBook::init()` method.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use mdbook::MDBook;
|
||||
//! use std::path::Path;
|
||||
//! use mdbook::config::Config;
|
||||
//!
|
||||
//! 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 root_dir = "/path/to/book/root";
|
||||
//!
|
||||
//! book.build().unwrap(); // Render the book
|
||||
//! }
|
||||
//! // create a default config and change a couple things
|
||||
//! let mut cfg = Config::default();
|
||||
//! cfg.book.title = Some("My Book".to_string());
|
||||
//! cfg.book.authors.push("Michael-F-Bryan".to_string());
|
||||
//!
|
||||
//! MDBook::init(root_dir)
|
||||
//! .create_gitignore(true)
|
||||
//! .with_config(cfg)
|
||||
//! .build()
|
||||
//! .expect("Book generation failed");
|
||||
//! ```
|
||||
//!
|
||||
//! ## Implementing a new Renderer
|
||||
//! You can also load an existing book and build it.
|
||||
//!
|
||||
//! If you want to create a new renderer for mdBook, the only thing you have to do is to implement
|
||||
//! the [Renderer trait](renderer/renderer/trait.Renderer.html)
|
||||
//! ```rust,no_run
|
||||
//! use mdbook::MDBook;
|
||||
//!
|
||||
//! And then you can swap in your renderer like this:
|
||||
//! let root_dir = "/path/to/book/root";
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # extern crate mdbook;
|
||||
//! #
|
||||
//! # use mdbook::MDBook;
|
||||
//! # use mdbook::renderer::HtmlHandlebars;
|
||||
//! # use std::path::Path;
|
||||
//! #
|
||||
//! # fn main() {
|
||||
//! # let your_renderer = HtmlHandlebars::new();
|
||||
//! #
|
||||
//! let book = MDBook::new(Path::new("my-book")).set_renderer(Box::new(your_renderer));
|
||||
//! # }
|
||||
//! let mut md = MDBook::load(root_dir)
|
||||
//! .expect("Unable to load the book");
|
||||
//! md.build().expect("Building failed");
|
||||
//! ```
|
||||
//! If you make a renderer, you get the book constructed in form of `Vec<BookItems>` and you get
|
||||
//! the book config in a `BookConfig` struct.
|
||||
//!
|
||||
//! It's your responsability to create the necessary files in the correct directories.
|
||||
//! ## Implementing a new Backend
|
||||
//!
|
||||
//! ## utils
|
||||
//! `mdbook` has a fairly flexible mechanism for creating additional backends
|
||||
//! for your book. The general idea is you'll add an extra table in the book's
|
||||
//! `book.toml` which specifies an executable to be invoked by `mdbook`. This
|
||||
//! executable will then be called during a build, with an in-memory
|
||||
//! representation ([`RenderContext`]) of the book being passed to the
|
||||
//! subprocess via `stdin`.
|
||||
//!
|
||||
//! I have regrouped some useful functions in the [utils](utils/index.html) module, like the following function
|
||||
//! The [`RenderContext`] gives the backend access to the contents of
|
||||
//! `book.toml` and lets it know which directory all generated artefacts should
|
||||
//! be placed in. For a much more in-depth explanation, consult the [relevant
|
||||
//! chapter] in the *For Developers* section of the user guide.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! utils::create_path(path: &Path)
|
||||
//! ```
|
||||
//! This function creates all the directories in a given path if they do not exist
|
||||
//! To make creating a backend easier, the `mdbook` crate can be imported
|
||||
//! directly, making deserializing the `RenderContext` easy and giving you
|
||||
//! access to the various methods for working with the [`Config`].
|
||||
//!
|
||||
//! Make sure to take a look at it.
|
||||
//! [user guide]: https://rust-lang-nursery.github.io/mdBook/
|
||||
//! [`RenderContext`]: renderer/struct.RenderContext.html
|
||||
//! [relevant chapter]: https://rust-lang-nursery.github.io/mdBook/for_developers/backends.html
|
||||
//! [`Config`]: config/struct.Config.html
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
extern crate error_chain;
|
||||
extern crate handlebars;
|
||||
extern crate itertools;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate memchr;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate regex;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
extern crate shlex;
|
||||
extern crate tempfile;
|
||||
extern crate toml;
|
||||
extern crate toml_query;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate pretty_assertions;
|
||||
|
||||
pub mod book;
|
||||
mod parse;
|
||||
pub mod config;
|
||||
pub mod preprocess;
|
||||
pub mod renderer;
|
||||
pub mod theme;
|
||||
pub mod utils;
|
||||
|
||||
pub use book::MDBook;
|
||||
/// The current version of `mdbook`.
|
||||
///
|
||||
/// This is provided as a way for custom preprocessors and renderers to do
|
||||
/// compatibility checks.
|
||||
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub use book::BookItem;
|
||||
pub use book::BookConfig;
|
||||
pub use book::MDBook;
|
||||
pub use config::Config;
|
||||
pub use renderer::Renderer;
|
||||
|
||||
/// The error types used through out this crate.
|
||||
pub mod errors {
|
||||
use std::path::PathBuf;
|
||||
|
||||
error_chain!{
|
||||
foreign_links {
|
||||
Io(::std::io::Error) #[doc = "A wrapper around `std::io::Error`"];
|
||||
HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"];
|
||||
HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"];
|
||||
Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
|
||||
SerdeJson(::serde_json::Error) #[doc = "JSON conversion failed"];
|
||||
}
|
||||
|
||||
links {
|
||||
TomlQuery(::toml_query::error::Error, ::toml_query::error::ErrorKind) #[doc = "A TomlQuery error"];
|
||||
}
|
||||
|
||||
errors {
|
||||
/// A subprocess exited with an unsuccessful return code.
|
||||
Subprocess(message: String, output: ::std::process::Output) {
|
||||
description("A subprocess failed")
|
||||
display("{}: {}", message, String::from_utf8_lossy(&output.stdout))
|
||||
}
|
||||
|
||||
/// An error was encountered while parsing the `SUMMARY.md` file.
|
||||
ParseError(line: usize, col: usize, message: String) {
|
||||
description("A SUMMARY.md parsing error")
|
||||
display("Error at line {}, column {}: {}", line, col, message)
|
||||
}
|
||||
|
||||
/// The user tried to use a reserved filename.
|
||||
ReservedFilenameError(filename: PathBuf) {
|
||||
description("Reserved Filename")
|
||||
display("{} is reserved for internal use", filename.display())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Box to halve the size of Error
|
||||
impl From<::handlebars::TemplateError> for Error {
|
||||
fn from(e: ::handlebars::TemplateError) -> Error {
|
||||
From::from(Box::new(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,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)*) => ();
|
||||
}
|
||||
114
src/main.rs
Normal file
114
src/main.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
extern crate chrono;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate env_logger;
|
||||
extern crate error_chain;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate mdbook;
|
||||
extern crate open;
|
||||
|
||||
use chrono::Local;
|
||||
use clap::{App, AppSettings, ArgMatches};
|
||||
use env_logger::Builder;
|
||||
use log::LevelFilter;
|
||||
use mdbook::utils;
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
mod cmd;
|
||||
|
||||
const NAME: &'static str = "mdBook";
|
||||
const VERSION: &'static str = concat!("v", crate_version!());
|
||||
|
||||
fn main() {
|
||||
init_logger();
|
||||
|
||||
// Create a list of valid arguments and sub-commands
|
||||
let app = App::new(NAME)
|
||||
.about("Creates a book from markdown files")
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
.version(VERSION)
|
||||
.setting(AppSettings::GlobalVersion)
|
||||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.after_help(
|
||||
"For more information about a specific command, try `mdbook <command> --help`\n\
|
||||
The source code for mdBook is available at: https://github.com/rust-lang-nursery/mdBook",
|
||||
)
|
||||
.subcommand(cmd::init::make_subcommand())
|
||||
.subcommand(cmd::build::make_subcommand())
|
||||
.subcommand(cmd::test::make_subcommand())
|
||||
.subcommand(cmd::clean::make_subcommand());
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
let app = app.subcommand(cmd::watch::make_subcommand());
|
||||
#[cfg(feature = "serve")]
|
||||
let app = app.subcommand(cmd::serve::make_subcommand());
|
||||
|
||||
// Check which subcomamnd the user ran...
|
||||
let res = match app.get_matches().subcommand() {
|
||||
("init", Some(sub_matches)) => cmd::init::execute(sub_matches),
|
||||
("build", Some(sub_matches)) => cmd::build::execute(sub_matches),
|
||||
("clean", Some(sub_matches)) => cmd::clean::execute(sub_matches),
|
||||
#[cfg(feature = "watch")]
|
||||
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches),
|
||||
#[cfg(feature = "serve")]
|
||||
("serve", Some(sub_matches)) => cmd::serve::execute(sub_matches),
|
||||
("test", Some(sub_matches)) => cmd::test::execute(sub_matches),
|
||||
(_, _) => unreachable!(),
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
utils::log_backtrace(&e);
|
||||
|
||||
::std::process::exit(101);
|
||||
}
|
||||
}
|
||||
|
||||
fn init_logger() {
|
||||
let mut builder = Builder::new();
|
||||
|
||||
builder.format(|formatter, record| {
|
||||
writeln!(
|
||||
formatter,
|
||||
"{} [{}] ({}): {}",
|
||||
Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
record.level(),
|
||||
record.target(),
|
||||
record.args()
|
||||
)
|
||||
});
|
||||
|
||||
if let Ok(var) = env::var("RUST_LOG") {
|
||||
builder.parse(&var);
|
||||
} else {
|
||||
// if no RUST_LOG provided, default to logging at the Info level
|
||||
builder.filter(None, LevelFilter::Info);
|
||||
// Filter extraneous html5ever not-implemented messages
|
||||
builder.filter(Some("html5ever"), LevelFilter::Error);
|
||||
}
|
||||
|
||||
builder.init();
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
p.to_path_buf()
|
||||
}
|
||||
} else {
|
||||
env::current_dir().expect("Unable to determine the current directory")
|
||||
}
|
||||
}
|
||||
|
||||
fn open<P: AsRef<OsStr>>(path: P) {
|
||||
if let Err(e) = open::that(path) {
|
||||
error!("Error opening web browser: {}", e);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub use self::summary::construct_bookitems;
|
||||
|
||||
pub mod summary;
|
||||
@@ -1,216 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Result, Error, ErrorKind};
|
||||
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));
|
||||
|
||||
debug!("[*]: Parse SUMMARY.md");
|
||||
let top_items = try!(parse_level(&mut summary.split('\n').collect(), 0, vec![0]));
|
||||
debug!("[*]: Done parsing SUMMARY.md");
|
||||
Ok(top_items)
|
||||
}
|
||||
|
||||
fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32>) -> Result<Vec<BookItem>> {
|
||||
debug!("[fn]: parse_level");
|
||||
let mut items: Vec<BookItem> = vec![];
|
||||
|
||||
// Construct the book recursively
|
||||
while !summary.is_empty() {
|
||||
let item: BookItem;
|
||||
// Indentation level of the line to parse
|
||||
let level = try!(level(summary[0], 4));
|
||||
|
||||
// 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 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");
|
||||
|
||||
item = if let BookItem::Chapter(ref s, ref ch) = last {
|
||||
let mut ch = ch.clone();
|
||||
ch.sub_items = try!(parse_level(summary, level, section.clone()));
|
||||
items.push(BookItem::Chapter(s.clone(), ch));
|
||||
|
||||
// Remove the last number from the section, because we got back to our level..
|
||||
section.pop();
|
||||
continue
|
||||
} else {
|
||||
return Err(Error::new( ErrorKind::Other, 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."
|
||||
)))
|
||||
};
|
||||
|
||||
} else {
|
||||
// level and current_level are the same, parse the line
|
||||
item = if let Some(parsed_item) = parse_line(summary[0]) {
|
||||
|
||||
// Eliminate possible errors and set section to -1 after suffix
|
||||
match parsed_item {
|
||||
// error if level != 0 and BookItem is != Chapter
|
||||
BookItem::Affix(_) | BookItem::Spacer if level > 0 => {
|
||||
return Err(Error::new( ErrorKind::Other, 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."
|
||||
)))
|
||||
},
|
||||
|
||||
// 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."
|
||||
)))
|
||||
},
|
||||
|
||||
// Set section = -1 after suffix
|
||||
BookItem::Affix(_) if section[0] > 0 => {
|
||||
section[0] = -1;
|
||||
}
|
||||
|
||||
_ => {},
|
||||
}
|
||||
|
||||
match parsed_item {
|
||||
BookItem::Chapter(_, ch) => {
|
||||
// Increment section
|
||||
let len = section.len() -1;
|
||||
section[len] += 1;
|
||||
let s = section.iter().fold("".to_owned(), |s, i| s + &i.to_string() + ".");
|
||||
BookItem::Chapter(s, ch)
|
||||
}
|
||||
_ => parsed_item
|
||||
}
|
||||
|
||||
} else {
|
||||
// If parse_line does not return Some(_) continue...
|
||||
summary.remove(0);
|
||||
continue;
|
||||
};
|
||||
}
|
||||
|
||||
summary.remove(0);
|
||||
items.push(item)
|
||||
}
|
||||
debug!("[*]: Level: {:?}", items);
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
|
||||
fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
|
||||
debug!("[fn]: level");
|
||||
let mut spaces = 0;
|
||||
let mut level = 0;
|
||||
|
||||
for ch in line.chars() {
|
||||
match ch {
|
||||
' ' => spaces += 1,
|
||||
'\t' => level += 1,
|
||||
_ => break,
|
||||
}
|
||||
if spaces >= spaces_in_tab {
|
||||
level += 1;
|
||||
spaces = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are spaces left, there is an indentation error
|
||||
if spaces > 0 {
|
||||
debug!("[SUMMARY.md]:");
|
||||
debug!("\t[line]: {}", line);
|
||||
debug!("[*]: There is an indentation error on this line. Indentation should be {} spaces", spaces_in_tab);
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Indentation error on line:\n\n{}", line)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Ok(level)
|
||||
}
|
||||
|
||||
|
||||
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' });
|
||||
|
||||
// Spacers are "------"
|
||||
if line.starts_with("--") {
|
||||
debug!("[*]: Line is spacer");
|
||||
return Some(BookItem::Spacer)
|
||||
}
|
||||
|
||||
if let Some(c) = line.chars().nth(0) {
|
||||
match c {
|
||||
// List item
|
||||
'-' | '*' => {
|
||||
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 }
|
||||
},
|
||||
// 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 }
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn read_link(line: &str) -> Option<(String, PathBuf)> {
|
||||
let mut start_delimitor;
|
||||
let mut end_delimitor;
|
||||
|
||||
// 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 {
|
||||
debug!("[*]: '[' not found, this line is not a link. Ignoring...");
|
||||
return None
|
||||
}
|
||||
|
||||
if let Some(i) = line[start_delimitor..].find("](") {
|
||||
end_delimitor = start_delimitor +i;
|
||||
}
|
||||
else {
|
||||
debug!("[*]: '](' not found, this line is not a link. Ignoring...");
|
||||
return None
|
||||
}
|
||||
|
||||
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 {
|
||||
debug!("[*]: ')' not found, this line is not a link. Ignoring...");
|
||||
return None
|
||||
}
|
||||
|
||||
let path = PathBuf::from(line[start_delimitor + 1 .. end_delimitor].to_owned());
|
||||
|
||||
Some((name, path))
|
||||
}
|
||||
99
src/preprocess/index.rs
Normal file
99
src/preprocess/index.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use regex::Regex;
|
||||
use std::path::Path;
|
||||
|
||||
use errors::*;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use book::{Book, BookItem};
|
||||
|
||||
/// A preprocessor for converting file name `README.md` to `index.md` since
|
||||
/// `README.md` is the de facto index file in a markdown-based documentation.
|
||||
pub struct IndexPreprocessor;
|
||||
|
||||
impl IndexPreprocessor {
|
||||
pub(crate) const NAME: &'static str = "index";
|
||||
|
||||
/// Create a new `IndexPreprocessor`.
|
||||
pub fn new() -> Self {
|
||||
IndexPreprocessor
|
||||
}
|
||||
}
|
||||
|
||||
impl Preprocessor for IndexPreprocessor {
|
||||
fn name(&self) -> &str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
||||
let source_dir = ctx.root.join(&ctx.config.book.src);
|
||||
book.for_each_mut(|section: &mut BookItem| {
|
||||
if let BookItem::Chapter(ref mut ch) = *section {
|
||||
if is_readme_file(&ch.path) {
|
||||
let index_md = source_dir.join(ch.path.with_file_name("index.md"));
|
||||
if index_md.exists() {
|
||||
warn_readme_name_conflict(&ch.path, &index_md);
|
||||
}
|
||||
|
||||
ch.path.set_file_name("index.md");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
|
||||
let file_name = readme_path.as_ref().file_name().unwrap_or_default();
|
||||
let parent_dir = index_path.as_ref().parent().unwrap_or(index_path.as_ref());
|
||||
warn!(
|
||||
"It seems that there are both {:?} and index.md under \"{}\".",
|
||||
file_name,
|
||||
parent_dir.display()
|
||||
);
|
||||
warn!(
|
||||
"mdbook converts {:?} into index.html by default. It may cause",
|
||||
file_name
|
||||
);
|
||||
warn!("unexpected behavior if putting both files under the same directory.");
|
||||
warn!("To solve the warning, try to rearrange the book structure or disable");
|
||||
warn!("\"index\" preprocessor to stop the conversion.");
|
||||
}
|
||||
|
||||
fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"(?i)^readme$").unwrap();
|
||||
}
|
||||
RE.is_match(
|
||||
path.as_ref()
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn file_stem_exactly_matches_readme_case_insensitively() {
|
||||
let path = "path/to/Readme.md";
|
||||
assert!(is_readme_file(path));
|
||||
|
||||
let path = "path/to/README.md";
|
||||
assert!(is_readme_file(path));
|
||||
|
||||
let path = "path/to/rEaDmE.md";
|
||||
assert!(is_readme_file(path));
|
||||
|
||||
let path = "path/to/README.markdown";
|
||||
assert!(is_readme_file(path));
|
||||
|
||||
let path = "path/to/README";
|
||||
assert!(is_readme_file(path));
|
||||
|
||||
let path = "path/to/README-README.md";
|
||||
assert!(!is_readme_file(path));
|
||||
}
|
||||
}
|
||||
525
src/preprocess/links.rs
Normal file
525
src/preprocess/links.rs
Normal file
@@ -0,0 +1,525 @@
|
||||
use errors::*;
|
||||
use regex::{CaptureMatches, Captures, Regex};
|
||||
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
|
||||
use std::path::{Path, PathBuf};
|
||||
use utils::fs::file_to_string;
|
||||
use utils::take_lines;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use book::{Book, BookItem};
|
||||
|
||||
const ESCAPE_CHAR: char = '\\';
|
||||
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
|
||||
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
|
||||
/// helpers in a chapter.
|
||||
pub struct LinkPreprocessor;
|
||||
|
||||
impl LinkPreprocessor {
|
||||
pub(crate) const NAME: &'static str = "links";
|
||||
|
||||
/// Create a new `LinkPreprocessor`.
|
||||
pub fn new() -> Self {
|
||||
LinkPreprocessor
|
||||
}
|
||||
}
|
||||
|
||||
impl Preprocessor for LinkPreprocessor {
|
||||
fn name(&self) -> &str {
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
||||
let src_dir = ctx.root.join(&ctx.config.book.src);
|
||||
|
||||
book.for_each_mut(|section: &mut BookItem| {
|
||||
if let BookItem::Chapter(ref mut ch) = *section {
|
||||
let base = ch
|
||||
.path
|
||||
.parent()
|
||||
.map(|dir| src_dir.join(dir))
|
||||
.expect("All book items have a parent");
|
||||
|
||||
let content = replace_all(&ch.content, base, &ch.path, 0);
|
||||
ch.content = content;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_all<P1, P2>(s: &str, path: P1, source: P2, depth: usize) -> String
|
||||
where
|
||||
P1: AsRef<Path>,
|
||||
P2: AsRef<Path>,
|
||||
{
|
||||
// When replacing one thing in a string by something with a different length,
|
||||
// the indices after that will not correspond,
|
||||
// we therefore have to store the difference to correct this
|
||||
let path = path.as_ref();
|
||||
let source = source.as_ref();
|
||||
let mut previous_end_index = 0;
|
||||
let mut replaced = String::new();
|
||||
|
||||
for playpen in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..playpen.start_index]);
|
||||
|
||||
match playpen.render_with_path(&path) {
|
||||
Ok(new_content) => {
|
||||
if depth < MAX_LINK_NESTED_DEPTH {
|
||||
if let Some(rel_path) = playpen.link.relative_path(path) {
|
||||
replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1));
|
||||
} else {
|
||||
replaced.push_str(&new_content);
|
||||
}
|
||||
} else {
|
||||
error!(
|
||||
"Stack depth exceeded in {}. Check for cyclic includes",
|
||||
source.display()
|
||||
);
|
||||
}
|
||||
previous_end_index = playpen.end_index;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error updating \"{}\", {}", playpen.link_text, e);
|
||||
// This should make sure we include the raw `{{# ... }}` snippet
|
||||
// in the page content if there are any errors.
|
||||
previous_end_index = playpen.start_index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replaced.push_str(&s[previous_end_index..]);
|
||||
replaced
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum LinkType<'a> {
|
||||
Escaped,
|
||||
IncludeRange(PathBuf, Range<usize>),
|
||||
IncludeRangeFrom(PathBuf, RangeFrom<usize>),
|
||||
IncludeRangeTo(PathBuf, RangeTo<usize>),
|
||||
IncludeRangeFull(PathBuf, RangeFull),
|
||||
Playpen(PathBuf, Vec<&'a str>),
|
||||
}
|
||||
|
||||
impl<'a> LinkType<'a> {
|
||||
fn relative_path<P: AsRef<Path>>(self, base: P) -> Option<PathBuf> {
|
||||
let base = base.as_ref();
|
||||
match self {
|
||||
LinkType::Escaped => None,
|
||||
LinkType::IncludeRange(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
|
||||
base.as_ref()
|
||||
.join(relative)
|
||||
.parent()
|
||||
.expect("Included file should not be /")
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
fn parse_include_path(path: &str) -> LinkType<'static> {
|
||||
let mut parts = path.split(':');
|
||||
let path = parts.next().unwrap().into();
|
||||
// subtract 1 since line numbers usually begin with 1
|
||||
let start = parts
|
||||
.next()
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.map(|val| val.saturating_sub(1));
|
||||
let end = parts.next();
|
||||
let has_end = end.is_some();
|
||||
let end = end.and_then(|s| s.parse::<usize>().ok());
|
||||
match start {
|
||||
Some(start) => match end {
|
||||
Some(end) => LinkType::IncludeRange(
|
||||
path,
|
||||
Range {
|
||||
start: start,
|
||||
end: end,
|
||||
},
|
||||
),
|
||||
None => if has_end {
|
||||
LinkType::IncludeRangeFrom(path, RangeFrom { start: start })
|
||||
} else {
|
||||
LinkType::IncludeRange(
|
||||
path,
|
||||
Range {
|
||||
start: start,
|
||||
end: start + 1,
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
None => match end {
|
||||
Some(end) => LinkType::IncludeRangeTo(path, RangeTo { end: end }),
|
||||
None => LinkType::IncludeRangeFull(path, RangeFull),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(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_arg = path_props.next();
|
||||
let props: Vec<&str> = path_props.collect();
|
||||
|
||||
match (typ.as_str(), file_arg) {
|
||||
("include", Some(pth)) => Some(parse_include_path(pth)),
|
||||
("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), 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::IncludeRange(ref pat, ref range) => file_to_string(base.join(pat))
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRangeFrom(ref pat, ref range) => file_to_string(base.join(pat))
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRangeTo(ref pat, ref range) => file_to_string(base.join(pat))
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRangeFull(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))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_replace_all_escaped() {
|
||||
let start = r"
|
||||
Some text over here.
|
||||
```hbs
|
||||
\{{#include file.rs}} << an escaped link!
|
||||
```";
|
||||
let end = r"
|
||||
Some text over here.
|
||||
```hbs
|
||||
{{#include file.rs}} << an escaped link!
|
||||
```";
|
||||
assert_eq!(replace_all(start, "", "", 0), end);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_no_link() {
|
||||
let s = "Some random text without link...";
|
||||
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_with_range() {
|
||||
let s = "Some random text with {{#include file.rs:10:20}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 48,
|
||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
|
||||
link_text: "{{#include file.rs:10:20}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_with_line_number() {
|
||||
let s = "Some random text with {{#include file.rs:10}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 45,
|
||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
|
||||
link_text: "{{#include file.rs:10}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_with_from_range() {
|
||||
let s = "Some random text with {{#include file.rs:10:}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
|
||||
link_text: "{{#include file.rs:10:}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_with_to_range() {
|
||||
let s = "Some random text with {{#include file.rs::20}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
|
||||
link_text: "{{#include file.rs::20}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_with_full_range() {
|
||||
let s = "Some random text with {{#include file.rs::}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 44,
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_text: "{{#include file.rs::}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_with_no_range_specified() {
|
||||
let s = "Some random text with {{#include file.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::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_text: "{{#include file.rs}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_escaped_link() {
|
||||
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: LinkType::Escaped,
|
||||
link_text: "\\{{#playpen file.rs editable}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_with_properties() {
|
||||
let s = "Some random text with escaped playpen {{#playpen file.rs editable }} and some \
|
||||
more\n text {{#playpen my.rs editable no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
|
||||
link_text: "{{#playpen file.rs editable }}",
|
||||
},
|
||||
Link {
|
||||
start_index: 89,
|
||||
end_index: 136,
|
||||
link: LinkType::Playpen(
|
||||
PathBuf::from("my.rs"),
|
||||
vec!["editable", "no_run", "should_panic"],
|
||||
),
|
||||
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_all_link_types() {
|
||||
let s = "Some random text with escaped playpen {{#include file.rs}} and \\{{#contents are \
|
||||
insignifficant in escaped link}} some more\n text {{#playpen my.rs editable \
|
||||
no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(res.len(), 3);
|
||||
assert_eq!(
|
||||
res[0],
|
||||
Link {
|
||||
start_index: 38,
|
||||
end_index: 58,
|
||||
link: LinkType::IncludeRangeFull(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}}",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
59
src/preprocess/mod.rs
Normal file
59
src/preprocess/mod.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! Book preprocessing.
|
||||
|
||||
pub use self::index::IndexPreprocessor;
|
||||
pub use self::links::LinkPreprocessor;
|
||||
|
||||
mod index;
|
||||
mod links;
|
||||
|
||||
use book::Book;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Extra information for a `Preprocessor` to give them more context when
|
||||
/// processing a book.
|
||||
pub struct PreprocessorContext {
|
||||
/// The location of the book directory on disk.
|
||||
pub root: PathBuf,
|
||||
/// The book configuration (`book.toml`).
|
||||
pub config: Config,
|
||||
/// The `Renderer` this preprocessor is being used with.
|
||||
pub renderer: String,
|
||||
/// The calling `mdbook` version.
|
||||
pub mdbook_version: String,
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
impl PreprocessorContext {
|
||||
/// Create a new `PreprocessorContext`.
|
||||
pub(crate) fn new(root: PathBuf, config: Config, renderer: String) -> Self {
|
||||
PreprocessorContext {
|
||||
root,
|
||||
config,
|
||||
renderer,
|
||||
mdbook_version: ::MDBOOK_VERSION.to_string(),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An operation which is run immediately after loading a book into memory and
|
||||
/// before it gets rendered.
|
||||
pub trait Preprocessor {
|
||||
/// Get the `Preprocessor`'s name.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Run this `Preprocessor`, allowing it to update the book before it is
|
||||
/// given to a renderer.
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>;
|
||||
|
||||
/// A hint to `MDBook` whether this preprocessor is compatible with a
|
||||
/// particular renderer.
|
||||
///
|
||||
/// By default, always returns `true`.
|
||||
fn supports_renderer(&self, _renderer: &str) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -1,248 +1,458 @@
|
||||
extern crate handlebars;
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use book::{Book, BookItem};
|
||||
use config::{Config, HtmlConfig, Playpen};
|
||||
use errors::*;
|
||||
use renderer::html_handlebars::helpers;
|
||||
use renderer::Renderer;
|
||||
use book::MDBook;
|
||||
use book::bookitem::BookItem;
|
||||
use {utils, theme};
|
||||
use renderer::{RenderContext, Renderer};
|
||||
use theme::{self, playpen_editor, Theme};
|
||||
use utils;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{self, File};
|
||||
use std::error::Error;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use self::handlebars::{Handlebars, JsonRender};
|
||||
use self::rustc_serialize::json::{Json, ToJson};
|
||||
|
||||
use handlebars::Handlebars;
|
||||
use regex::{Captures, Regex};
|
||||
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) => {
|
||||
let content = ch.content.clone();
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
print_content.push_str(&content);
|
||||
|
||||
// Load theme
|
||||
let theme = theme::Theme::new(book.get_src());
|
||||
// Update the context with data for this file
|
||||
let path = ch
|
||||
.path
|
||||
.to_str()
|
||||
.chain_err(|| "Could not convert path to str")?;
|
||||
let filepath = Path::new(&ch.path).with_extension("html");
|
||||
|
||||
// Register template
|
||||
debug!("[*]: Register handlebars template");
|
||||
try!(handlebars.register_template_string("index", try!(String::from_utf8(theme.index))));
|
||||
// "print.html" is used for the print page.
|
||||
if ch.path == Path::new("print.md") {
|
||||
bail!(ErrorKind::ReservedFilenameError(ch.path.clone()));
|
||||
};
|
||||
|
||||
// Register helpers
|
||||
debug!("[*]: Register handlebars helpers");
|
||||
handlebars.register_helper("toc", Box::new(helpers::toc::RenderToc));
|
||||
// Non-lexical lifetimes needed :'(
|
||||
let title: String;
|
||||
{
|
||||
let book_title = ctx
|
||||
.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
title = ch.name.clone() + " - " + book_title;
|
||||
}
|
||||
|
||||
ctx.data.insert("path".to_owned(), json!(path));
|
||||
ctx.data.insert("content".to_owned(), json!(content));
|
||||
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
|
||||
ctx.data.insert("title".to_owned(), json!(title));
|
||||
ctx.data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&ch.path)),
|
||||
);
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
|
||||
let rendered = self.post_process(rendered, &ctx.html_config.playpen);
|
||||
|
||||
// Write to file
|
||||
debug!("Creating {}", filepath.display());
|
||||
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
|
||||
|
||||
if ctx.is_index {
|
||||
ctx.data.insert("path".to_owned(), json!("index.html"));
|
||||
ctx.data.insert("path_to_root".to_owned(), json!(""));
|
||||
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
|
||||
let rendered_index =
|
||||
self.post_process(rendered_index, &ctx.html_config.playpen);
|
||||
debug!("Creating index.html from {}", path);
|
||||
utils::fs::write_file(
|
||||
&ctx.destination,
|
||||
"index.html",
|
||||
rendered_index.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
|
||||
fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String {
|
||||
let rendered = build_header_links(&rendered);
|
||||
let rendered = fix_code_blocks(&rendered);
|
||||
let rendered = add_playpen_pre(&rendered, playpen_config);
|
||||
|
||||
rendered
|
||||
}
|
||||
|
||||
fn copy_static_files(
|
||||
&self,
|
||||
destination: &Path,
|
||||
theme: &Theme,
|
||||
html_config: &HtmlConfig,
|
||||
) -> Result<()> {
|
||||
use utils::fs::write_file;
|
||||
|
||||
write_file(
|
||||
destination,
|
||||
".nojekyll",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.",
|
||||
)?;
|
||||
|
||||
write_file(destination, "book.js", &theme.js)?;
|
||||
write_file(destination, "css/general.css", &theme.general_css)?;
|
||||
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
|
||||
write_file(destination, "css/print.css", &theme.print_css)?;
|
||||
write_file(destination, "css/variables.css", &theme.variables_css)?;
|
||||
write_file(destination, "favicon.png", &theme.favicon)?;
|
||||
write_file(destination, "highlight.css", &theme.highlight_css)?;
|
||||
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||
write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||
write_file(destination, "highlight.js", &theme.highlight_js)?;
|
||||
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/css/font-awesome.css",
|
||||
theme::FONT_AWESOME,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
|
||||
let playpen_config = &html_config.playpen;
|
||||
|
||||
// Ace is a very large dependency, so only load it when requested
|
||||
if playpen_config.editable && playpen_config.copy_js {
|
||||
// Load the editor
|
||||
write_file(destination, "editor.js", playpen_editor::JS)?;
|
||||
write_file(destination, "ace.js", playpen_editor::ACE_JS)?;
|
||||
write_file(destination, "mode-rust.js", playpen_editor::MODE_RUST_JS)?;
|
||||
write_file(destination, "theme-dawn.js", playpen_editor::THEME_DAWN_JS)?;
|
||||
write_file(
|
||||
destination,
|
||||
"theme-tomorrow_night.js",
|
||||
playpen_editor::THEME_TOMORROW_NIGHT_JS,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the context with data for this file
|
||||
fn configure_print_version(
|
||||
&self,
|
||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||
print_content: &str,
|
||||
) {
|
||||
// Make sure that the Print chapter does not display the title from
|
||||
// the last rendered chapter by removing it from its context
|
||||
data.remove("title");
|
||||
data.insert("is_print".to_owned(), json!(true));
|
||||
data.insert("path".to_owned(), json!("print.md"));
|
||||
data.insert("content".to_owned(), json!(print_content));
|
||||
data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(Path::new("print.md"))),
|
||||
);
|
||||
}
|
||||
|
||||
fn register_hbs_helpers(&self, handlebars: &mut Handlebars, html_config: &HtmlConfig) {
|
||||
handlebars.register_helper(
|
||||
"toc",
|
||||
Box::new(helpers::toc::RenderToc {
|
||||
no_section_label: html_config.no_section_label,
|
||||
}),
|
||||
);
|
||||
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
|
||||
handlebars.register_helper("next", Box::new(helpers::navigation::next));
|
||||
}
|
||||
|
||||
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,
|
||||
html: &HtmlConfig,
|
||||
root: &Path,
|
||||
destination: &Path,
|
||||
) -> Result<()> {
|
||||
let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
|
||||
|
||||
// Print version
|
||||
let mut print_content: String = String::new();
|
||||
debug!("Copying additional CSS and JS");
|
||||
|
||||
// Check if dest directory exists
|
||||
debug!("[*]: Check if destination directory exists");
|
||||
if let Err(_) = fs::create_dir_all(book.get_dest()) {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Unexpected error when constructing destination path")))
|
||||
}
|
||||
|
||||
// Render a file for every entry in the book
|
||||
let mut index = true;
|
||||
for item in book.iter() {
|
||||
|
||||
match *item {
|
||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
|
||||
if ch.path != PathBuf::new() {
|
||||
|
||||
let path = book.get_src().join(&ch.path);
|
||||
|
||||
debug!("[*]: Opening file: {:?}", path);
|
||||
let mut f = try!(File::open(&path));
|
||||
let mut content: String = String::new();
|
||||
|
||||
debug!("[*]: Reading file");
|
||||
try!(f.read_to_string(&mut content));
|
||||
|
||||
// Parse for playpen links
|
||||
if let Some(p) = path.parent() {
|
||||
content = helpers::playpen::render_playpen(&content, p);
|
||||
}
|
||||
|
||||
// Render markdown using the pulldown-cmark crate
|
||||
content = utils::render_markdown(&content);
|
||||
print_content.push_str(&content);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
for custom_file in custom_files {
|
||||
let input_location = root.join(custom_file);
|
||||
let output_location = destination.join(custom_file);
|
||||
if let Some(parent) = output_location.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.chain_err(|| format!("Unable to create {}", parent.display()))?;
|
||||
}
|
||||
debug!(
|
||||
"Copying {} -> {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
);
|
||||
|
||||
fs::copy(&input_location, &output_location).chain_err(|| {
|
||||
format!(
|
||||
"Unable to copy {} to {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
// 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 = if let Ok(f) = File::create(book.get_dest().join("book.js")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create book.js")))
|
||||
};
|
||||
try!(js_file.write_all(&theme.js));
|
||||
|
||||
// Css
|
||||
let mut css_file = if let Ok(f) = File::create(book.get_dest().join("book.css")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create book.css")))
|
||||
};
|
||||
try!(css_file.write_all(&theme.css));
|
||||
|
||||
// Favicon
|
||||
let mut favicon_file = if let Ok(f) = File::create(book.get_dest().join("favicon.png")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create favicon.png")))
|
||||
};
|
||||
try!(favicon_file.write_all(&theme.favicon));
|
||||
|
||||
// JQuery local fallback
|
||||
let mut jquery = if let Ok(f) = File::create(book.get_dest().join("jquery.js")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create jquery.js")))
|
||||
};
|
||||
try!(jquery.write_all(&theme.jquery));
|
||||
|
||||
// syntax highlighting
|
||||
let mut highlight_css = if let Ok(f) = File::create(book.get_dest().join("highlight.css")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create highlight.css")))
|
||||
};
|
||||
try!(highlight_css.write_all(&theme.highlight_css));
|
||||
|
||||
let mut tomorrow_night_css = if let Ok(f) = File::create(book.get_dest().join("tomorrow-night.css")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create tomorrow-night.css")))
|
||||
};
|
||||
try!(tomorrow_night_css.write_all(&theme.tomorrow_night_css));
|
||||
|
||||
let mut highlight_js = if let Ok(f) = File::create(book.get_dest().join("highlight.js")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create highlight.js")))
|
||||
};
|
||||
try!(highlight_js.write_all(&theme.highlight_js));
|
||||
|
||||
// Font Awesome local fallback
|
||||
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/css/font-awesome.css")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create font-awesome.css")))
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME));
|
||||
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.eot")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.eot")))
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_EOT));
|
||||
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.svg")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.svg")))
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_SVG));
|
||||
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.ttf")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.ttf")))
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
|
||||
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.woff")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff")))
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF));
|
||||
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.woff2")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff2")))
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF2));
|
||||
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/fonts/FontAwesome.ttf")) { f } else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create FontAwesome.ttf")))
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
|
||||
|
||||
// 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>> {
|
||||
debug!("[fn]: make_data");
|
||||
// TODO(mattico): Remove some time after the 0.1.8 release
|
||||
fn maybe_wrong_theme_dir(dir: &Path) -> Result<bool> {
|
||||
fn entry_is_maybe_book_file(entry: fs::DirEntry) -> Result<bool> {
|
||||
Ok(entry.file_type()?.is_file()
|
||||
&& entry.path().extension().map_or(false, |ext| ext == "md"))
|
||||
}
|
||||
|
||||
let mut data = BTreeMap::new();
|
||||
data.insert("language".to_owned(), "en".to_json());
|
||||
data.insert("title".to_owned(), book.get_title().to_json());
|
||||
data.insert("description".to_owned(), book.get_description().to_json());
|
||||
data.insert("favicon".to_owned(), "favicon.png".to_json());
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
if entry_is_maybe_book_file(entry?).unwrap_or(false) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for HtmlHandlebars {
|
||||
fn name(&self) -> &str {
|
||||
"html"
|
||||
}
|
||||
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||
let html_config = ctx.config.html_config().unwrap_or_default();
|
||||
let src_dir = ctx.root.join(&ctx.config.book.src);
|
||||
let destination = &ctx.destination;
|
||||
let book = &ctx.book;
|
||||
|
||||
trace!("render");
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
let theme_dir = match html_config.theme {
|
||||
Some(ref theme) => theme.to_path_buf(),
|
||||
None => ctx.root.join("theme"),
|
||||
};
|
||||
|
||||
if html_config.theme.is_none()
|
||||
&& maybe_wrong_theme_dir(&src_dir.join("theme")).unwrap_or(false)
|
||||
{
|
||||
warn!(
|
||||
"Previous versions of mdBook erroneously accepted `./src/theme` as an automatic \
|
||||
theme directory"
|
||||
);
|
||||
warn!("Please move your theme files to `./theme` for them to continue being used");
|
||||
}
|
||||
|
||||
let theme = theme::Theme::new(theme_dir);
|
||||
|
||||
debug!("Register the index handlebars template");
|
||||
handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
|
||||
|
||||
debug!("Register the header handlebars template");
|
||||
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
|
||||
|
||||
debug!("Register handlebars helpers");
|
||||
self.register_hbs_helpers(&mut handlebars, &html_config);
|
||||
|
||||
let mut data = make_data(&ctx.root, &book, &ctx.config, &html_config)?;
|
||||
|
||||
// Print version
|
||||
let mut print_content = String::new();
|
||||
|
||||
fs::create_dir_all(&destination)
|
||||
.chain_err(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
let mut is_index = true;
|
||||
for item in book.iter() {
|
||||
let ctx = RenderItemContext {
|
||||
handlebars: &handlebars,
|
||||
destination: destination.to_path_buf(),
|
||||
data: data.clone(),
|
||||
is_index: is_index,
|
||||
html_config: html_config.clone(),
|
||||
};
|
||||
self.render_item(item, ctx, &mut print_content)?;
|
||||
is_index = false;
|
||||
}
|
||||
|
||||
// Print version
|
||||
self.configure_print_version(&mut data, &print_content);
|
||||
if let Some(ref title) = ctx.config.book.title {
|
||||
data.insert("title".to_owned(), json!(title));
|
||||
}
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
|
||||
let rendered = self.post_process(rendered, &html_config.playpen);
|
||||
|
||||
utils::fs::write_file(&destination, "print.html", rendered.as_bytes())?;
|
||||
debug!("Creating print.html ✓");
|
||||
|
||||
debug!("Copy static files");
|
||||
self.copy_static_files(&destination, &theme, &html_config)
|
||||
.chain_err(|| "Unable to copy across static files")?;
|
||||
self.copy_additional_css_and_js(&html_config, &ctx.root, &destination)
|
||||
.chain_err(|| "Unable to copy across additional CSS and JS")?;
|
||||
|
||||
// Render search index
|
||||
#[cfg(feature = "search")]
|
||||
{
|
||||
let search = html_config.search.unwrap_or_default();
|
||||
if search.enable {
|
||||
super::search::create_files(&search, &destination, &book)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy all remaining files
|
||||
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_data(
|
||||
root: &Path,
|
||||
book: &Book,
|
||||
config: &Config,
|
||||
html_config: &HtmlConfig,
|
||||
) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
trace!("make_data");
|
||||
let html = config.html_config().unwrap_or_default();
|
||||
|
||||
let mut data = serde_json::Map::new();
|
||||
data.insert("language".to_owned(), json!("en"));
|
||||
data.insert(
|
||||
"book_title".to_owned(),
|
||||
json!(config.book.title.clone().unwrap_or_default()),
|
||||
);
|
||||
data.insert(
|
||||
"description".to_owned(),
|
||||
json!(config.book.description.clone().unwrap_or_default()),
|
||||
);
|
||||
data.insert("favicon".to_owned(), json!("favicon.png"));
|
||||
if let Some(ref livereload) = html_config.livereload_url {
|
||||
data.insert("livereload".to_owned(), json!(livereload));
|
||||
}
|
||||
|
||||
// Add google analytics tag
|
||||
if let Some(ref ga) = config.html_config().and_then(|html| html.google_analytics) {
|
||||
data.insert("google_analytics".to_owned(), json!(ga));
|
||||
}
|
||||
|
||||
if html.mathjax_support {
|
||||
data.insert("mathjax_support".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional style
|
||||
if !html.additional_css.is_empty() {
|
||||
let mut css = Vec::new();
|
||||
for style in &html.additional_css {
|
||||
match style.strip_prefix(root) {
|
||||
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => css.push(style.to_str().expect("Could not convert to str")),
|
||||
}
|
||||
}
|
||||
data.insert("additional_css".to_owned(), json!(css));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional script
|
||||
if !html.additional_js.is_empty() {
|
||||
let mut js = Vec::new();
|
||||
for script in &html.additional_js {
|
||||
match script.strip_prefix(root) {
|
||||
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => js.push(
|
||||
script
|
||||
.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str"),
|
||||
),
|
||||
}
|
||||
}
|
||||
data.insert("additional_js".to_owned(), json!(js));
|
||||
}
|
||||
|
||||
if html.playpen.editable && html.playpen.copy_js {
|
||||
data.insert("playpen_js".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
let search = html_config.search.clone();
|
||||
if cfg!(feature = "search") {
|
||||
let search = search.unwrap_or_default();
|
||||
data.insert("search_enabled".to_owned(), json!(search.enable));
|
||||
data.insert(
|
||||
"search_js".to_owned(),
|
||||
json!(search.enable && search.copy_js),
|
||||
);
|
||||
} else if search.is_some() {
|
||||
warn!("mdBook compiled without search support, ignoring `output.html.search` table");
|
||||
warn!(
|
||||
"please reinstall with `cargo install mdbook --force --features search`to use the \
|
||||
search feature"
|
||||
)
|
||||
}
|
||||
|
||||
let mut chapters = vec![];
|
||||
|
||||
@@ -251,32 +461,200 @@ fn make_data(book: &MDBook) -> Result<BTreeMap<String,Json>, Box<Error>> {
|
||||
let mut chapter = BTreeMap::new();
|
||||
|
||||
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"))),
|
||||
BookItem::Chapter(ref ch) => {
|
||||
if let Some(ref section) = ch.number {
|
||||
chapter.insert("section".to_owned(), json!(section.to_string()));
|
||||
}
|
||||
},
|
||||
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"))),
|
||||
}
|
||||
},
|
||||
BookItem::Spacer => {
|
||||
chapter.insert("spacer".to_owned(), "_spacer_".to_json());
|
||||
}
|
||||
|
||||
chapter.insert("name".to_owned(), json!(ch.name));
|
||||
let path = ch
|
||||
.path
|
||||
.to_str()
|
||||
.chain_err(|| "Could not convert path to str")?;
|
||||
chapter.insert("path".to_owned(), json!(path));
|
||||
}
|
||||
BookItem::Separator => {
|
||||
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) -> 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)
|
||||
}).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>,
|
||||
) -> String {
|
||||
let raw_id = utils::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="#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content
|
||||
)
|
||||
}
|
||||
|
||||
// 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: &Playpen) -> String {
|
||||
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
|
||||
if (classes.contains("language-rust")
|
||||
&& !classes.contains("ignore")
|
||||
&& !classes.contains("noplaypen"))
|
||||
|| classes.contains("mdbook-runnable")
|
||||
{
|
||||
// wrap the contents in an external pre block
|
||||
if playpen_config.editable && classes.contains("editable")
|
||||
|| text.contains("fn main")
|
||||
|| text.contains("quick_main!")
|
||||
{
|
||||
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,
|
||||
destination: PathBuf,
|
||||
data: serde_json::Map<String, serde_json::Value>,
|
||||
is_index: bool,
|
||||
html_config: HtmlConfig,
|
||||
}
|
||||
|
||||
#[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="#foo" id="foo"><h1>Foo</h1></a>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1>",
|
||||
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
|
||||
),
|
||||
(
|
||||
"<h3>Foo^bar</h3>",
|
||||
r##"<a class="header" href="#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
|
||||
),
|
||||
(
|
||||
"<h4></h4>",
|
||||
r##"<a class="header" href="#" id=""><h4></h4></a>"##,
|
||||
),
|
||||
(
|
||||
"<h4><em>Hï</em></h4>",
|
||||
r##"<a class="header" href="#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1><h3>Foo</h3>",
|
||||
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a><a class="header" href="#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
|
||||
),
|
||||
];
|
||||
|
||||
for (src, should_be) in inputs {
|
||||
let got = build_header_links(&src);
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod navigation;
|
||||
pub mod toc;
|
||||
pub mod playpen;
|
||||
|
||||
@@ -1,195 +1,269 @@
|
||||
extern crate handlebars;
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use self::rustc_serialize::json::{self, ToJson};
|
||||
use self::handlebars::{Handlebars, RenderError, RenderContext, Helper, Context, Renderable};
|
||||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
|
||||
use serde_json;
|
||||
|
||||
// Handlebars helper for navigation
|
||||
use utils;
|
||||
|
||||
pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: previous (handlebars helper)");
|
||||
type StringMap = BTreeMap<String, String>;
|
||||
|
||||
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()
|
||||
.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 {
|
||||
|
||||
match item.get("path") {
|
||||
Some(path) if !path.is_empty() => {
|
||||
if path == ¤t {
|
||||
|
||||
debug!("[*]: Found current chapter");
|
||||
if let Some(previous) = previous{
|
||||
|
||||
debug!("[*]: Creating BTreeMap to inject in context");
|
||||
// Create new BTreeMap to extend the context: 'title' and 'link'
|
||||
let mut previous_chapter = BTreeMap::new();
|
||||
|
||||
// Chapter title
|
||||
match previous.get("name") {
|
||||
Some(n) => {
|
||||
debug!("[*]: Inserting title: {}", n);
|
||||
previous_chapter.insert("title".to_owned(), n.to_json())
|
||||
},
|
||||
None => {
|
||||
debug!("[*]: No title found for chapter");
|
||||
return Err(RenderError{ desc: "No title found for chapter in JSON data".to_owned() })
|
||||
}
|
||||
};
|
||||
|
||||
// Chapter link
|
||||
|
||||
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() })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else {
|
||||
previous = Some(item.clone());
|
||||
}
|
||||
},
|
||||
_ => continue,
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Ok(())
|
||||
/// Target for `find_chapter`.
|
||||
enum Target {
|
||||
Previous,
|
||||
Next,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
/// Returns target if found.
|
||||
fn find(
|
||||
&self,
|
||||
base_path: &String,
|
||||
current_path: &String,
|
||||
current_item: &StringMap,
|
||||
previous_item: &StringMap,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
match self {
|
||||
&Target::Next => {
|
||||
let previous_path = previous_item
|
||||
.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
|
||||
|
||||
if previous_path == base_path {
|
||||
return Ok(Some(current_item.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
&Target::Previous => {
|
||||
if current_path == base_path {
|
||||
return Ok(Some(previous_item.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: next (handlebars helper)");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
fn find_chapter(
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
target: Target,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
debug!("Get data from context");
|
||||
|
||||
let current = c.navigate(rc.get_path(), "path")
|
||||
.to_string()
|
||||
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
|
||||
let base_path = rc
|
||||
.evaluate_absolute(ctx, "path", true)?
|
||||
.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;
|
||||
let mut previous: Option<StringMap> = None;
|
||||
|
||||
debug!("[*]: Search for current Chapter");
|
||||
// Search for current chapter and return previous entry
|
||||
for item in decoded {
|
||||
debug!("Search for chapter");
|
||||
|
||||
for item in chapters {
|
||||
match item.get("path") {
|
||||
|
||||
Some(path) if !path.is_empty() => {
|
||||
|
||||
if let Some(previous) = previous {
|
||||
|
||||
let previous_path = match previous.get("path") {
|
||||
Some(p) => p,
|
||||
None => return Err(RenderError{ desc: "No path found for chapter in JSON data".to_owned() })
|
||||
};
|
||||
|
||||
if previous_path == ¤t {
|
||||
|
||||
debug!("[*]: Found current chapter");
|
||||
debug!("[*]: Creating BTreeMap to inject in context");
|
||||
// Create new BTreeMap to extend the context: 'title' and 'link'
|
||||
let mut next_chapter = BTreeMap::new();
|
||||
|
||||
match item.get("name") {
|
||||
Some(n) => {
|
||||
debug!("[*]: Inserting title: {}", n);
|
||||
next_chapter.insert("title".to_owned(), n.to_json());
|
||||
}
|
||||
None => return Err(RenderError{ desc: "No title found for chapter in JSON data".to_owned() })
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
|
||||
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
|
||||
if let Some(item) = target.find(&base_path, &path, &item, &previous)? {
|
||||
return Ok(Some(item));
|
||||
}
|
||||
}
|
||||
|
||||
previous = Some(item.clone());
|
||||
},
|
||||
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn render(
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
chapter: &StringMap,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("Creating BTreeMap to inject in context");
|
||||
|
||||
let mut context = BTreeMap::new();
|
||||
let base_path = rc
|
||||
.evaluate_absolute(ctx, "path", false)?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
context.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&base_path)),
|
||||
);
|
||||
|
||||
chapter
|
||||
.get("name")
|
||||
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
|
||||
.map(|name| context.insert("title".to_owned(), json!(name)))?;
|
||||
|
||||
chapter
|
||||
.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
|
||||
.and_then(|p| {
|
||||
Path::new(p)
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
|
||||
.map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/"))))
|
||||
})?;
|
||||
|
||||
trace!("Render template");
|
||||
|
||||
_h.template()
|
||||
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
||||
.and_then(|t| {
|
||||
let mut local_rc = rc.new_for_block();
|
||||
let local_ctx = Context::wraps(&context)?;
|
||||
t.render(r, &local_ctx, &mut local_rc, out)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn previous(
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("previous (handlebars helper)");
|
||||
|
||||
if let Some(previous) = find_chapter(ctx, rc, Target::Previous)? {
|
||||
render(_h, r, ctx, rc, out, &previous)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next(
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("next (handlebars helper)");
|
||||
|
||||
if let Some(next) = find_chapter(ctx, rc, Target::Next)? {
|
||||
render(_h, r, ctx, rc, out, &next)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
static TEMPLATE: &'static str =
|
||||
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
|
||||
|
||||
#[test]
|
||||
fn test_next_previous() {
|
||||
let data = json!({
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
|
||||
assert_eq!(
|
||||
h.render_template(TEMPLATE, &data).unwrap(),
|
||||
"one: one.html|three: three.html"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first() {
|
||||
let data = json!({
|
||||
"name": "one",
|
||||
"path": "one.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
|
||||
assert_eq!(
|
||||
h.render_template(TEMPLATE, &data).unwrap(),
|
||||
"|two: two.html"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_last() {
|
||||
let data = json!({
|
||||
"name": "three",
|
||||
"path": "three.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
|
||||
assert_eq!(
|
||||
h.render_template(TEMPLATE, &data).unwrap(),
|
||||
"two: two.html|"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
extern crate handlebars;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
|
||||
pub fn render_playpen(s: &str, path: &Path) -> String {
|
||||
// When replacing one thing in a string by something with a different length, the indices
|
||||
// after that will not correspond, we therefore have to store the difference to correct this
|
||||
let mut previous_end_index = 0;
|
||||
let mut replaced = String::new();
|
||||
|
||||
for playpen in find_playpens(s, path) {
|
||||
|
||||
if playpen.escaped {
|
||||
replaced.push_str(&s[previous_end_index..playpen.start_index-1]);
|
||||
replaced.push_str(&s[playpen.start_index..playpen.end_index]);
|
||||
previous_end_index = playpen.end_index;
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the file exists
|
||||
if !playpen.rust_file.exists() || !playpen.rust_file.is_file() {
|
||||
output!("[-] No file exists for {{{{#playpen }}}}\n {}", playpen.rust_file.to_str().unwrap());
|
||||
continue
|
||||
}
|
||||
|
||||
// Open file & read file
|
||||
let mut file = if let Ok(f) = File::open(&playpen.rust_file) { f } else { continue };
|
||||
let mut file_content = String::new();
|
||||
if let Err(_) = file.read_to_string(&mut file_content) { continue };
|
||||
|
||||
let replacement = String::new() + "<pre class=\"playpen\"><code class=\"language-rust\">" + &file_content + "</code></pre>";
|
||||
|
||||
replaced.push_str(&s[previous_end_index..playpen.start_index]);
|
||||
replaced.push_str(&replacement);
|
||||
previous_end_index = playpen.end_index;
|
||||
//println!("Playpen{{ {}, {}, {:?}, {} }}", playpen.start_index, playpen.end_index, playpen.rust_file, playpen.editable);
|
||||
}
|
||||
|
||||
replaced.push_str(&s[previous_end_index..]);
|
||||
|
||||
replaced
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, PartialEq, Debug)]
|
||||
struct Playpen{
|
||||
start_index: usize,
|
||||
end_index: usize,
|
||||
rust_file: PathBuf,
|
||||
editable: bool,
|
||||
escaped: bool,
|
||||
}
|
||||
|
||||
fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> {
|
||||
let mut playpens = vec![];
|
||||
for (i, _) in s.match_indices("{{#playpen") {
|
||||
debug!("[*]: find_playpen");
|
||||
|
||||
let mut escaped = false;
|
||||
|
||||
if i > 0 {
|
||||
if let Some(c) = s[i-1..].chars().nth(0) {
|
||||
if c == '\\' { escaped = true }
|
||||
}
|
||||
}
|
||||
// DON'T forget the "+ i" else you have an index out of bounds error !!
|
||||
let end_i = if let Some(n) = s[i..].find("}}") { n } else { continue } + i + 2;
|
||||
|
||||
debug!("s[{}..{}] = {}", i, end_i, s[i..end_i].to_string());
|
||||
|
||||
// If there is nothing between "{{#playpen" and "}}" skip
|
||||
if end_i-2 - (i+10) < 1 { continue }
|
||||
if s[i+10..end_i-2].trim().len() == 0 { continue }
|
||||
|
||||
debug!("{}", s[i+10..end_i-2].to_string());
|
||||
|
||||
// Split on whitespaces
|
||||
let params: Vec<&str> = s[i+10..end_i-2].split_whitespace().collect();
|
||||
let mut editable = false;
|
||||
|
||||
if params.len() > 1 {
|
||||
editable = if let Some(_) = params[1].find("editable") {true} else {false};
|
||||
}
|
||||
|
||||
playpens.push(
|
||||
Playpen{
|
||||
start_index: i,
|
||||
end_index: end_i,
|
||||
rust_file: base_path.join(PathBuf::from(params[0])),
|
||||
editable: editable,
|
||||
escaped: escaped
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
playpens
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_no_playpen() {
|
||||
let s = "Some random text without playpen...";
|
||||
assert!(find_playpens(s, Path::new("")) == vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_partial_playpen() {
|
||||
let s = "Some random text with {{#playpen...";
|
||||
assert!(find_playpens(s, Path::new("")) == vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_empty_playpen() {
|
||||
let s = "Some random text with {{#playpen}} and {{#playpen }}...";
|
||||
assert!(find_playpens(s, Path::new("")) == vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_simple_playpen() {
|
||||
let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";
|
||||
|
||||
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("")));
|
||||
|
||||
assert!(find_playpens(s, Path::new("")) == vec![
|
||||
Playpen{start_index: 22, end_index: 42, rust_file: PathBuf::from("file.rs"), editable: false, escaped: false},
|
||||
Playpen{start_index: 47, end_index: 68, rust_file: PathBuf::from("test.rs"), editable: false, escaped: false}
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_complex_playpen() {
|
||||
let s = "Some random text with {{#playpen file.rs editable}} and {{#playpen test.rs editable }}...";
|
||||
|
||||
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("dir")));
|
||||
|
||||
assert!(find_playpens(s, Path::new("dir")) == vec![
|
||||
Playpen{start_index: 22, end_index: 51, rust_file: PathBuf::from("dir/file.rs"), editable: true, escaped: false},
|
||||
Playpen{start_index: 56, end_index: 86, rust_file: PathBuf::from("dir/test.rs"), editable: true, escaped: false}
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_escaped_playpen() {
|
||||
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
|
||||
|
||||
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("")));
|
||||
|
||||
assert!(find_playpens(s, Path::new("")) == vec![
|
||||
Playpen{start_index: 39, end_index: 68, rust_file: PathBuf::from("file.rs"), editable: true, escaped: true},
|
||||
]);
|
||||
}
|
||||
@@ -1,130 +1,151 @@
|
||||
extern crate handlebars;
|
||||
extern crate rustc_serialize;
|
||||
extern crate pulldown_cmark;
|
||||
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use self::rustc_serialize::json;
|
||||
use self::handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
|
||||
use self::pulldown_cmark::{Parser, html, Event, Tag};
|
||||
use utils;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
|
||||
use pulldown_cmark::{html, Event, Parser, Tag};
|
||||
use serde_json;
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RenderToc;
|
||||
pub struct RenderToc {
|
||||
pub no_section_label: bool,
|
||||
}
|
||||
|
||||
impl HelperDef for RenderToc {
|
||||
fn call(&self, c: &Context, _h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
_h: &Helper,
|
||||
_: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
) -> Result<(), RenderError> {
|
||||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
let current = rc
|
||||
.evaluate_absolute(ctx, "path", true)?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
// 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()));
|
||||
out.write("<ol class=\"chapter\">")?;
|
||||
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String,String>> = json::decode(&chapters.to_string()).unwrap();
|
||||
let mut current_level = 1;
|
||||
|
||||
let mut current_level = 1;
|
||||
|
||||
for item in decoded {
|
||||
|
||||
// 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;
|
||||
for item in chapters {
|
||||
// Spacer
|
||||
if item.get("spacer").is_some() {
|
||||
out.write("<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 {
|
||||
out.write("<li>")?;
|
||||
out.write("<ol class=\"section\">")?;
|
||||
current_level += 1;
|
||||
}
|
||||
out.write("<li>")?;
|
||||
} else if level < current_level {
|
||||
while level < current_level {
|
||||
out.write("</ol>")?;
|
||||
out.write("</li>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
out.write("<li>")?;
|
||||
} else {
|
||||
out.write("<li")?;
|
||||
if item.get("section").is_none() {
|
||||
out.write(" class=\"affix\"")?;
|
||||
}
|
||||
out.write(">")?;
|
||||
}
|
||||
|
||||
try!(rc.writer.write(">".as_bytes()));
|
||||
true
|
||||
// Link
|
||||
let path_exists = if let Some(path) = item.get("path") {
|
||||
if !path.is_empty() {
|
||||
out.write("<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
|
||||
out.write(&utils::fs::path_to_root(¤t))?;
|
||||
out.write(&tmp)?;
|
||||
out.write("\"")?;
|
||||
|
||||
if path == ¤t {
|
||||
out.write(" class=\"active\"")?;
|
||||
}
|
||||
|
||||
out.write(">")?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}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) => true,
|
||||
&Event::InlineHtml(_) => true,
|
||||
&Event::Text(_) => true,
|
||||
_ => false,
|
||||
if !self.no_section_label {
|
||||
// Section does not necessarily exist
|
||||
if let Some(section) = item.get("section") {
|
||||
out.write("<strong aria-hidden=\"true\">")?;
|
||||
out.write(§ion)?;
|
||||
out.write("</strong> ")?;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// render markdown to html
|
||||
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
|
||||
html::push_html(&mut markdown_parsed_name, parser);
|
||||
if let Some(name) = item.get("name") {
|
||||
// Render only inline code blocks
|
||||
|
||||
// write to the handlebars template
|
||||
try!(rc.writer.write(markdown_parsed_name.as_bytes()));
|
||||
// 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
|
||||
out.write(&markdown_parsed_name)?;
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
out.write("</a>")?;
|
||||
}
|
||||
|
||||
out.write("</li>")?;
|
||||
}
|
||||
while current_level > 1 {
|
||||
out.write("</ol>")?;
|
||||
out.write("</li>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
try!(rc.writer.write("</a>".as_bytes()));
|
||||
}
|
||||
|
||||
try!(rc.writer.write("</li>".as_bytes()));
|
||||
|
||||
current_level = level;
|
||||
out.write("</ol>")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
try!(rc.writer.write("</ul>".as_bytes()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
#![allow(missing_docs)] // FIXME: Document this
|
||||
|
||||
pub use self::hbs_renderer::HtmlHandlebars;
|
||||
|
||||
mod hbs_renderer;
|
||||
mod helpers;
|
||||
|
||||
#[cfg(feature = "search")]
|
||||
mod search;
|
||||
|
||||
251
src/renderer/html_handlebars/search.rs
Normal file
251
src/renderer/html_handlebars/search.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
extern crate ammonia;
|
||||
extern crate elasticlunr;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use self::elasticlunr::Index;
|
||||
use pulldown_cmark::*;
|
||||
use serde_json;
|
||||
|
||||
use book::{Book, BookItem};
|
||||
use config::Search;
|
||||
use errors::*;
|
||||
use theme::searcher;
|
||||
use utils;
|
||||
|
||||
/// Creates all files required for search.
|
||||
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
|
||||
let mut index = Index::new(&["title", "body", "breadcrumbs"]);
|
||||
let mut doc_urls = Vec::with_capacity(book.sections.len());
|
||||
|
||||
for item in book.iter() {
|
||||
render_item(&mut index, &search_config, &mut doc_urls, item)?;
|
||||
}
|
||||
|
||||
let index = write_to_json(index, &search_config, doc_urls)?;
|
||||
debug!("Writing search index ✓");
|
||||
if index.len() > 10_000_000 {
|
||||
warn!("searchindex.json is very large ({} bytes)", index.len());
|
||||
}
|
||||
|
||||
if search_config.copy_js {
|
||||
utils::fs::write_file(destination, "searchindex.json", index.as_bytes())?;
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
"searchindex.js",
|
||||
format!("window.search = {};", index).as_bytes(),
|
||||
)?;
|
||||
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
||||
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
||||
utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
|
||||
debug!("Copying search files ✓");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uses the given arguments to construct a search document, then inserts it to the given index.
|
||||
fn add_doc(
|
||||
index: &mut Index,
|
||||
doc_urls: &mut Vec<String>,
|
||||
anchor_base: &str,
|
||||
section_id: &Option<String>,
|
||||
items: &[&str],
|
||||
) {
|
||||
let url = if let &Some(ref id) = section_id {
|
||||
Cow::Owned(format!("{}#{}", anchor_base, id))
|
||||
} else {
|
||||
Cow::Borrowed(anchor_base)
|
||||
};
|
||||
let url = utils::collapse_whitespace(url.trim());
|
||||
let doc_ref = doc_urls.len().to_string();
|
||||
doc_urls.push(url.into());
|
||||
|
||||
let items = items.iter().map(|&x| utils::collapse_whitespace(x.trim()));
|
||||
index.add_doc(&doc_ref, items);
|
||||
}
|
||||
|
||||
/// Renders markdown into flat unformatted text and adds it to the search index.
|
||||
fn render_item(
|
||||
index: &mut Index,
|
||||
search_config: &Search,
|
||||
doc_urls: &mut Vec<String>,
|
||||
item: &BookItem,
|
||||
) -> Result<()> {
|
||||
let chapter = match item {
|
||||
&BookItem::Chapter(ref ch) => ch,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
let filepath = Path::new(&chapter.path).with_extension("html");
|
||||
let filepath = filepath
|
||||
.to_str()
|
||||
.chain_err(|| "Could not convert HTML path to str")?;
|
||||
let anchor_base = utils::fs::normalize_path(filepath);
|
||||
|
||||
let mut opts = Options::empty();
|
||||
opts.insert(OPTION_ENABLE_TABLES);
|
||||
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
||||
let p = Parser::new_ext(&chapter.content, opts);
|
||||
|
||||
let mut in_header = false;
|
||||
let max_section_depth = search_config.heading_split_level as i32;
|
||||
let mut section_id = None;
|
||||
let mut heading = String::new();
|
||||
let mut body = String::new();
|
||||
let mut breadcrumbs = chapter.parent_names.clone();
|
||||
let mut footnote_numbers = HashMap::new();
|
||||
|
||||
for event in p {
|
||||
match event {
|
||||
Event::Start(Tag::Header(i)) if i <= max_section_depth => {
|
||||
if heading.len() > 0 {
|
||||
// Section finished, the next header is following now
|
||||
// Write the data to the index, and clear it for the next section
|
||||
add_doc(
|
||||
index,
|
||||
doc_urls,
|
||||
&anchor_base,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
section_id = None;
|
||||
heading.clear();
|
||||
body.clear();
|
||||
breadcrumbs.pop();
|
||||
}
|
||||
|
||||
in_header = true;
|
||||
}
|
||||
Event::End(Tag::Header(i)) if i <= max_section_depth => {
|
||||
in_header = false;
|
||||
section_id = Some(utils::id_from_content(&heading));
|
||||
breadcrumbs.push(heading.clone());
|
||||
}
|
||||
Event::Start(Tag::FootnoteDefinition(name)) => {
|
||||
let number = footnote_numbers.len() + 1;
|
||||
footnote_numbers.entry(name).or_insert(number);
|
||||
}
|
||||
Event::Start(_) | Event::End(_) | Event::SoftBreak | Event::HardBreak => {
|
||||
// Insert spaces where HTML output would usually seperate text
|
||||
// to ensure words don't get merged together
|
||||
if in_header {
|
||||
heading.push(' ');
|
||||
} else {
|
||||
body.push(' ');
|
||||
}
|
||||
}
|
||||
Event::Text(text) => {
|
||||
if in_header {
|
||||
heading.push_str(&text);
|
||||
} else {
|
||||
body.push_str(&text);
|
||||
}
|
||||
}
|
||||
Event::Html(html) | Event::InlineHtml(html) => {
|
||||
body.push_str(&clean_html(&html));
|
||||
}
|
||||
Event::FootnoteReference(name) => {
|
||||
let len = footnote_numbers.len() + 1;
|
||||
let number = footnote_numbers.entry(name).or_insert(len);
|
||||
body.push_str(&format!(" [{}] ", number));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if heading.len() > 0 {
|
||||
// Make sure the last section is added to the index
|
||||
add_doc(
|
||||
index,
|
||||
doc_urls,
|
||||
&anchor_base,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
|
||||
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ResultsOptions {
|
||||
limit_results: u32,
|
||||
teaser_word_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SearchindexJson {
|
||||
/// The options used for displaying search results
|
||||
results_options: ResultsOptions,
|
||||
/// The searchoptions for elasticlunr.js
|
||||
search_options: SearchOptions,
|
||||
/// Used to lookup a document's URL from an integer document ref.
|
||||
doc_urls: Vec<String>,
|
||||
/// The index for elasticlunr.js
|
||||
index: elasticlunr::Index,
|
||||
}
|
||||
|
||||
let mut fields = BTreeMap::new();
|
||||
let mut opt = SearchOptionsField::default();
|
||||
opt.boost = Some(search_config.boost_title);
|
||||
fields.insert("title".into(), opt);
|
||||
opt.boost = Some(search_config.boost_paragraph);
|
||||
fields.insert("body".into(), opt);
|
||||
opt.boost = Some(search_config.boost_hierarchy);
|
||||
fields.insert("breadcrumbs".into(), opt);
|
||||
|
||||
let search_options = SearchOptions {
|
||||
bool: if search_config.use_boolean_and {
|
||||
SearchBool::And
|
||||
} else {
|
||||
SearchBool::Or
|
||||
},
|
||||
expand: search_config.expand,
|
||||
fields,
|
||||
};
|
||||
|
||||
let results_options = ResultsOptions {
|
||||
limit_results: search_config.limit_results,
|
||||
teaser_word_count: search_config.teaser_word_count,
|
||||
};
|
||||
|
||||
let json_contents = SearchindexJson {
|
||||
results_options,
|
||||
search_options,
|
||||
doc_urls,
|
||||
index,
|
||||
};
|
||||
|
||||
// By converting to serde_json::Value as an intermediary, we use a
|
||||
// BTreeMap internally and can force a stable ordering of map keys.
|
||||
let json_contents = serde_json::to_value(&json_contents)?;
|
||||
let json_contents = serde_json::to_string(&json_contents)?;
|
||||
|
||||
Ok(json_contents)
|
||||
}
|
||||
|
||||
fn clean_html(html: &str) -> String {
|
||||
lazy_static! {
|
||||
static ref AMMONIA: ammonia::Builder<'static> = {
|
||||
let mut clean_content = HashSet::new();
|
||||
clean_content.insert("script");
|
||||
clean_content.insert("style");
|
||||
let mut builder = ammonia::Builder::new();
|
||||
builder
|
||||
.tags(HashSet::new())
|
||||
.tag_attributes(HashMap::new())
|
||||
.generic_attributes(HashSet::new())
|
||||
.link_rel(None)
|
||||
.allowed_classes(HashMap::new())
|
||||
.clean_content_tags(clean_content);
|
||||
builder
|
||||
};
|
||||
}
|
||||
AMMONIA.clean(html).to_string()
|
||||
}
|
||||
@@ -1,5 +1,207 @@
|
||||
pub use self::renderer::Renderer;
|
||||
//! `mdbook`'s low level rendering interface.
|
||||
//!
|
||||
//! # Note
|
||||
//!
|
||||
//! You usually don't need to work with this module directly. If you want to
|
||||
//! implement your own backend, then check out the [For Developers] section of
|
||||
//! the user guide.
|
||||
//!
|
||||
//! The definition for [RenderContext] may be useful though.
|
||||
//!
|
||||
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/lib/index.html
|
||||
//! [RenderContext]: struct.RenderContext.html
|
||||
|
||||
pub use self::html_handlebars::HtmlHandlebars;
|
||||
|
||||
pub mod renderer;
|
||||
mod html_handlebars;
|
||||
|
||||
use serde_json;
|
||||
use shlex::Shlex;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use book::Book;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
|
||||
/// An arbitrary `mdbook` backend.
|
||||
///
|
||||
/// Although it's quite possible for you to import `mdbook` as a library and
|
||||
/// provide your own renderer, there are two main renderer implementations that
|
||||
/// 99% of users will ever use:
|
||||
///
|
||||
/// - [HtmlHandlebars] - the built-in HTML renderer
|
||||
/// - [CmdRenderer] - a generic renderer which shells out to a program to do the
|
||||
/// actual rendering
|
||||
///
|
||||
/// [HtmlHandlebars]: struct.HtmlHandlebars.html
|
||||
/// [CmdRenderer]: struct.CmdRenderer.html
|
||||
pub trait Renderer {
|
||||
/// The `Renderer`'s name.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Invoke the `Renderer`, passing in all the necessary information for
|
||||
/// describing a book.
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()>;
|
||||
}
|
||||
|
||||
/// The context provided to all renderers.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RenderContext {
|
||||
/// Which version of `mdbook` did this come from (as written in `mdbook`'s
|
||||
/// `Cargo.toml`). Useful if you know the renderer is only compatible with
|
||||
/// certain versions of `mdbook`.
|
||||
pub version: String,
|
||||
/// The book's root directory.
|
||||
pub root: PathBuf,
|
||||
/// A loaded representation of the book itself.
|
||||
pub book: Book,
|
||||
/// The loaded configuration file.
|
||||
pub config: Config,
|
||||
/// Where the renderer *must* put any build artefacts generated. To allow
|
||||
/// renderers to cache intermediate results, this directory is not
|
||||
/// guaranteed to be empty or even exist.
|
||||
pub destination: PathBuf,
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
impl RenderContext {
|
||||
/// Create a new `RenderContext`.
|
||||
pub fn new<P, Q>(root: P, book: Book, config: Config, destination: Q) -> RenderContext
|
||||
where
|
||||
P: Into<PathBuf>,
|
||||
Q: Into<PathBuf>,
|
||||
{
|
||||
RenderContext {
|
||||
book: book,
|
||||
config: config,
|
||||
version: ::MDBOOK_VERSION.to_string(),
|
||||
root: root.into(),
|
||||
destination: destination.into(),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the source directory's (absolute) path on disk.
|
||||
pub fn source_dir(&self) -> PathBuf {
|
||||
self.root.join(&self.config.book.src)
|
||||
}
|
||||
|
||||
/// Load a `RenderContext` from its JSON representation.
|
||||
pub fn from_json<R: Read>(reader: R) -> Result<RenderContext> {
|
||||
serde_json::from_reader(reader).chain_err(|| "Unable to deserialize the `RenderContext`")
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic renderer which will shell out to an arbitrary executable.
|
||||
///
|
||||
/// # Rendering Protocol
|
||||
///
|
||||
/// When the renderer's `render()` method is invoked, `CmdRenderer` will spawn
|
||||
/// the `cmd` as a subprocess. The `RenderContext` is passed to the subprocess
|
||||
/// as a JSON string (using `serde_json`).
|
||||
///
|
||||
/// > **Note:** The command used doesn't necessarily need to be a single
|
||||
/// > executable (i.e. `/path/to/renderer`). The `cmd` string lets you pass
|
||||
/// > in command line arguments, so there's no reason why it couldn't be
|
||||
/// > `python /path/to/renderer --from mdbook --to epub`.
|
||||
///
|
||||
/// Anything the subprocess writes to `stdin` or `stdout` will be passed through
|
||||
/// to the user. While this gives the renderer maximum flexibility to output
|
||||
/// whatever it wants, to avoid spamming users it is recommended to avoid
|
||||
/// unnecessary output.
|
||||
///
|
||||
/// To help choose the appropriate output level, the `RUST_LOG` environment
|
||||
/// variable will be passed through to the subprocess, if set.
|
||||
///
|
||||
/// If the subprocess wishes to indicate that rendering failed, it should exit
|
||||
/// with a non-zero return code.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CmdRenderer {
|
||||
name: String,
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
impl CmdRenderer {
|
||||
/// Create a new `CmdRenderer` which will invoke the provided `cmd` string.
|
||||
pub fn new(name: String, cmd: String) -> CmdRenderer {
|
||||
CmdRenderer { name, cmd }
|
||||
}
|
||||
|
||||
fn compose_command(&self) -> Result<Command> {
|
||||
let mut words = Shlex::new(&self.cmd);
|
||||
let executable = match words.next() {
|
||||
Some(e) => e,
|
||||
None => bail!("Command string was empty"),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(executable);
|
||||
|
||||
for arg in words {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for CmdRenderer {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||
info!("Invoking the \"{}\" renderer", self.name);
|
||||
|
||||
let _ = fs::create_dir_all(&ctx.destination);
|
||||
|
||||
let mut child = match self
|
||||
.compose_command()?
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.current_dir(&ctx.destination)
|
||||
.spawn()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
warn!(
|
||||
"The command wasn't found, is the \"{}\" backend installed?",
|
||||
self.name
|
||||
);
|
||||
warn!("\tCommand: {}", self.cmd);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e).chain_err(|| "Unable to start the backend")?;
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
let mut stdin = child.stdin.take().expect("Child has stdin");
|
||||
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
|
||||
// Looks like the backend hung up before we could finish
|
||||
// sending it the render context. Log the error and keep going
|
||||
warn!("Error writing the RenderContext to the backend, {}", e);
|
||||
}
|
||||
|
||||
// explicitly close the `stdin` file handle
|
||||
drop(stdin);
|
||||
}
|
||||
|
||||
let status = child
|
||||
.wait()
|
||||
.chain_err(|| "Error waiting for the backend to complete")?;
|
||||
|
||||
trace!("{} exited with output: {:?}", self.cmd, status);
|
||||
|
||||
if !status.success() {
|
||||
error!("Renderer exited with non-zero return code.");
|
||||
bail!("The \"{}\" renderer failed", self.name);
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
use std::error::Error;
|
||||
|
||||
pub trait Renderer {
|
||||
fn render(&self, book: &::book::MDBook) -> Result<(), Box<Error>>;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 348 KiB After Width: | Height: | Size: 348 KiB |
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;
|
||||
}
|
||||
@@ -1,767 +0,0 @@
|
||||
html,
|
||||
body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
.left {
|
||||
float: left;
|
||||
}
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
h2,
|
||||
h3 {
|
||||
margin-top: 2.5em;
|
||||
}
|
||||
h4,
|
||||
h5 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
.header + .header h3,
|
||||
.header + .header h4,
|
||||
.header + .header h5 {
|
||||
margin-top: 1em;
|
||||
}
|
||||
table {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table td {
|
||||
padding: 3px 20px;
|
||||
border: 1px solid;
|
||||
}
|
||||
table thead td {
|
||||
font-weight: 700;
|
||||
}
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 10px 10px;
|
||||
font-size: 0.875em;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-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) {
|
||||
.sidebar {
|
||||
left: -300px;
|
||||
}
|
||||
}
|
||||
.sidebar code {
|
||||
line-height: 2em;
|
||||
}
|
||||
.sidebar-hidden .sidebar {
|
||||
left: -300px;
|
||||
}
|
||||
.sidebar-visible .sidebar {
|
||||
left: 0;
|
||||
}
|
||||
.chapter {
|
||||
list-style: none outside none;
|
||||
padding-left: 0;
|
||||
line-height: 1.9em;
|
||||
}
|
||||
.chapter li a {
|
||||
padding: 5px 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.chapter li a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.chapter .spacer {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
.section {
|
||||
list-style: none outside none;
|
||||
padding-left: 20px;
|
||||
line-height: 2.5em;
|
||||
}
|
||||
.section li {
|
||||
-o-text-overflow: ellipsis;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.page-wrapper {
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
left: 315px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
-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;
|
||||
}
|
||||
@media only screen and (max-width: 1060px) {
|
||||
.page-wrapper {
|
||||
left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
.sidebar-hidden .page-wrapper {
|
||||
left: 15px;
|
||||
}
|
||||
.sidebar-visible .page-wrapper {
|
||||
left: 315px;
|
||||
}
|
||||
.page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding-right: 15px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.content {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 750px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
.content a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.content img {
|
||||
max-width: 100%;
|
||||
}
|
||||
.menu-bar {
|
||||
position: relative;
|
||||
height: 50px;
|
||||
}
|
||||
.menu-bar i {
|
||||
position: relative;
|
||||
margin: 0 10px;
|
||||
z-index: 10;
|
||||
line-height: 50px;
|
||||
-webkit-transition: color 0.5s;
|
||||
-moz-transition: color 0.5s;
|
||||
-o-transition: color 0.5s;
|
||||
-ms-transition: color 0.5s;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
.menu-bar i:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.menu-bar .left-buttons {
|
||||
float: left;
|
||||
}
|
||||
.menu-bar .right-buttons {
|
||||
float: right;
|
||||
}
|
||||
.menu-title {
|
||||
display: inline-block;
|
||||
font-weight: 200;
|
||||
font-size: 20px;
|
||||
line-height: 50px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
|
||||
filter: alpha(opacity=0);
|
||||
-webkit-transition: opacity 0.5s ease-in-out;
|
||||
-moz-transition: opacity 0.5s ease-in-out;
|
||||
-o-transition: opacity 0.5s ease-in-out;
|
||||
-ms-transition: opacity 0.5s ease-in-out;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
.menu-bar:hover .menu-title {
|
||||
opacity: 1;
|
||||
-ms-filter: none;
|
||||
filter: none;
|
||||
}
|
||||
.nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
position: absolute;
|
||||
top: 50px /* Height of menu-bar */;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
max-width: 150px;
|
||||
min-width: 90px;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: box;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-moz-box-pack: center;
|
||||
-o-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
-ms-flex-line-pack: center;
|
||||
-webkit-align-content: center;
|
||||
align-content: center;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
-o-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-transition: color 0.5s;
|
||||
-moz-transition: color 0.5s;
|
||||
-o-transition: color 0.5s;
|
||||
-ms-transition: color 0.5s;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
.mobile-nav-chapters {
|
||||
display: none;
|
||||
}
|
||||
.nav-chapters:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.previous {
|
||||
left: 0;
|
||||
}
|
||||
.next {
|
||||
right: 15px;
|
||||
}
|
||||
.theme-popup {
|
||||
position: relative;
|
||||
left: 10px;
|
||||
z-index: 1000;
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
.theme-popup .theme {
|
||||
margin: 0;
|
||||
padding: 2px 10px;
|
||||
line-height: 25px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.theme-popup .theme:hover:first-child {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
}
|
||||
.theme-popup .theme:hover:last-child {
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1250px) {
|
||||
.nav-chapters {
|
||||
display: none;
|
||||
}
|
||||
.mobile-nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
max-width: 150px;
|
||||
min-width: 90px;
|
||||
-webkit-box-pack: center;
|
||||
-moz-box-pack: center;
|
||||
-o-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
-ms-flex-line-pack: center;
|
||||
-webkit-align-content: center;
|
||||
align-content: center;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: 50px;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.next {
|
||||
float: right;
|
||||
}
|
||||
.previous {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
.light {
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
/* Inline code */
|
||||
}
|
||||
.light .content .header:link,
|
||||
.light .content .header:visited {
|
||||
color: #333;
|
||||
pointer: cursor;
|
||||
}
|
||||
.light .content .header:link:hover,
|
||||
.light .content .header:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.light .sidebar {
|
||||
background-color: #fafafa;
|
||||
color: #364149;
|
||||
}
|
||||
.light .chapter li {
|
||||
color: #aaa;
|
||||
}
|
||||
.light .chapter li a {
|
||||
color: #364149;
|
||||
}
|
||||
.light .chapter li .active,
|
||||
.light .chapter li a:hover {
|
||||
/* Animate color change */
|
||||
color: #008cff;
|
||||
}
|
||||
.light .chapter .spacer {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.light .menu-bar,
|
||||
.light .menu-bar:visited,
|
||||
.light .nav-chapters,
|
||||
.light .nav-chapters:visited,
|
||||
.light .mobile-nav-chapters,
|
||||
.light .mobile-nav-chapters:visited {
|
||||
color: #ccc;
|
||||
}
|
||||
.light .menu-bar i:hover,
|
||||
.light .nav-chapters:hover,
|
||||
.light .mobile-nav-chapters i:hover {
|
||||
color: #333;
|
||||
}
|
||||
.light .mobile-nav-chapters i:hover {
|
||||
color: #364149;
|
||||
}
|
||||
.light .mobile-nav-chapters {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.light .content a:link,
|
||||
.light a:visited {
|
||||
color: #4183c4;
|
||||
}
|
||||
.light .theme-popup {
|
||||
color: #333;
|
||||
background: #fafafa;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.light .theme-popup .theme:hover {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
.light .theme-popup .default {
|
||||
color: #ccc;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.light pre {
|
||||
position: relative;
|
||||
}
|
||||
.light pre > .buttons {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #364149;
|
||||
cursor: pointer;
|
||||
}
|
||||
.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;
|
||||
color: #a1adb8;
|
||||
}
|
||||
.coal .chapter li {
|
||||
color: #505254;
|
||||
}
|
||||
.coal .chapter li a {
|
||||
color: #a1adb8;
|
||||
}
|
||||
.coal .chapter li .active,
|
||||
.coal .chapter li a:hover {
|
||||
/* Animate color change */
|
||||
color: #3473ad;
|
||||
}
|
||||
.coal .chapter .spacer {
|
||||
background-color: #393939;
|
||||
}
|
||||
.coal .menu-bar,
|
||||
.coal .menu-bar:visited,
|
||||
.coal .nav-chapters,
|
||||
.coal .nav-chapters:visited,
|
||||
.coal .mobile-nav-chapters,
|
||||
.coal .mobile-nav-chapters:visited {
|
||||
color: #43484d;
|
||||
}
|
||||
.coal .menu-bar i:hover,
|
||||
.coal .nav-chapters:hover,
|
||||
.coal .mobile-nav-chapters i:hover {
|
||||
color: #b3c0cc;
|
||||
}
|
||||
.coal .mobile-nav-chapters i:hover {
|
||||
color: #a1adb8;
|
||||
}
|
||||
.coal .mobile-nav-chapters {
|
||||
background-color: #292c2f;
|
||||
}
|
||||
.coal .content a:link,
|
||||
.coal a:visited {
|
||||
color: #2b79a2;
|
||||
}
|
||||
.coal .theme-popup {
|
||||
color: #98a3ad;
|
||||
background: #141617;
|
||||
border: 1px solid #43484d;
|
||||
}
|
||||
.coal .theme-popup .theme:hover {
|
||||
background-color: #1f2124;
|
||||
}
|
||||
.coal .theme-popup .default {
|
||||
color: #43484d;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.coal pre {
|
||||
position: relative;
|
||||
}
|
||||
.coal pre > .buttons {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #a1adb8;
|
||||
cursor: pointer;
|
||||
}
|
||||
.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;
|
||||
color: #c8c9db;
|
||||
}
|
||||
.navy .chapter li {
|
||||
color: #505274;
|
||||
}
|
||||
.navy .chapter li a {
|
||||
color: #c8c9db;
|
||||
}
|
||||
.navy .chapter li .active,
|
||||
.navy .chapter li a:hover {
|
||||
/* Animate color change */
|
||||
color: #2b79a2;
|
||||
}
|
||||
.navy .chapter .spacer {
|
||||
background-color: #2d334f;
|
||||
}
|
||||
.navy .menu-bar,
|
||||
.navy .menu-bar:visited,
|
||||
.navy .nav-chapters,
|
||||
.navy .nav-chapters:visited,
|
||||
.navy .mobile-nav-chapters,
|
||||
.navy .mobile-nav-chapters:visited {
|
||||
color: #737480;
|
||||
}
|
||||
.navy .menu-bar i:hover,
|
||||
.navy .nav-chapters:hover,
|
||||
.navy .mobile-nav-chapters i:hover {
|
||||
color: #b7b9cc;
|
||||
}
|
||||
.navy .mobile-nav-chapters i:hover {
|
||||
color: #c8c9db;
|
||||
}
|
||||
.navy .mobile-nav-chapters {
|
||||
background-color: #282d3f;
|
||||
}
|
||||
.navy .content a:link,
|
||||
.navy a:visited {
|
||||
color: #2b79a2;
|
||||
}
|
||||
.navy .theme-popup {
|
||||
color: #bcbdd0;
|
||||
background: #161923;
|
||||
border: 1px solid #737480;
|
||||
}
|
||||
.navy .theme-popup .theme:hover {
|
||||
background-color: #282e40;
|
||||
}
|
||||
.navy .theme-popup .default {
|
||||
color: #737480;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.navy pre {
|
||||
position: relative;
|
||||
}
|
||||
.navy pre > .buttons {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #c8c9db;
|
||||
cursor: pointer;
|
||||
}
|
||||
.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;
|
||||
color: #c8c9db;
|
||||
}
|
||||
.rust .chapter li {
|
||||
color: #505254;
|
||||
}
|
||||
.rust .chapter li a {
|
||||
color: #c8c9db;
|
||||
}
|
||||
.rust .chapter li .active,
|
||||
.rust .chapter li a:hover {
|
||||
/* Animate color change */
|
||||
color: #e69f67;
|
||||
}
|
||||
.rust .chapter .spacer {
|
||||
background-color: #45373a;
|
||||
}
|
||||
.rust .menu-bar,
|
||||
.rust .menu-bar:visited,
|
||||
.rust .nav-chapters,
|
||||
.rust .nav-chapters:visited,
|
||||
.rust .mobile-nav-chapters,
|
||||
.rust .mobile-nav-chapters:visited {
|
||||
color: #737480;
|
||||
}
|
||||
.rust .menu-bar i:hover,
|
||||
.rust .nav-chapters:hover,
|
||||
.rust .mobile-nav-chapters i:hover {
|
||||
color: #262625;
|
||||
}
|
||||
.rust .mobile-nav-chapters i:hover {
|
||||
color: #c8c9db;
|
||||
}
|
||||
.rust .mobile-nav-chapters {
|
||||
background-color: #3b2e2a;
|
||||
}
|
||||
.rust .content a:link,
|
||||
.rust a:visited {
|
||||
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;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.rust pre {
|
||||
position: relative;
|
||||
}
|
||||
.rust pre > .buttons {
|
||||
position: absolute;
|
||||
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;
|
||||
}
|
||||
@@ -1,17 +1,131 @@
|
||||
$( document ).ready(function() {
|
||||
"use strict";
|
||||
|
||||
// url
|
||||
var url = window.location.pathname;
|
||||
// Fix back button cache problem
|
||||
window.onunload = function () { };
|
||||
|
||||
// Fix back button cache problem
|
||||
window.onunload = function(){};
|
||||
// Global variable, shared between modules
|
||||
function playpen_text(playpen) {
|
||||
let code_block = playpen.querySelector("code");
|
||||
|
||||
// Set theme
|
||||
var theme = localStorage.getItem('theme');
|
||||
if (theme === null) { theme = 'light'; }
|
||||
if (window.ace && code_block.classList.contains("editable")) {
|
||||
let editor = window.ace.edit(code_block);
|
||||
return editor.getValue();
|
||||
} else {
|
||||
return code_block.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
set_theme(theme);
|
||||
(function codeSnippets() {
|
||||
// Hide Rust code lines prepended with a specific character
|
||||
var hiding_character = "#";
|
||||
|
||||
function fetch_with_timeout(url, options, timeout = 6000) {
|
||||
return Promise.race([
|
||||
fetch(url, options),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
|
||||
]);
|
||||
}
|
||||
|
||||
var playpens = Array.from(document.querySelectorAll(".playpen"));
|
||||
if (playpens.length > 0) {
|
||||
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
// get list of crates available in the rust playground
|
||||
let playground_crates = response.crates.map(item => item["id"]);
|
||||
playpens.forEach(block => handle_crate_list_update(block, playground_crates));
|
||||
});
|
||||
}
|
||||
|
||||
function handle_crate_list_update(playpen_block, playground_crates) {
|
||||
// update the play buttons after receiving the response
|
||||
update_play_button(playpen_block, playground_crates);
|
||||
|
||||
// and install on change listener to dynamically update ACE editors
|
||||
if (window.ace) {
|
||||
let code_block = playpen_block.querySelector("code");
|
||||
if (code_block.classList.contains("editable")) {
|
||||
let editor = window.ace.edit(code_block);
|
||||
editor.addEventListener("change", function (e) {
|
||||
update_play_button(playpen_block, playground_crates);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updates the visibility of play button based on `no_run` class and
|
||||
// used crates vs ones available on http://play.rust-lang.org
|
||||
function update_play_button(pre_block, playground_crates) {
|
||||
var play_button = pre_block.querySelector(".play-button");
|
||||
|
||||
// skip if code is `no_run`
|
||||
if (pre_block.querySelector('code').classList.contains("no_run")) {
|
||||
play_button.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
// get list of `extern crate`'s from snippet
|
||||
var txt = playpen_text(pre_block);
|
||||
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
|
||||
var snippet_crates = [];
|
||||
var item;
|
||||
while (item = re.exec(txt)) {
|
||||
snippet_crates.push(item[1]);
|
||||
}
|
||||
|
||||
// check if all used crates are available on play.rust-lang.org
|
||||
var all_available = snippet_crates.every(function (elem) {
|
||||
return playground_crates.indexOf(elem) > -1;
|
||||
});
|
||||
|
||||
if (all_available) {
|
||||
play_button.classList.remove("hidden");
|
||||
} else {
|
||||
play_button.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function run_rust_code(code_block) {
|
||||
var result_block = code_block.querySelector(".result");
|
||||
if (!result_block) {
|
||||
result_block = document.createElement('code');
|
||||
result_block.className = 'result hljs language-bash';
|
||||
|
||||
code_block.append(result_block);
|
||||
}
|
||||
|
||||
let text = playpen_text(code_block);
|
||||
|
||||
var params = {
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
code: text
|
||||
};
|
||||
|
||||
if (text.indexOf("#![feature") !== -1) {
|
||||
params.version = "nightly";
|
||||
}
|
||||
|
||||
result_block.innerText = "Running...";
|
||||
|
||||
fetch_with_timeout("https://play.rust-lang.org/evaluate.json", {
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
body: JSON.stringify(params)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => result_block.innerText = response.result)
|
||||
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
|
||||
}
|
||||
|
||||
// Syntax highlighting Configuration
|
||||
hljs.configure({
|
||||
@@ -19,212 +133,468 @@ $( document ).ready(function() {
|
||||
languages: [], // Languages used for auto-detection
|
||||
});
|
||||
|
||||
$('code').each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
if (window.ace) {
|
||||
// language-rust class needs to be removed for editable
|
||||
// blocks or highlightjs will capture events
|
||||
Array
|
||||
.from(document.querySelectorAll('code.editable'))
|
||||
.forEach(function (block) { block.classList.remove('language-rust'); });
|
||||
|
||||
var KEY_CODES = {
|
||||
PREVIOUS_KEY: 37,
|
||||
NEXT_KEY: 39
|
||||
};
|
||||
|
||||
$(document).on('keydown', function (e) {
|
||||
switch (e.keyCode) {
|
||||
case KEY_CODES.NEXT_KEY:
|
||||
e.preventDefault();
|
||||
if($('.nav-chapters.next').length) {
|
||||
window.location.href = $('.nav-chapters.next').attr('href');
|
||||
}
|
||||
break;
|
||||
case KEY_CODES.PREVIOUS_KEY:
|
||||
e.preventDefault();
|
||||
if($('.nav-chapters.previous').length) {
|
||||
window.location.href = $('.nav-chapters.previous').attr('href');
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 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());
|
||||
// Add so that when you click the link actually shows up in the url bar...
|
||||
wrapper.attr("href", $(location).attr('href') + "#" + $(this).text());
|
||||
return wrapper;
|
||||
});
|
||||
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Scroll sidebar to current active section
|
||||
var activeSection = sidebar.find(".active");
|
||||
if(activeSection.length) {
|
||||
sidebar.scrollTop(activeSection.offset().top);
|
||||
Array
|
||||
.from(document.querySelectorAll('code:not(.editable)'))
|
||||
.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
} else {
|
||||
Array
|
||||
.from(document.querySelectorAll('code'))
|
||||
.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
}
|
||||
|
||||
// Adding the hljs class gives code blocks the color css
|
||||
// even if highlighting doesn't apply
|
||||
Array
|
||||
.from(document.querySelectorAll('code'))
|
||||
.forEach(function (block) { block.classList.add('hljs'); });
|
||||
|
||||
// Print button
|
||||
$("#print-button").click(function(){
|
||||
var printWindow = window.open("print.html");
|
||||
});
|
||||
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
|
||||
|
||||
if( url.substring(url.lastIndexOf('/')+1) == "print.html" ) {
|
||||
window.print();
|
||||
}
|
||||
|
||||
|
||||
// Theme button
|
||||
$("#theme-toggle").click(function(){
|
||||
if($('.theme-popup').length) {
|
||||
$('.theme-popup').remove();
|
||||
} else {
|
||||
var popup = $('<div class="theme-popup"></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>'));
|
||||
|
||||
|
||||
popup.insertAfter(this);
|
||||
|
||||
$('.theme').click(function(){
|
||||
var theme = $(this).attr('id');
|
||||
|
||||
set_theme(theme);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function set_theme(theme) {
|
||||
if (theme == 'coal' || theme == 'navy') {
|
||||
$("[href='tomorrow-night.css']").prop('disabled', false);
|
||||
$("[href='highlight.css']").prop('disabled', true);
|
||||
} else {
|
||||
$("[href='tomorrow-night.css']").prop('disabled', true);
|
||||
$("[href='highlight.css']").prop('disabled', false);
|
||||
}
|
||||
|
||||
localStorage.setItem('theme', theme);
|
||||
|
||||
$('body').removeClass().addClass(theme);
|
||||
}
|
||||
|
||||
|
||||
// Hide Rust code lines prepended with a specific character
|
||||
var hiding_character = "#";
|
||||
|
||||
$("code.language-rust").each(function(i, block){
|
||||
|
||||
var code_block = $(this);
|
||||
var pre_block = $(this).parent();
|
||||
var code_block = block;
|
||||
var pre_block = block.parentNode;
|
||||
// hide lines
|
||||
var lines = code_block.html().split("\n");
|
||||
var lines = code_block.innerHTML.split("\n");
|
||||
var first_non_hidden_line = false;
|
||||
var lines_hidden = false;
|
||||
var trimmed_line = "";
|
||||
|
||||
for(var n = 0; n < lines.length; n++){
|
||||
if($.trim(lines[n])[0] == hiding_character){
|
||||
if(first_non_hidden_line){
|
||||
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)#/, "$1") + "</span>";
|
||||
for (var n = 0; n < lines.length; n++) {
|
||||
trimmed_line = lines[n].trim();
|
||||
if (trimmed_line[0] == hiding_character && trimmed_line[1] != hiding_character) {
|
||||
if (first_non_hidden_line) {
|
||||
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)# ?/, "$1") + "</span>";
|
||||
}
|
||||
else {
|
||||
lines[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)#/, "$1") + "\n" + "</span>";
|
||||
lines[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)# ?/, "$1") + "\n" + "</span>";
|
||||
}
|
||||
lines_hidden = true;
|
||||
}
|
||||
else if(first_non_hidden_line) {
|
||||
else if (first_non_hidden_line) {
|
||||
lines[n] = "\n" + lines[n];
|
||||
}
|
||||
else {
|
||||
first_non_hidden_line = true;
|
||||
}
|
||||
if (trimmed_line[0] == hiding_character && trimmed_line[1] == hiding_character) {
|
||||
lines[n] = lines[n].replace("##", "#")
|
||||
}
|
||||
}
|
||||
code_block.html(lines.join(""));
|
||||
code_block.innerHTML = lines.join("");
|
||||
|
||||
// If no lines were hidden, return
|
||||
if(!lines_hidden) { return; }
|
||||
if (!lines_hidden) { return; }
|
||||
|
||||
var buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
buttons.innerHTML = "<button class=\"fa fa-expand\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
|
||||
|
||||
// add expand button
|
||||
pre_block.prepend("<div class=\"buttons\"><i class=\"fa fa-expand\"></i></div>");
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
|
||||
pre_block.find("i").click(function(e){
|
||||
if( $(this).hasClass("fa-expand") ) {
|
||||
$(this).removeClass("fa-expand").addClass("fa-compress");
|
||||
pre_block.find("span.hidden").removeClass("hidden").addClass("unhidden");
|
||||
}
|
||||
else {
|
||||
$(this).removeClass("fa-compress").addClass("fa-expand");
|
||||
pre_block.find("span.unhidden").removeClass("unhidden").addClass("hidden");
|
||||
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('fa-expand')) {
|
||||
var lines = pre_block.querySelectorAll('span.hidden');
|
||||
|
||||
e.target.classList.remove('fa-expand');
|
||||
e.target.classList.add('fa-compress');
|
||||
e.target.title = 'Hide lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
|
||||
Array.from(lines).forEach(function (line) {
|
||||
line.classList.remove('hidden');
|
||||
line.classList.add('unhidden');
|
||||
});
|
||||
} else if (e.target.classList.contains('fa-compress')) {
|
||||
var lines = pre_block.querySelectorAll('span.unhidden');
|
||||
|
||||
e.target.classList.remove('fa-compress');
|
||||
e.target.classList.add('fa-expand');
|
||||
e.target.title = 'Show hidden lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
|
||||
Array.from(lines).forEach(function (line) {
|
||||
line.classList.remove('unhidden');
|
||||
line.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
|
||||
var pre_block = block.parentNode;
|
||||
if (!pre_block.classList.contains('playpen')) {
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
var clipButton = document.createElement('button');
|
||||
clipButton.className = 'fa fa-copy clip-button';
|
||||
clipButton.title = 'Copy to clipboard';
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
|
||||
|
||||
buttons.insertBefore(clipButton, buttons.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
// Process playpen code blocks
|
||||
$(".playpen").each(function(block){
|
||||
var pre_block = $(this);
|
||||
Array.from(document.querySelectorAll(".playpen")).forEach(function (pre_block) {
|
||||
// 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");
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
buttons.prepend("<i class=\"fa fa-play play-button\"></i>");
|
||||
|
||||
buttons.find(".play-button").click(function(e){
|
||||
var runCodeButton = document.createElement('button');
|
||||
runCodeButton.className = 'fa fa-play play-button';
|
||||
runCodeButton.hidden = true;
|
||||
runCodeButton.title = 'Run this code';
|
||||
runCodeButton.setAttribute('aria-label', runCodeButton.title);
|
||||
|
||||
var copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
|
||||
buttons.insertBefore(runCodeButton, buttons.firstChild);
|
||||
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
|
||||
|
||||
runCodeButton.addEventListener('click', function (e) {
|
||||
run_rust_code(pre_block);
|
||||
});
|
||||
|
||||
let code_block = pre_block.querySelector("code");
|
||||
if (window.ace && code_block.classList.contains("editable")) {
|
||||
var undoChangesButton = document.createElement('button');
|
||||
undoChangesButton.className = 'fa fa-history reset-button';
|
||||
undoChangesButton.title = 'Undo changes';
|
||||
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
|
||||
|
||||
buttons.insertBefore(undoChangesButton, buttons.firstChild);
|
||||
|
||||
undoChangesButton.addEventListener('click', function () {
|
||||
let editor = window.ace.edit(code_block);
|
||||
editor.setValue(editor.originalCode);
|
||||
editor.clearSelection();
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function themes() {
|
||||
var html = document.querySelector('html');
|
||||
var themeToggleButton = document.getElementById('theme-toggle');
|
||||
var themePopup = document.getElementById('theme-list');
|
||||
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
|
||||
var stylesheets = {
|
||||
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
|
||||
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
|
||||
highlight: document.querySelector("[href$='highlight.css']"),
|
||||
};
|
||||
|
||||
function showThemes() {
|
||||
themePopup.style.display = 'block';
|
||||
themeToggleButton.setAttribute('aria-expanded', true);
|
||||
themePopup.querySelector("button#" + document.body.className).focus();
|
||||
}
|
||||
|
||||
function hideThemes() {
|
||||
themePopup.style.display = 'none';
|
||||
themeToggleButton.setAttribute('aria-expanded', false);
|
||||
themeToggleButton.focus();
|
||||
}
|
||||
|
||||
function set_theme(theme) {
|
||||
let ace_theme;
|
||||
|
||||
if (theme == 'coal' || theme == 'navy') {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = false;
|
||||
stylesheets.highlight.disabled = true;
|
||||
|
||||
ace_theme = "ace/theme/tomorrow_night";
|
||||
} else if (theme == 'ayu') {
|
||||
stylesheets.ayuHighlight.disabled = false;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = true;
|
||||
|
||||
ace_theme = "ace/theme/tomorrow_night";
|
||||
} else {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = false;
|
||||
|
||||
ace_theme = "ace/theme/dawn";
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor;
|
||||
}, 1);
|
||||
|
||||
if (window.ace && window.editors) {
|
||||
window.editors.forEach(function (editor) {
|
||||
editor.setTheme(ace_theme);
|
||||
});
|
||||
}
|
||||
|
||||
var previousTheme;
|
||||
try { previousTheme = localStorage.getItem('mdbook-theme'); } catch (e) { }
|
||||
if (previousTheme === null || previousTheme === undefined) { previousTheme = 'light'; }
|
||||
|
||||
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
|
||||
|
||||
document.body.className = theme;
|
||||
html.classList.remove(previousTheme);
|
||||
html.classList.add(theme);
|
||||
}
|
||||
|
||||
// Set theme
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||
|
||||
set_theme(theme);
|
||||
|
||||
themeToggleButton.addEventListener('click', function () {
|
||||
if (themePopup.style.display === 'block') {
|
||||
hideThemes();
|
||||
} else {
|
||||
showThemes();
|
||||
}
|
||||
});
|
||||
|
||||
themePopup.addEventListener('click', function (e) {
|
||||
var theme = e.target.id || e.target.parentElement.id;
|
||||
set_theme(theme);
|
||||
});
|
||||
|
||||
themePopup.addEventListener('focusout', function(e) {
|
||||
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
|
||||
if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang-nursery/mdBook/issues/628
|
||||
document.addEventListener('click', function(e) {
|
||||
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
if (!themePopup.contains(e.target)) { return; }
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
hideThemes();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
var li = document.activeElement.parentElement;
|
||||
if (li && li.previousElementSibling) {
|
||||
li.previousElementSibling.querySelector('button').focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
var li = document.activeElement.parentElement;
|
||||
if (li && li.nextElementSibling) {
|
||||
li.nextElementSibling.querySelector('button').focus();
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
themePopup.querySelector('li:first-child button').focus();
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
themePopup.querySelector('li:last-child button').focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function sidebar() {
|
||||
var html = document.querySelector("html");
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
var sidebarToggleButton = document.getElementById("sidebar-toggle");
|
||||
var firstContact = null;
|
||||
|
||||
function showSidebar() {
|
||||
html.classList.remove('sidebar-hidden')
|
||||
html.classList.add('sidebar-visible');
|
||||
Array.from(sidebarLinks).forEach(function (link) {
|
||||
link.setAttribute('tabIndex', 0);
|
||||
});
|
||||
sidebarToggleButton.setAttribute('aria-expanded', true);
|
||||
sidebar.setAttribute('aria-hidden', false);
|
||||
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
|
||||
}
|
||||
|
||||
function hideSidebar() {
|
||||
html.classList.remove('sidebar-visible')
|
||||
html.classList.add('sidebar-hidden');
|
||||
Array.from(sidebarLinks).forEach(function (link) {
|
||||
link.setAttribute('tabIndex', -1);
|
||||
});
|
||||
sidebarToggleButton.setAttribute('aria-expanded', false);
|
||||
sidebar.setAttribute('aria-hidden', true);
|
||||
try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { }
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
|
||||
if (html.classList.contains("sidebar-hidden")) {
|
||||
showSidebar();
|
||||
} else if (html.classList.contains("sidebar-visible")) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (getComputedStyle(sidebar)['transform'] === 'none') {
|
||||
hideSidebar();
|
||||
} else {
|
||||
showSidebar();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('touchstart', function (e) {
|
||||
firstContact = {
|
||||
x: e.touches[0].clientX,
|
||||
time: Date.now()
|
||||
};
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchmove', function (e) {
|
||||
if (!firstContact)
|
||||
return;
|
||||
|
||||
var curX = e.touches[0].clientX;
|
||||
var xDiff = curX - firstContact.x,
|
||||
tDiff = Date.now() - firstContact.time;
|
||||
|
||||
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
|
||||
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300))
|
||||
showSidebar();
|
||||
else if (xDiff < 0 && curX < 300)
|
||||
hideSidebar();
|
||||
|
||||
firstContact = null;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Scroll sidebar to current active section
|
||||
var activeSection = sidebar.querySelector(".active");
|
||||
if (activeSection) {
|
||||
sidebar.scrollTop = activeSection.offsetTop;
|
||||
}
|
||||
})();
|
||||
|
||||
(function chapterNavigation() {
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
if (window.search && window.search.hasFocus()) { return; }
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
var nextButton = document.querySelector('.nav-chapters.next');
|
||||
if (nextButton) {
|
||||
window.location.href = nextButton.href;
|
||||
}
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
var previousButton = document.querySelector('.nav-chapters.previous');
|
||||
if (previousButton) {
|
||||
window.location.href = previousButton.href;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function clipboard() {
|
||||
var clipButtons = document.querySelectorAll('.clip-button');
|
||||
|
||||
function hideTooltip(elem) {
|
||||
elem.firstChild.innerText = "";
|
||||
elem.className = 'fa fa-copy clip-button';
|
||||
}
|
||||
|
||||
function showTooltip(elem, msg) {
|
||||
elem.firstChild.innerText = msg;
|
||||
elem.className = 'fa fa-copy tooltipped';
|
||||
}
|
||||
|
||||
var clipboardSnippets = new Clipboard('.clip-button', {
|
||||
text: function (trigger) {
|
||||
hideTooltip(trigger);
|
||||
let playpen = trigger.closest("pre");
|
||||
return playpen_text(playpen);
|
||||
}
|
||||
});
|
||||
|
||||
Array.from(clipButtons).forEach(function (clipButton) {
|
||||
clipButton.addEventListener('mouseout', function (e) {
|
||||
hideTooltip(e.currentTarget);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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({version: "stable", optimize: "0", code: code_block.find(".language-rust").text() }),
|
||||
success: function(response){
|
||||
result_block.text(response.result);
|
||||
}
|
||||
clipboardSnippets.on('success', function (e) {
|
||||
e.clearSelection();
|
||||
showTooltip(e.trigger, "Copied!");
|
||||
});
|
||||
}
|
||||
|
||||
clipboardSnippets.on('error', function (e) {
|
||||
showTooltip(e.trigger, "Clipboard error!");
|
||||
});
|
||||
})();
|
||||
|
||||
(function scrollToTop () {
|
||||
var menuTitle = document.querySelector('.menu-title');
|
||||
|
||||
menuTitle.addEventListener('click', function () {
|
||||
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
})();
|
||||
|
||||
(function autoHideMenu() {
|
||||
var menu = document.getElementById('menu-bar');
|
||||
|
||||
var previousScrollTop = document.scrollingElement.scrollTop;
|
||||
|
||||
document.addEventListener('scroll', function () {
|
||||
if (menu.classList.contains('folded') && document.scrollingElement.scrollTop < previousScrollTop) {
|
||||
menu.classList.remove('folded');
|
||||
} else if (!menu.classList.contains('folded') && document.scrollingElement.scrollTop > previousScrollTop) {
|
||||
menu.classList.add('folded');
|
||||
}
|
||||
|
||||
if (!menu.classList.contains('bordered') && document.scrollingElement.scrollTop > 0) {
|
||||
menu.classList.add('bordered');
|
||||
}
|
||||
|
||||
if (menu.classList.contains('bordered') && document.scrollingElement.scrollTop === 0) {
|
||||
menu.classList.remove('bordered');
|
||||
}
|
||||
|
||||
previousScrollTop = document.scrollingElement.scrollTop;
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
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
417
src/theme/css/chrome.css
Normal file
417
src/theme/css/chrome.css
Normal file
@@ -0,0 +1,417 @@
|
||||
/* CSS for UI elements (a.k.a. chrome) */
|
||||
|
||||
@import 'variables.css';
|
||||
|
||||
::-webkit-scrollbar {
|
||||
background: var(--bg);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar);
|
||||
}
|
||||
|
||||
#searchresults a,
|
||||
.content a:link,
|
||||
a:visited,
|
||||
a > .hljs {
|
||||
color: var(--links);
|
||||
}
|
||||
|
||||
/* Menu Bar */
|
||||
|
||||
#menu-bar {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 101;
|
||||
margin: auto calc(0px - var(--page-padding));
|
||||
}
|
||||
#menu-bar > #menu-bar-sticky-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: var(--bg);
|
||||
border-bottom-color: var(--bg);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
.js #menu-bar > #menu-bar-sticky-container {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
#menu-bar.bordered > #menu-bar-sticky-container {
|
||||
border-bottom-color: var(--table-border-color);
|
||||
}
|
||||
#menu-bar i, #menu-bar .icon-button {
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
z-index: 10;
|
||||
line-height: 50px;
|
||||
cursor: pointer;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
@media only screen and (max-width: 420px) {
|
||||
#menu-bar i, #menu-bar .icon-button {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
.icon-button i {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#print-button {
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
|
||||
transform: translateY(-60px);
|
||||
}
|
||||
|
||||
.left-buttons {
|
||||
display: flex;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.no-js .left-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
display: inline-block;
|
||||
font-weight: 200;
|
||||
font-size: 20px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.js .menu-title {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-bar,
|
||||
.menu-bar:visited,
|
||||
.nav-chapters,
|
||||
.nav-chapters:visited,
|
||||
.mobile-nav-chapters,
|
||||
.mobile-nav-chapters:visited,
|
||||
.menu-bar .icon-button,
|
||||
.menu-bar a i {
|
||||
color: var(--icons);
|
||||
}
|
||||
|
||||
.menu-bar i:hover,
|
||||
.menu-bar .icon-button:hover,
|
||||
.nav-chapters:hover,
|
||||
.mobile-nav-chapters i:hover {
|
||||
color: var(--icons-hover);
|
||||
}
|
||||
|
||||
/* Nav Icons */
|
||||
|
||||
.nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
|
||||
position: fixed;
|
||||
top: 50px; /* Height of menu-bar */
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
max-width: 150px;
|
||||
min-width: 90px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.nav-chapters:hover { text-decoration: none; }
|
||||
|
||||
.nav-wrapper {
|
||||
margin-top: 50px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
width: 90px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
.previous {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.next {
|
||||
float: right;
|
||||
right: var(--page-padding);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1080px) {
|
||||
.nav-wide-wrapper { display: none; }
|
||||
.nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1380px) {
|
||||
.sidebar-visible .nav-wide-wrapper { display: none; }
|
||||
.sidebar-visible .nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
|
||||
:not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
color: var(--inline-code-color);
|
||||
}
|
||||
|
||||
a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
}
|
||||
pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
|
||||
color: var(--sidebar-fg);
|
||||
cursor: pointer;
|
||||
}
|
||||
pre > .buttons :hover {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
pre > .buttons i {
|
||||
margin-left: 8px;
|
||||
}
|
||||
pre > .buttons button {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: inherit;
|
||||
}
|
||||
pre > .result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
|
||||
#searchresults a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
mark {
|
||||
border-radius: 2px;
|
||||
padding: 0 3px 1px 3px;
|
||||
margin: 0 -3px -1px -3px;
|
||||
background-color: var(--search-mark-bg);
|
||||
transition: background-color 300ms linear;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
mark.fade-out {
|
||||
background-color: rgba(0,0,0,0) !important;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.searchbar-outer {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
|
||||
#searchbar {
|
||||
width: 100%;
|
||||
margin: 5px auto 0px auto;
|
||||
padding: 10px 16px;
|
||||
transition: box-shadow 300ms ease-in-out;
|
||||
border: 1px solid var(--searchbar-border-color);
|
||||
border-radius: 3px;
|
||||
background-color: var(--searchbar-bg);
|
||||
color: var(--searchbar-fg);
|
||||
}
|
||||
#searchbar:focus,
|
||||
#searchbar.active {
|
||||
box-shadow: 0 0 3px var(--searchbar-shadow-color);
|
||||
}
|
||||
|
||||
.searchresults-header {
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
padding: 18px 0 0 5px;
|
||||
color: var(--searchresults-header-fg);
|
||||
}
|
||||
|
||||
.searchresults-outer {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: var(--content-max-width);
|
||||
border-bottom: 1px dashed var(--searchresults-border-color);
|
||||
}
|
||||
|
||||
ul#searchresults {
|
||||
list-style: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
ul#searchresults li {
|
||||
margin: 10px 0px;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
ul#searchresults li.focus {
|
||||
background-color: var(--searchresults-li-bg);
|
||||
}
|
||||
ul#searchresults span.teaser {
|
||||
display: block;
|
||||
clear: both;
|
||||
margin: 5px 0 0 20px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
ul#searchresults span.teaser em {
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--sidebar-width);
|
||||
overflow-y: auto;
|
||||
padding: 10px 10px;
|
||||
font-size: 0.875em;
|
||||
box-sizing: border-box;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.js .sidebar {
|
||||
transition: transform 0.3s; /* Animation: slide away */
|
||||
}
|
||||
.sidebar code {
|
||||
line-height: 2em;
|
||||
}
|
||||
.sidebar-hidden .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width)));
|
||||
}
|
||||
.sidebar::-webkit-scrollbar {
|
||||
background: var(--sidebar-bg);
|
||||
}
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar);
|
||||
}
|
||||
|
||||
.sidebar-visible .page-wrapper {
|
||||
transform: translateX(var(--sidebar-width));
|
||||
}
|
||||
@media only screen and (min-width: 620px) {
|
||||
.sidebar-visible .page-wrapper {
|
||||
transform: none;
|
||||
margin-left: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
|
||||
.chapter {
|
||||
list-style: none outside none;
|
||||
padding-left: 0;
|
||||
line-height: 2.2em;
|
||||
}
|
||||
.chapter li {
|
||||
color: var(--sidebar-non-existant);
|
||||
}
|
||||
.chapter li a {
|
||||
color: var(--sidebar-fg);
|
||||
display: block;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.chapter li a:hover { text-decoration: none }
|
||||
.chapter li .active,
|
||||
a:hover {
|
||||
/* Animate color change */
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
.chapter .spacer {
|
||||
background-color: var(--sidebar-spacer);
|
||||
}
|
||||
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
.chapter li a { padding: 5px 0; }
|
||||
.spacer { margin: 10px 0; }
|
||||
}
|
||||
|
||||
.section {
|
||||
list-style: none outside none;
|
||||
padding-left: 20px;
|
||||
line-height: 1.9em;
|
||||
}
|
||||
|
||||
/* Theme Menu Popup */
|
||||
|
||||
.theme-popup {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50px;
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7em;
|
||||
color: var(--fg);
|
||||
background: var(--theme-popup-bg);
|
||||
border: 1px solid var(--theme-popup-border);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: none;
|
||||
}
|
||||
.theme-popup .default {
|
||||
color: var(--icons);
|
||||
}
|
||||
.theme-popup .theme {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 2px 10px;
|
||||
line-height: 25px;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
background: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
.theme-popup .theme:hover {
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
.theme-popup .theme:hover:first-child,
|
||||
.theme-popup .theme:hover:last-child {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
}
|
||||
144
src/theme/css/general.css
Normal file
144
src/theme/css/general.css
Normal file
@@ -0,0 +1,144 @@
|
||||
/* Base styles and content styles */
|
||||
|
||||
@import 'variables.css';
|
||||
|
||||
html {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
color: var(--fg);
|
||||
background-color: var(--bg);
|
||||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
|
||||
}
|
||||
|
||||
.left { float: left; }
|
||||
.right { float: right; }
|
||||
.hidden { display: none; }
|
||||
.play-button.hidden { display: none; }
|
||||
|
||||
h2, h3 { margin-top: 2.5em; }
|
||||
h4, h5 { margin-top: 2em; }
|
||||
|
||||
.header + .header h3,
|
||||
.header + .header h4,
|
||||
.header + .header h5 {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
a.header:target h1:before,
|
||||
a.header:target h2:before,
|
||||
a.header:target h3:before,
|
||||
a.header:target h4:before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-left: -30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.page {
|
||||
outline: 0;
|
||||
padding: 0 var(--page-padding);
|
||||
}
|
||||
.page-wrapper {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.js .page-wrapper {
|
||||
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
padding: 0 15px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
.content main {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
.content a { text-decoration: none; }
|
||||
.content a:hover { text-decoration: underline; }
|
||||
.content img { max-width: 100%; }
|
||||
.content .header:link,
|
||||
.content .header:visited {
|
||||
color: var(--fg);
|
||||
}
|
||||
.content .header:link,
|
||||
.content .header:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table td {
|
||||
padding: 3px 20px;
|
||||
border: 1px var(--table-border-color) solid;
|
||||
}
|
||||
table thead {
|
||||
background: var(--table-header-bg);
|
||||
}
|
||||
table thead td {
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
table thead tr {
|
||||
border: 1px var(--table-header-bg) solid;
|
||||
}
|
||||
/* Alternate background colors for rows */
|
||||
table tbody tr:nth-child(2n) {
|
||||
background: var(--table-alternate-bg);
|
||||
}
|
||||
|
||||
|
||||
blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
color: var(--fg);
|
||||
background-color: var(--quote-bg);
|
||||
border-top: .1em solid var(--quote-border);
|
||||
border-bottom: .1em solid var(--quote-border);
|
||||
}
|
||||
|
||||
|
||||
:not(.footnote-definition) + .footnote-definition,
|
||||
.footnote-definition + :not(.footnote-definition) {
|
||||
margin-top: 2em;
|
||||
}
|
||||
.footnote-definition {
|
||||
font-size: 0.9em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
.footnote-definition p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.tooltiptext {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
|
||||
left: -8px; /* Half of the width of the icon */
|
||||
top: -35px;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px 8px;
|
||||
margin: 5px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.tooltipped .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
54
src/theme/css/print.css
Normal file
54
src/theme/css/print.css
Normal file
@@ -0,0 +1,54 @@
|
||||
|
||||
#sidebar,
|
||||
#menu-bar,
|
||||
.nav-chapters,
|
||||
.mobile-nav-chapters {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#page-wrapper.page-wrapper {
|
||||
transform: none;
|
||||
margin-left: 0px;
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
#content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page {
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #666666;
|
||||
border-radius: 5px;
|
||||
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
|
||||
pre > .buttons {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
a, a:visited, a:active, a:hover {
|
||||
color: #4183c4;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
page-break-inside: avoid;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.fa {
|
||||
display: none !important;
|
||||
}
|
||||
210
src/theme/css/variables.css
Normal file
210
src/theme/css/variables.css
Normal file
@@ -0,0 +1,210 @@
|
||||
|
||||
/* Globals */
|
||||
|
||||
:root {
|
||||
--sidebar-width: 300px;
|
||||
--page-padding: 15px;
|
||||
--content-max-width: 750px;
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
|
||||
.ayu {
|
||||
--bg: hsl(210, 25%, 8%);
|
||||
--fg: #c5c5c5;
|
||||
|
||||
--sidebar-bg: #14191f;
|
||||
--sidebar-fg: #c8c9db;
|
||||
--sidebar-non-existant: #5c6773;
|
||||
--sidebar-active: #ffb454;
|
||||
--sidebar-spacer: #2d334f;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #737480;
|
||||
--icons-hover: #b7b9cc;
|
||||
|
||||
--links: #0096cf;
|
||||
|
||||
--inline-code-color: #ffb454;
|
||||
|
||||
--theme-popup-bg: #14191f;
|
||||
--theme-popup-border: #5c6773;
|
||||
--theme-hover: #191f26;
|
||||
|
||||
--quote-bg: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
|
||||
--table-border-color: hsl(210, 25%, 13%);
|
||||
--table-header-bg: hsl(210, 25%, 28%);
|
||||
--table-alternate-bg: hsl(210, 25%, 11%);
|
||||
|
||||
--searchbar-border-color: #848484;
|
||||
--searchbar-bg: #424242;
|
||||
--searchbar-fg: #fff;
|
||||
--searchbar-shadow-color: #d4c89f;
|
||||
--searchresults-header-fg: #666;
|
||||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #252932;
|
||||
--search-mark-bg: #e3b171;
|
||||
}
|
||||
|
||||
.coal {
|
||||
--bg: hsl(200, 7%, 8%);
|
||||
--fg: #98a3ad;
|
||||
|
||||
--sidebar-bg: #292c2f;
|
||||
--sidebar-fg: #a1adb8;
|
||||
--sidebar-non-existant: #505254;
|
||||
--sidebar-active: #3473ad;
|
||||
--sidebar-spacer: #393939;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #43484d;
|
||||
--icons-hover: #b3c0cc;
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;;
|
||||
|
||||
--theme-popup-bg: #141617;
|
||||
--theme-popup-border: #43484d;
|
||||
--theme-hover: #1f2124;
|
||||
|
||||
--quote-bg: hsl(234, 21%, 18%);
|
||||
--quote-border: hsl(234, 21%, 23%);
|
||||
|
||||
--table-border-color: hsl(200, 7%, 13%);
|
||||
--table-header-bg: hsl(200, 7%, 28%);
|
||||
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||
|
||||
--searchbar-border-color: #aaa;
|
||||
--searchbar-bg: #b7b7b7;
|
||||
--searchbar-fg: #000;
|
||||
--searchbar-shadow-color: #aaa;
|
||||
--searchresults-header-fg: #666;
|
||||
--searchresults-border-color: #98a3ad;
|
||||
--searchresults-li-bg: #2b2b2f;
|
||||
--search-mark-bg: #355c7d;
|
||||
}
|
||||
|
||||
.light {
|
||||
--bg: hsl(0, 0%, 100%);
|
||||
--fg: #333333;
|
||||
|
||||
--sidebar-bg: #fafafa;
|
||||
--sidebar-fg: #364149;
|
||||
--sidebar-non-existant: #aaaaaa;
|
||||
--sidebar-active: #008cff;
|
||||
--sidebar-spacer: #f4f4f4;
|
||||
|
||||
--scrollbar: #cccccc;
|
||||
|
||||
--icons: #cccccc;
|
||||
--icons-hover: #333333;
|
||||
|
||||
--links: #4183c4;
|
||||
|
||||
--inline-code-color: #6e6b5e;
|
||||
|
||||
--theme-popup-bg: #fafafa;
|
||||
--theme-popup-border: #cccccc;
|
||||
--theme-hover: #e6e6e6;
|
||||
|
||||
--quote-bg: hsl(197, 37%, 96%);
|
||||
--quote-border: hsl(197, 37%, 91%);
|
||||
|
||||
--table-border-color: hsl(0, 0%, 95%);
|
||||
--table-header-bg: hsl(0, 0%, 80%);
|
||||
--table-alternate-bg: hsl(0, 0%, 97%);
|
||||
|
||||
--searchbar-border-color: #aaa;
|
||||
--searchbar-bg: #fafafa;
|
||||
--searchbar-fg: #000;
|
||||
--searchbar-shadow-color: #aaa;
|
||||
--searchresults-header-fg: #666;
|
||||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #e4f2fe;
|
||||
--search-mark-bg: #a2cff5;
|
||||
}
|
||||
|
||||
.navy {
|
||||
--bg: hsl(226, 23%, 11%);
|
||||
--fg: #bcbdd0;
|
||||
|
||||
--sidebar-bg: #282d3f;
|
||||
--sidebar-fg: #c8c9db;
|
||||
--sidebar-non-existant: #505274;
|
||||
--sidebar-active: #2b79a2;
|
||||
--sidebar-spacer: #2d334f;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #737480;
|
||||
--icons-hover: #b7b9cc;
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;;
|
||||
|
||||
--theme-popup-bg: #161923;
|
||||
--theme-popup-border: #737480;
|
||||
--theme-hover: #282e40;
|
||||
|
||||
--quote-bg: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
|
||||
--table-border-color: hsl(226, 23%, 16%);
|
||||
--table-header-bg: hsl(226, 23%, 31%);
|
||||
--table-alternate-bg: hsl(226, 23%, 14%);
|
||||
|
||||
--searchbar-border-color: #aaa;
|
||||
--searchbar-bg: #aeaec6;
|
||||
--searchbar-fg: #000;
|
||||
--searchbar-shadow-color: #aaa;
|
||||
--searchresults-header-fg: #5f5f71;
|
||||
--searchresults-border-color: #5c5c68;
|
||||
--searchresults-li-bg: #242430;
|
||||
--search-mark-bg: #a2cff5;
|
||||
}
|
||||
|
||||
.rust {
|
||||
--bg: hsl(60, 9%, 87%);
|
||||
--fg: #262625;
|
||||
|
||||
--sidebar-bg: #3b2e2a;
|
||||
--sidebar-fg: #c8c9db;
|
||||
--sidebar-non-existant: #505254;
|
||||
--sidebar-active: #e69f67;
|
||||
--sidebar-spacer: #45373a;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #737480;
|
||||
--icons-hover: #262625;
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #6e6b5e;
|
||||
|
||||
--theme-popup-bg: #e1e1db;
|
||||
--theme-popup-border: #b38f6b;
|
||||
--theme-hover: #99908a;
|
||||
|
||||
--quote-bg: hsl(60, 5%, 75%);
|
||||
--quote-border: hsl(60, 5%, 70%);
|
||||
|
||||
--table-border-color: hsl(60, 9%, 82%);
|
||||
--table-header-bg: #b3a497;
|
||||
--table-alternate-bg: hsl(60, 9%, 84%);
|
||||
|
||||
--searchbar-border-color: #aaa;
|
||||
--searchbar-bg: #fafafa;
|
||||
--searchbar-fg: #000;
|
||||
--searchbar-shadow-color: #aaa;
|
||||
--searchresults-header-fg: #666;
|
||||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #dec2a2;
|
||||
--search-mark-bg: #e69f67;
|
||||
}
|
||||
1
src/theme/header.hbs
Normal file
1
src/theme/header.hbs
Normal file
@@ -0,0 +1 @@
|
||||
{{!-- Put your header HTML text here --}}
|
||||
@@ -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
@@ -1,113 +1,260 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}">
|
||||
<html lang="{{ language }}" class="sidebar-visible no-js">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="description" content="{{ description }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
<base href="{{ path_to_root }}">
|
||||
<link rel="shortcut icon" href="{{ path_to_root }}{{ favicon }}">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
|
||||
|
||||
<link rel="stylesheet" href="book.css">
|
||||
<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'>
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
|
||||
|
||||
<link rel="shortcut icon" href="{{ favicon }}">
|
||||
<!-- Highlight.js Stylesheets -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}highlight.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}tomorrow-night.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}ayu-highlight.css">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="http://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">
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ ../path_to_root }}{{ 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>
|
||||
|
||||
<!-- Fetch JQuery from CDN but have a local fallback -->
|
||||
<script src="http://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>
|
||||
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{{/if}}
|
||||
</head>
|
||||
<body>
|
||||
<body class="light">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script type="text/javascript">var path_to_root = "{{ path_to_root }}";</script>
|
||||
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script type="text/javascript">
|
||||
try {
|
||||
var theme = localStorage.getItem('mdbook-theme');
|
||||
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||
|
||||
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||
}
|
||||
|
||||
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||
}
|
||||
} catch (e) { }
|
||||
</script>
|
||||
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
<script type="text/javascript">
|
||||
var theme = localStorage.getItem('theme');
|
||||
if (theme == null) { theme = 'light'; }
|
||||
$('body').removeClass().addClass(theme);
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||
document.body.className = theme;
|
||||
document.querySelector('html').className = theme + ' js';
|
||||
</script>
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script type="text/javascript">
|
||||
var sidebar = localStorage.getItem('sidebar');
|
||||
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
|
||||
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
|
||||
var html = document.querySelector('html');
|
||||
var sidebar = 'hidden';
|
||||
if (document.body.clientWidth >= 1080) {
|
||||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||
sidebar = sidebar || 'visible';
|
||||
}
|
||||
html.classList.remove('sidebar-visible');
|
||||
html.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<div id="sidebar" class="sidebar">
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
{{#toc}}{{/toc}}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page">
|
||||
{{> header}}
|
||||
<div id="menu-bar" class="menu-bar">
|
||||
<div class="left-buttons">
|
||||
<i id="sidebar-toggle" class="fa fa-bars"></i>
|
||||
<i id="theme-toggle" class="fa fa-paint-brush"></i>
|
||||
</div>
|
||||
<div id="menu-bar-sticky-container">
|
||||
<div class="left-buttons">
|
||||
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
</button>
|
||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="light">Light <span class="default">(default)</span></button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||||
</ul>
|
||||
{{#if search_enabled}}
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<h1 class="menu-title">{{ title }}</h1>
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
|
||||
<div class="right-buttons">
|
||||
<i id="print-button" class="fa fa-print" title="Print this book"></i>
|
||||
<div class="right-buttons">
|
||||
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
|
||||
<i id="print-button" class="fa fa-print"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if search_enabled}}
|
||||
<div id="search-wrapper" class="hidden">
|
||||
<form id="searchbar-outer" class="searchbar-outer">
|
||||
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
</form>
|
||||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||
<div id="searchresults-header" class="searchresults-header"></div>
|
||||
<ul id="searchresults">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||
<script type="text/javascript">
|
||||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="content" class="content">
|
||||
{{{ content }}}
|
||||
</div>
|
||||
<main>
|
||||
{{{ content }}}
|
||||
</main>
|
||||
|
||||
<!-- Mobile navigation buttons -->
|
||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||
<!-- Mobile navigation buttons -->
|
||||
{{#previous}}
|
||||
<a rel="prev" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
|
||||
{{#next}}
|
||||
<a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
{{/next}}
|
||||
|
||||
<div style="clear: both"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="mobile-nav-chapters previous">
|
||||
<a href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
|
||||
{{#next}}
|
||||
<a href="{{link}}" class="mobile-nav-chapters next">
|
||||
<a href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
{{/next}}
|
||||
|
||||
</div>
|
||||
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="nav-chapters previous" title="You can navigate through the chapters using the arrow keys">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
|
||||
{{#next}}
|
||||
<a href="{{link}}" class="nav-chapters next" title="You can navigate through the chapters using the arrow keys">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
{{/next}}
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
{{#if livereload}}
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
<script type="text/javascript">
|
||||
var socket = new WebSocket("{{{livereload}}}");
|
||||
socket.onmessage = function (event) {
|
||||
if (event.data === "reload") {
|
||||
socket.close();
|
||||
location.reload(true); // force reload from server (not from cache)
|
||||
}
|
||||
};
|
||||
|
||||
<!-- Local fallback for Font Awesome -->
|
||||
<script>
|
||||
if ($(".fa").css("font-family") !== "FontAwesome") {
|
||||
$('<link rel="stylesheet" type="text/css" href="_FontAwesome/css/font-awesome.css">').prependTo('head');
|
||||
window.onbeforeunload = function() {
|
||||
socket.close();
|
||||
}
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if google_analytics}}
|
||||
<!-- Google Analytics Tag -->
|
||||
<script type="text/javascript">
|
||||
var localAddrs = ["localhost", "127.0.0.1", ""];
|
||||
|
||||
// make sure we don't activate google analytics if the developer is
|
||||
// inspecting the book locally...
|
||||
if (localAddrs.indexOf(document.location.hostname) === -1) {
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', '{{google_analytics}}', 'auto');
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playpen_js}}
|
||||
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
|
||||
{{/if}}
|
||||
|
||||
{{#if search_js}}
|
||||
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||
{{/if}}
|
||||
|
||||
<script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script>
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
{{#each additional_js}}
|
||||
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
{{#if is_print}}
|
||||
{{#if mathjax_support}}
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', function() {
|
||||
MathJax.Hub.Register.StartupHook('End', function() {
|
||||
window.setTimeout(window.print, 100);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{else}}
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', function() {
|
||||
window.setTimeout(window.print, 100);
|
||||
});
|
||||
</script>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<script src="highlight.js"></script>
|
||||
<script src="book.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
4
src/theme/jquery-2.1.4.min.js
vendored
4
src/theme/jquery-2.1.4.min.js
vendored
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user