mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 15:01:45 -05:00
Compare commits
555 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92afe9bd3c | ||
|
|
4c1aca0abb | ||
|
|
da166e051d | ||
|
|
6a4ba95926 | ||
|
|
6688bd8d7b | ||
|
|
01313a39cc | ||
|
|
f92911b8aa | ||
|
|
42d6fd5804 | ||
|
|
a8a45a5fbe | ||
|
|
11781f0c1b | ||
|
|
53055e0345 | ||
|
|
1af6d4b0ec | ||
|
|
3e311dc975 | ||
|
|
04e31eb07b | ||
|
|
eb82ddca0b | ||
|
|
1d2b720ebe | ||
|
|
4c303c3b1d | ||
|
|
42129c6181 | ||
|
|
a10a57e67d | ||
|
|
fa5f32c7fd | ||
|
|
a91e888575 | ||
|
|
8571883923 | ||
|
|
4cf005d4bd | ||
|
|
b38792c166 | ||
|
|
248863addf | ||
|
|
7e2752e71f | ||
|
|
cbf0ca027d | ||
|
|
2c2ba636a9 | ||
|
|
494e6722b2 | ||
|
|
ddf71222c5 | ||
|
|
1d89127d8f | ||
|
|
5f00625c14 | ||
|
|
000a93dc77 | ||
|
|
1f8c090a5f | ||
|
|
0547868d4d | ||
|
|
1056b8361c | ||
|
|
a5f861bf2b | ||
|
|
93aee6419e | ||
|
|
4b1a7e9ae7 | ||
|
|
2f5e89f3ec | ||
|
|
2b903ad057 | ||
|
|
fb397e6fa0 | ||
|
|
00a55b35a8 | ||
|
|
d65ce55453 | ||
|
|
37d756ae75 | ||
|
|
f8782666ba | ||
|
|
c74c682939 | ||
|
|
8b49600673 | ||
|
|
29c729fd23 | ||
|
|
5d65967448 | ||
|
|
bf258eeb9b | ||
|
|
af6237015a | ||
|
|
a462fb63c3 | ||
|
|
f3332fb0da | ||
|
|
5bea83114b | ||
|
|
fe8bb38ec1 | ||
|
|
a60571321a | ||
|
|
e1c2e1a753 | ||
|
|
800dbf2929 | ||
|
|
1880447dce | ||
|
|
268dbb099f | ||
|
|
ae275ad1b1 | ||
|
|
ceff050bb4 | ||
|
|
8357811d96 | ||
|
|
860a17d85a | ||
|
|
ba324cddb6 | ||
|
|
a5dcd78393 | ||
|
|
8e1195322a | ||
|
|
2a2b51c8ab | ||
|
|
eb5ec2a314 | ||
|
|
445529a68f | ||
|
|
981b79b3b3 | ||
|
|
78bcda02cb | ||
|
|
cdfa5ad990 | ||
|
|
720f502e9d | ||
|
|
adf6129769 | ||
|
|
a5fddfa468 | ||
|
|
7c37dd5e85 | ||
|
|
857ca19fe4 | ||
|
|
a19d91ef37 | ||
|
|
ac8526688a | ||
|
|
1e1c99bbdb | ||
|
|
0c89293029 | ||
|
|
39eb78c88b | ||
|
|
372842aac6 | ||
|
|
7934e06668 | ||
|
|
44f982f8e5 | ||
|
|
1f04a62648 | ||
|
|
675c8c3f4e | ||
|
|
97cb77bbdd | ||
|
|
46345b8e49 | ||
|
|
566451e9a7 | ||
|
|
15626294b0 | ||
|
|
6cab04554e | ||
|
|
2ae7f007cc | ||
|
|
89e37a7751 | ||
|
|
0dca4d9b9f | ||
|
|
b85c3035fe | ||
|
|
6899d94027 | ||
|
|
fa0f9df497 | ||
|
|
917df6e97d | ||
|
|
5921f59817 | ||
|
|
50bad7f983 | ||
|
|
972c61fa76 | ||
|
|
b73d02fb8c | ||
|
|
6c4974b5c6 | ||
|
|
81d661c4f1 | ||
|
|
2213312938 | ||
|
|
4ae7ab5e87 | ||
|
|
59569984e2 | ||
|
|
89b580ab52 | ||
|
|
85df785cd3 | ||
|
|
fde88c22a8 | ||
|
|
0ec4b692f4 | ||
|
|
c5c8f1a6d3 | ||
|
|
336640633b | ||
|
|
4206739492 | ||
|
|
9e6217871e | ||
|
|
7b1241d0f2 | ||
|
|
9acc0debec | ||
|
|
a226de38b6 | ||
|
|
f5b0b1934a | ||
|
|
64838ce07d | ||
|
|
526a0394b0 | ||
|
|
6ed570940d | ||
|
|
b0511f408d | ||
|
|
68a5c09fdf | ||
|
|
97b6a35afc | ||
|
|
1cacef025d | ||
|
|
ddb0d2396f | ||
|
|
f2fba30786 | ||
|
|
f3e5fce6bf | ||
|
|
2d36cd9263 | ||
|
|
09087097b5 | ||
|
|
18b9f42fba | ||
|
|
b38949a408 | ||
|
|
c32869cf10 | ||
|
|
e3e170715e | ||
|
|
a387f482d8 | ||
|
|
4e03818f3e | ||
|
|
3f268cb0df | ||
|
|
25829926e5 | ||
|
|
c828002b70 | ||
|
|
cdbfb4a5b9 | ||
|
|
191dccab10 | ||
|
|
5eb7d46a99 | ||
|
|
dffcedf031 | ||
|
|
c9b6be8660 | ||
|
|
23af80c506 | ||
|
|
857acb9759 | ||
|
|
2ddcb43899 | ||
|
|
1c0983b811 | ||
|
|
1be69af553 | ||
|
|
c63000f365 | ||
|
|
bbaa0ea1fa | ||
|
|
58bc92d380 | ||
|
|
17d1ed3716 | ||
|
|
8df8ce063d | ||
|
|
c3ff4a5129 | ||
|
|
4d20fa578b | ||
|
|
9e47498458 | ||
|
|
903469a45f | ||
|
|
b8ef89db62 | ||
|
|
c283211a37 | ||
|
|
d5af051d0e | ||
|
|
68f9afe64b | ||
|
|
ffa8284743 | ||
|
|
3e91f9cd5d | ||
|
|
f55028b61a | ||
|
|
0d887505af | ||
|
|
6c20736a55 | ||
|
|
e4a46c9477 | ||
|
|
6ae5c686d9 | ||
|
|
b862080006 | ||
|
|
6b790b83ec | ||
|
|
d8ad68c947 | ||
|
|
6b784be616 | ||
|
|
f5598b2eee | ||
|
|
ff4b8e7a8d | ||
|
|
9c34e602bd | ||
|
|
a306da3ad7 | ||
|
|
9bede85efa | ||
|
|
11b1e86187 | ||
|
|
10d30a2dc0 | ||
|
|
601ebc5499 | ||
|
|
4251d7a838 | ||
|
|
93008cf20b | ||
|
|
3f9f681b9e | ||
|
|
63680d0786 | ||
|
|
656a1825cc | ||
|
|
1a2fa29209 | ||
|
|
6be81214b1 | ||
|
|
d22299d998 | ||
|
|
0af417085f | ||
|
|
9634798eb7 | ||
|
|
2a8af1c21d | ||
|
|
981f8695ff | ||
|
|
48b5e52f62 | ||
|
|
c4fec94c4c | ||
|
|
ab0c338c08 | ||
|
|
8a82f6336a | ||
|
|
1700783594 | ||
|
|
e6629cd75b | ||
|
|
5a077b9ff4 | ||
|
|
8b4e488de1 | ||
|
|
68d8ceec47 | ||
|
|
db337d4a6f | ||
|
|
5e277140be | ||
|
|
14add9c290 | ||
|
|
87877a9dae | ||
|
|
2cf00d0880 | ||
|
|
8c7af3c767 | ||
|
|
6dd785ea6c | ||
|
|
8d131b4310 | ||
|
|
97b38063b1 | ||
|
|
d23734f82e | ||
|
|
2ccfaadd1d | ||
|
|
3d04e5c7ff | ||
|
|
49ef7b6f02 | ||
|
|
da7026190c | ||
|
|
92377013cc | ||
|
|
34b586ab32 | ||
|
|
a79065b0d3 | ||
|
|
b3ab93a4b3 | ||
|
|
49b75810fa | ||
|
|
b85d5eb455 | ||
|
|
27faa54ae8 | ||
|
|
fae0759626 | ||
|
|
cc74ca2e6e | ||
|
|
7cae3a058d | ||
|
|
8fb6ac7987 | ||
|
|
82d32ee761 | ||
|
|
fe9b534ad7 | ||
|
|
56652e8fa6 | ||
|
|
c3a1e41ed7 | ||
|
|
3976c9d8f0 | ||
|
|
96b6f02834 | ||
|
|
b571511737 | ||
|
|
ebdab38a32 | ||
|
|
c06f450e7d | ||
|
|
b87c231fc3 | ||
|
|
8024b08f93 | ||
|
|
8ec0bf6e30 | ||
|
|
a8926d5392 | ||
|
|
00473d8420 | ||
|
|
86d390032b | ||
|
|
b3c0b01350 | ||
|
|
e33192753d | ||
|
|
7932e13512 | ||
|
|
9fd2509c0d | ||
|
|
5dec8508c7 | ||
|
|
4d2dc6f482 | ||
|
|
efb13d7bc1 | ||
|
|
27b1e05c87 | ||
|
|
e440094b37 | ||
|
|
15cae10ca8 | ||
|
|
dc2062ab36 | ||
|
|
d9ce98d710 | ||
|
|
b59aab56f2 | ||
|
|
b899c48019 | ||
|
|
515a253e97 | ||
|
|
7ddc3df945 | ||
|
|
2f7293af5c | ||
|
|
fa3ae53d46 | ||
|
|
6425c29893 | ||
|
|
d0bb830491 | ||
|
|
d325c601bb | ||
|
|
e9e889f523 | ||
|
|
e5e10c681a | ||
|
|
05edc4421b | ||
|
|
22ea5fe335 | ||
|
|
714c5fb81e | ||
|
|
56ceb627b8 | ||
|
|
c1b2bec7d7 | ||
|
|
8201b411ab | ||
|
|
836546cf0d | ||
|
|
9813802b3e | ||
|
|
fcf8f938d2 | ||
|
|
60aaa7ae31 | ||
|
|
1b584d1746 | ||
|
|
aa4cb9465f | ||
|
|
89a2e39b80 | ||
|
|
3c2b8cd10f | ||
|
|
6b0b42ebcc | ||
|
|
7a3513200f | ||
|
|
3db0c0b9a1 | ||
|
|
2c7aac6d7a | ||
|
|
3ee22fb430 | ||
|
|
16c5ec4d74 | ||
|
|
7e7e779ef7 | ||
|
|
b364e8ea2c | ||
|
|
78325aaccb | ||
|
|
1411ea967a | ||
|
|
d147a85006 | ||
|
|
0f0dce8d6c | ||
|
|
379574dc61 | ||
|
|
6a7de13c6f | ||
|
|
331aad1597 | ||
|
|
7e01cf9e18 | ||
|
|
c922b8aae6 | ||
|
|
b21446898a | ||
|
|
f4b4a331d7 | ||
|
|
aa349e0b7c | ||
|
|
b592b10633 | ||
|
|
d62cf8e883 | ||
|
|
c6844dd771 | ||
|
|
009247be01 | ||
|
|
84b3b7218e | ||
|
|
71ba6c9eb8 | ||
|
|
9d4ee689db | ||
|
|
ffe88d7e29 | ||
|
|
9f930706bb | ||
|
|
24fa615149 | ||
|
|
a72d6002b7 | ||
|
|
5b7abf4714 | ||
|
|
d0ef70e574 | ||
|
|
7525b35383 | ||
|
|
b54e73e3b6 | ||
|
|
59c76fa665 | ||
|
|
c1d982d92b | ||
|
|
3db275d68a | ||
|
|
94e797fba0 | ||
|
|
c3beecc96a | ||
|
|
7aff98a859 | ||
|
|
bbf54d7459 | ||
|
|
dcc642e66d | ||
|
|
2b738d4425 | ||
|
|
b3670ece0e | ||
|
|
30ce7e79ac | ||
|
|
94f7578576 | ||
|
|
e6568a70eb | ||
|
|
0eb23efd44 | ||
|
|
e78a8471c7 | ||
|
|
dcccd3289d | ||
|
|
5637a66459 | ||
|
|
536873ca26 | ||
|
|
d6ea4e3f7a | ||
|
|
fcceee4761 | ||
|
|
3f39ba82f9 | ||
|
|
7da38715c1 | ||
|
|
c83bbd6319 | ||
|
|
fad3c663f4 | ||
|
|
f8b9054265 | ||
|
|
f26116a491 | ||
|
|
7f59fdd9bd | ||
|
|
45d41eac5f | ||
|
|
2b5890e2ed | ||
|
|
0b9570b160 | ||
|
|
90396c5b76 | ||
|
|
24b76dd879 | ||
|
|
9a9eb0124a | ||
|
|
257374d76b | ||
|
|
1a0c296532 | ||
|
|
9b4ab72a80 | ||
|
|
b1c2e466e7 | ||
|
|
cdea0f6b61 | ||
|
|
e9b0be7090 | ||
|
|
d402a12e88 | ||
|
|
218e200117 | ||
|
|
3d55375f61 | ||
|
|
77e7cfd22b | ||
|
|
76cd39e5e2 | ||
|
|
09e7bb76dc | ||
|
|
28387130c0 | ||
|
|
33d3d9c3ec | ||
|
|
beec17e55d | ||
|
|
e651f4d734 | ||
|
|
87d2cd9845 | ||
|
|
32abeef088 | ||
|
|
5de9b6841e | ||
|
|
95e0743bc0 | ||
|
|
3c97525743 | ||
|
|
9a65c8ab92 | ||
|
|
a64a7b7470 | ||
|
|
fd4137a9ea | ||
|
|
a3d4febe3e | ||
|
|
7af4b1dfe8 | ||
|
|
ba6bffac5a | ||
|
|
6201e577fe | ||
|
|
cf2459f730 | ||
|
|
45a481049e | ||
|
|
6bcabcbb6b | ||
|
|
ef993e8cc2 | ||
|
|
a3a5386da0 | ||
|
|
3ab911afa1 | ||
|
|
4615ce2f8c | ||
|
|
7cb8087469 | ||
|
|
d1721667b6 | ||
|
|
1038f0b7f5 | ||
|
|
942cc12a74 | ||
|
|
59f2a9bf4e | ||
|
|
75d0f1efd4 | ||
|
|
552e3378cf | ||
|
|
7c0ddff96a | ||
|
|
07e72757d3 | ||
|
|
58f66a146d | ||
|
|
643d5ecc5c | ||
|
|
c712ba7aab | ||
|
|
1450070f73 | ||
|
|
e310dfc605 | ||
|
|
cbfd75a821 | ||
|
|
eaa6914205 | ||
|
|
a76557a678 | ||
|
|
01836ba5d4 | ||
|
|
46ce077de6 | ||
|
|
f7c9180d80 | ||
|
|
9e9cf49c50 | ||
|
|
4c951d530d | ||
|
|
780fb979a0 | ||
|
|
b77942d3c8 | ||
|
|
d0deee90b0 | ||
|
|
e6ac8ecdd9 | ||
|
|
d1f5ecc103 | ||
|
|
e0b247e9d6 | ||
|
|
db8a2821ea | ||
|
|
39d7130019 | ||
|
|
2eccb457d2 | ||
|
|
d1682d27fb | ||
|
|
a94a940ff7 | ||
|
|
daf402e1dc | ||
|
|
5ebd2c0527 | ||
|
|
b349e8abc9 | ||
|
|
e225586953 | ||
|
|
cf7663f800 | ||
|
|
3155c63e88 | ||
|
|
4df9ec90af | ||
|
|
73cabeb904 | ||
|
|
4b773024ae | ||
|
|
33ea661350 | ||
|
|
1b18740b56 | ||
|
|
6fed9e52f9 | ||
|
|
fd59dc73e5 | ||
|
|
146bea48c6 | ||
|
|
efb5bc285d | ||
|
|
5ea8e55aea | ||
|
|
1acf23ff73 | ||
|
|
69cc1fa005 | ||
|
|
2fb489137b | ||
|
|
4d9eb9b4b4 | ||
|
|
f6768b816c | ||
|
|
8f7e030ac3 | ||
|
|
9180dd1659 | ||
|
|
9278b838a8 | ||
|
|
2674347768 | ||
|
|
3d44553671 | ||
|
|
9d5c454e47 | ||
|
|
a00e7d1769 | ||
|
|
60be20a783 | ||
|
|
8746206060 | ||
|
|
f5ae7c4f13 | ||
|
|
dcf9462d1e | ||
|
|
78aa2a16f8 | ||
|
|
65d9eb6f7e | ||
|
|
303db0ddec | ||
|
|
a884c2574e | ||
|
|
60029e4e15 | ||
|
|
4e16d96ed5 | ||
|
|
0eefd63a13 | ||
|
|
89c2743cc6 | ||
|
|
a825427722 | ||
|
|
c99047bbda | ||
|
|
20a0b99c3d | ||
|
|
ec495a7823 | ||
|
|
e38fb1ecc6 | ||
|
|
f37ea9a4e7 | ||
|
|
8f74804c70 | ||
|
|
649f3555e5 | ||
|
|
8432df1e80 | ||
|
|
9eba9ed93a | ||
|
|
b0c6f2d7a3 | ||
|
|
6e0688afef | ||
|
|
e9951af73e | ||
|
|
138dc696b7 | ||
|
|
91b2fb86bf | ||
|
|
d4df7e7cdd | ||
|
|
4699269e49 | ||
|
|
c1ed6ee108 | ||
|
|
f59cfe7e2f | ||
|
|
9268884b17 | ||
|
|
4f435c62e6 | ||
|
|
9a97f0a096 | ||
|
|
bc23d08fa5 | ||
|
|
84d848f292 | ||
|
|
d7df832cce | ||
|
|
406b325c54 | ||
|
|
6d6e5407a3 | ||
|
|
06efa7a675 | ||
|
|
bff36e7229 | ||
|
|
cda28bb618 | ||
|
|
fe1ba71d45 | ||
|
|
23f5ffd6d6 | ||
|
|
484e5c0b8f | ||
|
|
a80febd318 | ||
|
|
16010ee28b | ||
|
|
fb1476d1e3 | ||
|
|
b375f4e3d5 | ||
|
|
25ec7ace1a | ||
|
|
ebc01dbb71 | ||
|
|
7b3e945a27 | ||
|
|
964a10ff29 | ||
|
|
5907caa732 | ||
|
|
da55cf273f | ||
|
|
a6ab4d8402 | ||
|
|
4c2318922f | ||
|
|
b2d50392ea | ||
|
|
a5086a1e58 | ||
|
|
6c4c3448e3 | ||
|
|
5d5c55e619 | ||
|
|
e2023fd72d | ||
|
|
e677b72eb8 | ||
|
|
7e090ca42f | ||
|
|
122c988477 | ||
|
|
d0fe9bd41c | ||
|
|
b1ccb30220 | ||
|
|
91e3aa4b55 | ||
|
|
2d63286c63 | ||
|
|
5dd2a5bff4 | ||
|
|
1b3b10d2ae | ||
|
|
2c26c65f4d | ||
|
|
e8d4bc52e1 | ||
|
|
6038af292f | ||
|
|
578e4da5b6 | ||
|
|
43008ef2ef | ||
|
|
d605938886 | ||
|
|
7e11d37e49 | ||
|
|
50bcf67f2b | ||
|
|
c2d58158da | ||
|
|
1731779a8d | ||
|
|
f7e349d37f | ||
|
|
61c8413138 | ||
|
|
8ee950e3de | ||
|
|
c44ef1b2f0 | ||
|
|
07dfc4b89a | ||
|
|
282e55122e | ||
|
|
17210b058f | ||
|
|
b1cf3f117d | ||
|
|
d665732056 | ||
|
|
2f59dbf1ef | ||
|
|
3a63276727 | ||
|
|
4c64f23089 | ||
|
|
683d2b2240 | ||
|
|
11f95f76e6 | ||
|
|
2732c5e8f7 | ||
|
|
6b550cb4bb | ||
|
|
712362f9e7 | ||
|
|
28ce8f5ac0 | ||
|
|
255756cfee | ||
|
|
53d821bf6d | ||
|
|
d39d4517aa | ||
|
|
bd0f434225 | ||
|
|
3806d7b6ea | ||
|
|
d1b484ff35 | ||
|
|
04c04dfc88 | ||
|
|
1d265fd143 | ||
|
|
5c91041dad |
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@@ -31,12 +31,12 @@ jobs:
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Build book
|
||||
run: cargo run -- build book-example
|
||||
run: cargo run -- build guide
|
||||
- name: Deploy to GitHub
|
||||
env:
|
||||
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
|
||||
run: |
|
||||
touch book-example/book/.nojekyll
|
||||
touch guide/book/.nojekyll
|
||||
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
|
||||
cd book-example/book
|
||||
cd guide/book
|
||||
/tmp/deploy
|
||||
|
||||
5
.github/workflows/main.yml
vendored
5
.github/workflows/main.yml
vendored
@@ -31,7 +31,8 @@ jobs:
|
||||
rust: stable
|
||||
- build: msrv
|
||||
os: ubuntu-latest
|
||||
rust: 1.35.0
|
||||
# sync MSRV with docs: guide/src/guide/installation.md
|
||||
rust: 1.54.0
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust
|
||||
@@ -48,4 +49,4 @@ jobs:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust
|
||||
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||
- run: cargo fmt -- --check
|
||||
- run: cargo fmt --check
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -4,10 +4,15 @@ target
|
||||
.DS_Store
|
||||
|
||||
book-test
|
||||
book-example/book
|
||||
guide/book
|
||||
|
||||
.vscode
|
||||
tests/dummy_book/book/
|
||||
test_book/book/
|
||||
|
||||
# Ignore Jetbrains specific files.
|
||||
.idea/
|
||||
|
||||
# Ignore Vim temporary and swap files.
|
||||
*.sw?
|
||||
*~
|
||||
|
||||
435
CHANGELOG.md
435
CHANGELOG.md
@@ -1,5 +1,440 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.4.20
|
||||
[53055e0...da166e0](https://github.com/rust-lang/mdBook/compare/53055e0...da166e0)
|
||||
|
||||
### Fixed
|
||||
- Fixed a regression in 0.4.19 where inline code would have excessive padding
|
||||
in some situations such as headings.
|
||||
[#1855](https://github.com/rust-lang/mdBook/pull/1855)
|
||||
|
||||
## mdBook 0.4.19
|
||||
[ae275ad...53055e0](https://github.com/rust-lang/mdBook/compare/ae275ad...53055e0)
|
||||
|
||||
### Added
|
||||
- The `serve` command now supports HEAD requests.
|
||||
[#1825](https://github.com/rust-lang/mdBook/pull/1825)
|
||||
|
||||
### Changed
|
||||
- An error is now generated when a custom theme directory does not exist.
|
||||
[#1791](https://github.com/rust-lang/mdBook/pull/1791)
|
||||
- Very wide tables now have independent horizontal scrolling so that scrolling
|
||||
to see the rest of the table will not scroll the entire page.
|
||||
[#1617](https://github.com/rust-lang/mdBook/pull/1617)
|
||||
- The buttons on code blocks are now only shown when the mouse cursor hovers
|
||||
over them (or tapped on mobile). There is also some extra spacing to reduce
|
||||
the overlap with the code.
|
||||
[#1806](https://github.com/rust-lang/mdBook/pull/1806)
|
||||
- The first chapter always generates an `index.html` file. Previously it would
|
||||
only generate the index file for prefix chapters.
|
||||
[#1829](https://github.com/rust-lang/mdBook/pull/1829)
|
||||
|
||||
### Fixed
|
||||
- `mdbook serve --open` now properly handles the case if the first chapter is a draft.
|
||||
[#1714](https://github.com/rust-lang/mdBook/pull/1714)
|
||||
[#1830](https://github.com/rust-lang/mdBook/pull/1830)
|
||||
- Very long words (over 80 characters) are no longer indexed to avoid a stack overflow.
|
||||
[#1833](https://github.com/rust-lang/mdBook/pull/1833)
|
||||
|
||||
## mdBook 0.4.18
|
||||
[981b79b...ae275ad](https://github.com/rust-lang/mdBook/compare/981b79b...ae275ad)
|
||||
|
||||
### Fixed
|
||||
- Fixed rendering of SUMMARY links that contain markdown escapes or other
|
||||
markdown elements.
|
||||
[#1785](https://github.com/rust-lang/mdBook/pull/1785)
|
||||
|
||||
## mdBook 0.4.17
|
||||
[a5fddfa...981b79b](https://github.com/rust-lang/mdBook/compare/a5fddfa...981b79b)
|
||||
|
||||
### Fixed
|
||||
- Fixed parsing of `output.html.print` configuration table.
|
||||
[#1775](https://github.com/rust-lang/mdBook/pull/1775)
|
||||
|
||||
## mdBook 0.4.16
|
||||
[68a5c09...a5fddfa](https://github.com/rust-lang/mdBook/compare/68a5c09...a5fddfa)
|
||||
|
||||
### Added
|
||||
- Added `output.html.print.page-break` config option to control whether or not
|
||||
there is a page break between chapters in the print output.
|
||||
[#1728](https://github.com/rust-lang/mdBook/pull/1728)
|
||||
- Added `output.html.playground.runnable` config option to globally disable
|
||||
the run button in code blocks.
|
||||
[#1546](https://github.com/rust-lang/mdBook/pull/1546)
|
||||
|
||||
### Changed
|
||||
- The `mdbook serve` live reload websocket now uses the protocol, host, and
|
||||
port of the current page, allowing access through a proxy.
|
||||
[#1771](https://github.com/rust-lang/mdBook/pull/1771)
|
||||
- The 404 not-found page now includes the books title in the HTML title tag.
|
||||
[#1693](https://github.com/rust-lang/mdBook/pull/1693)
|
||||
- Migrated to clap 3.0 which which handles CLI option parsing.
|
||||
[#1731](https://github.com/rust-lang/mdBook/pull/1731)
|
||||
|
||||
### Fixed
|
||||
- Minor fixes to the markdown parser.
|
||||
[#1729](https://github.com/rust-lang/mdBook/pull/1729)
|
||||
- Fixed incorrect parsing in `SUMMARY.md` when it didn't start with a title.
|
||||
[#1744](https://github.com/rust-lang/mdBook/pull/1744)
|
||||
- Fixed duplicate anchor IDs for links in search results.
|
||||
[#1749](https://github.com/rust-lang/mdBook/pull/1749)
|
||||
|
||||
## mdBook 0.4.15
|
||||
[5eb7d46...68a5c09](https://github.com/rust-lang/mdBook/compare/5eb7d46...68a5c09)
|
||||
|
||||
### Changed
|
||||
- Major update to expand the documentation located at <https://rust-lang.github.io/mdBook/>.
|
||||
[#1709](https://github.com/rust-lang/mdBook/pull/1709)
|
||||
[#1710](https://github.com/rust-lang/mdBook/pull/1710)
|
||||
- Updated the markdown parser with various fixes for common-mark compliance.
|
||||
[#1712](https://github.com/rust-lang/mdBook/pull/1712)
|
||||
|
||||
## mdBook 0.4.14
|
||||
[ffa8284...c9b6be8](https://github.com/rust-lang/mdBook/compare/ffa8284...c9b6be8)
|
||||
|
||||
### Added
|
||||
- The 2021 Rust edition option has been stabilized.
|
||||
[#1642](https://github.com/rust-lang/mdBook/pull/1642)
|
||||
|
||||
### Changed
|
||||
- Header anchors no longer include any HTML tags. Previously only a small
|
||||
subset were excluded.
|
||||
[#1683](https://github.com/rust-lang/mdBook/pull/1683)
|
||||
- Deprecated the google-analytics option. Books using this option should place
|
||||
the appropriate code in the `theme/head.hbs` file instead.
|
||||
[#1675](https://github.com/rust-lang/mdBook/pull/1675)
|
||||
|
||||
### Fixed
|
||||
- Updated the markdown parser which brings in a few small fixes and removes
|
||||
the custom smart quote handling.
|
||||
[#1668](https://github.com/rust-lang/mdBook/pull/1668)
|
||||
- Fixed iOS Safari enlarging text when going into landscape mode.
|
||||
[#1685](https://github.com/rust-lang/mdBook/pull/1685)
|
||||
|
||||
## mdBook 0.4.13
|
||||
[e6629cd...f55028b](https://github.com/rust-lang/mdBook/compare/e6629cd...f55028b)
|
||||
|
||||
### Added
|
||||
|
||||
- Added the ability to specify the preprocessor order.
|
||||
[#1607](https://github.com/rust-lang/mdBook/pull/1607)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Include chapters with no headers in the search index
|
||||
[#1637](https://github.com/rust-lang/mdBook/pull/1637)
|
||||
- Switched to the `opener` crate for opening a web browser, which should fix
|
||||
some issues with blocking.
|
||||
[#1656](https://github.com/rust-lang/mdBook/pull/1656)
|
||||
- Fixed clicking the border of the theme switcher breaking the theme selection.
|
||||
[#1651](https://github.com/rust-lang/mdBook/pull/1651)
|
||||
|
||||
## mdBook 0.4.12
|
||||
[14add9c...8b4e488](https://github.com/rust-lang/mdBook/compare/14add9c...8b4e488)
|
||||
|
||||
### Changed
|
||||
- Reverted the change to update to highlight.js 11, as it broke hidden code lines.
|
||||
[#1597](https://github.com/rust-lang/mdBook/pull/1621)
|
||||
|
||||
## mdBook 0.4.11
|
||||
[e440094...2cf00d0](https://github.com/rust-lang/mdBook/compare/e440094...2cf00d0)
|
||||
|
||||
### Added
|
||||
- Added support for Rust 2021 edition.
|
||||
[#1596](https://github.com/rust-lang/mdBook/pull/1596)
|
||||
- Added `mdbook completions` subcommand which provides shell completions.
|
||||
[#1425](https://github.com/rust-lang/mdBook/pull/1425)
|
||||
- Added `--title` and `--ignore` flags to `mdbook init` to avoid the
|
||||
interactive input.
|
||||
[#1559](https://github.com/rust-lang/mdBook/pull/1559)
|
||||
|
||||
### Changed
|
||||
- If running a Rust example does not have any output, it now displays the text
|
||||
"No output" instead of not showing anything.
|
||||
[#1599](https://github.com/rust-lang/mdBook/pull/1599)
|
||||
- Code block language tags can now be separated by space or tab (along with
|
||||
commas) to match the behavior of other sites like GitHub and rustdoc.
|
||||
[#1469](https://github.com/rust-lang/mdBook/pull/1469)
|
||||
- Updated `warp` (the web server) to the latest version.
|
||||
This also updates the minimum supported Rust version to 1.46.
|
||||
[#1612](https://github.com/rust-lang/mdBook/pull/1612)
|
||||
- Updated to highlight.js 11. This has various highlighting improvements.
|
||||
[#1597](https://github.com/rust-lang/mdBook/pull/1597)
|
||||
|
||||
### Fixed
|
||||
- Inline code blocks inside a header are no longer highlighted when
|
||||
`output.html.playground.editable` is `true`.
|
||||
[#1613](https://github.com/rust-lang/mdBook/pull/1613)
|
||||
|
||||
## mdBook 0.4.10
|
||||
[2f7293a...dc2062a](https://github.com/rust-lang/mdBook/compare/2f7293a...dc2062a)
|
||||
|
||||
### Changed
|
||||
- Reverted breaking change in 0.4.9 that removed the `__non_exhaustive` marker
|
||||
on the `Book` struct.
|
||||
[#1572](https://github.com/rust-lang/mdBook/pull/1572)
|
||||
- Updated handlebars to 4.0.
|
||||
[#1550](https://github.com/rust-lang/mdBook/pull/1550)
|
||||
- Removed the `chapter_begin` id on the print page's chapter separators.
|
||||
[#1541](https://github.com/rust-lang/mdBook/pull/1541)
|
||||
|
||||
## mdBook 0.4.9
|
||||
[7e01cf9...d325c60](https://github.com/rust-lang/mdBook/compare/7e01cf9...d325c60)
|
||||
|
||||
### Changed
|
||||
- Updated all dependencies and raised the minimum Rust version to 1.42.
|
||||
[#1528](https://github.com/rust-lang/mdBook/pull/1528)
|
||||
- Added more detail to error message when a preprocessor fails.
|
||||
[#1526](https://github.com/rust-lang/mdBook/pull/1526)
|
||||
- Set max-width of HTML video tags to 100% to match img tags.
|
||||
[#1542](https://github.com/rust-lang/mdBook/pull/1542)
|
||||
|
||||
### Fixed
|
||||
- Type errors when parsing `book.toml` are no longer ignored.
|
||||
[#1539](https://github.com/rust-lang/mdBook/pull/1539)
|
||||
- Better handling if `mdbook serve` fails to start the http server.
|
||||
[#1555](https://github.com/rust-lang/mdBook/pull/1555)
|
||||
- Fixed the path for `edit-url-template` if the book used a source directory
|
||||
other than `src`.
|
||||
[#1554](https://github.com/rust-lang/mdBook/pull/1554)
|
||||
|
||||
## mdBook 0.4.8
|
||||
[fcceee4...b592b10](https://github.com/rust-lang/mdBook/compare/fcceee4...b592b10)
|
||||
|
||||
### Added
|
||||
- Added the option `output.html.edit-url-template` which can be a URL which is
|
||||
linked on each page to direct the user to a site (such as GitHub) where the
|
||||
user can directly suggest an edit for the page they are currently reading.
|
||||
[#1506](https://github.com/rust-lang/mdBook/pull/1506)
|
||||
|
||||
### Changed
|
||||
- Printed output now includes a page break between chapters.
|
||||
[#1485](https://github.com/rust-lang/mdBook/pull/1485)
|
||||
|
||||
### Fixed
|
||||
- HTML, such as HTML comments, is now ignored if it appears above the title line
|
||||
in `SUMMARY.md`.
|
||||
[#1437](https://github.com/rust-lang/mdBook/pull/1437)
|
||||
|
||||
## mdBook 0.4.7
|
||||
[9a9eb01...c83bbd6](https://github.com/rust-lang/mdBook/compare/9a9eb01...c83bbd6)
|
||||
|
||||
### Changed
|
||||
- Updated shlex parser to fix a minor parsing issue (used by the
|
||||
preprocessor/backend custom command config).
|
||||
[#1471](https://github.com/rust-lang/mdBook/pull/1471)
|
||||
- Enhanced text contrast of `light` theme to improve accessibility.
|
||||
[#1470](https://github.com/rust-lang/mdBook/pull/1470)
|
||||
|
||||
### Fixed
|
||||
- Fixed some issues with fragment scrolling and linking.
|
||||
[#1463](https://github.com/rust-lang/mdBook/pull/1463)
|
||||
|
||||
## mdBook 0.4.6
|
||||
[eaa6914...1a0c296](https://github.com/rust-lang/mdBook/compare/eaa6914...1a0c296)
|
||||
|
||||
### Changed
|
||||
- The chapter name is now included in the search breadcrumbs.
|
||||
[#1389](https://github.com/rust-lang/mdBook/pull/1389)
|
||||
- Pressing Escape will remove the `?highlight` argument from the URL.
|
||||
[#1427](https://github.com/rust-lang/mdBook/pull/1427)
|
||||
- `mdbook init --theme` will now place the theme in the root of the book
|
||||
directory instead of in the `src` directory.
|
||||
[#1432](https://github.com/rust-lang/mdBook/pull/1432)
|
||||
- A custom renderer that sets the `command` to a relative path now interprets
|
||||
the relative path relative to the book root. Previously it was inconsistent
|
||||
based on the platform (either relative to the current directory, or relative
|
||||
to the renderer output directory). Paths relative to the output directory
|
||||
are still supported with a deprecation warning.
|
||||
[#1418](https://github.com/rust-lang/mdBook/pull/1418)
|
||||
- The `theme` directory in the config is now interpreted as relative to the
|
||||
book root, instead of the current directory.
|
||||
[#1405](https://github.com/rust-lang/mdBook/pull/1405)
|
||||
- Handle UTF-8 BOM for chapter sources.
|
||||
[#1285](https://github.com/rust-lang/mdBook/pull/1285)
|
||||
- Removed extra whitespace added to `{{#playground}}` snippets.
|
||||
[#1375](https://github.com/rust-lang/mdBook/pull/1375)
|
||||
|
||||
### Fixed
|
||||
- Clicking on a search result with multiple search words will now correctly
|
||||
highlight all of the words.
|
||||
[#1426](https://github.com/rust-lang/mdBook/pull/1426)
|
||||
- Properly handle `<` and `>` characters in the table of contents.
|
||||
[#1376](https://github.com/rust-lang/mdBook/pull/1376)
|
||||
- Fixed to properly serialize the `build` table in the config, which prevented
|
||||
setting it in the API.
|
||||
[#1378](https://github.com/rust-lang/mdBook/pull/1378)
|
||||
|
||||
## mdBook 0.4.5
|
||||
[eaa6914...f66df09](https://github.com/rust-lang/mdBook/compare/eaa6914...f66df09)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed XSS in the search page.
|
||||
[CVE-2020-26297](https://groups.google.com/g/rustlang-security-announcements/c/3-sO6of29O0)
|
||||
[648c9ae](https://github.com/rust-lang/mdBook/commit/648c9ae772bec83f0a5954d17b4287d5bb1d6606)
|
||||
|
||||
## mdBook 0.4.4
|
||||
[4df9ec9...01836ba](https://github.com/rust-lang/mdBook/compare/4df9ec9...01836ba)
|
||||
|
||||
### Added
|
||||
- Added the `output.html.print.enable` configuration value to disable the
|
||||
"print" page.
|
||||
[#1169](https://github.com/rust-lang/mdBook/pull/1169)
|
||||
- Added a list of supported languages for syntax-highlighting to the
|
||||
documentation.
|
||||
[#1345](https://github.com/rust-lang/mdBook/pull/1345)
|
||||
|
||||
### Fixed
|
||||
- Now supports symbolic links for files in the `src` directory.
|
||||
[#1323](https://github.com/rust-lang/mdBook/pull/1323)
|
||||
|
||||
## mdBook 0.4.3
|
||||
[9278b83...4df9ec9](https://github.com/rust-lang/mdBook/compare/9278b83...4df9ec9)
|
||||
|
||||
### Added
|
||||
- Added `output.html.cname` option to emit a `CNAME` file which is used by
|
||||
GitHub Pages to know which domain is being used.
|
||||
[#1311](https://github.com/rust-lang/mdBook/pull/1311)
|
||||
|
||||
### Changed
|
||||
- `mdbook test` no longer stops on the first test failure, but instead will
|
||||
run all the tests.
|
||||
[#1313](https://github.com/rust-lang/mdBook/pull/1313)
|
||||
- Removed the `local` font source for Source Code Pro, as the locally
|
||||
installed font may not render properly on FireFox on macOS.
|
||||
[#1307](https://github.com/rust-lang/mdBook/pull/1307)
|
||||
|
||||
### Fixed
|
||||
- Added newline to end of `.nojekyll` file.
|
||||
[#1310](https://github.com/rust-lang/mdBook/pull/1310)
|
||||
- Fixed missing space before draft chapter titles.
|
||||
[#1309](https://github.com/rust-lang/mdBook/pull/1309)
|
||||
|
||||
## mdBook 0.4.2
|
||||
[649f355...9278b83](https://github.com/rust-lang/mdBook/compare/649f355...9278b83)
|
||||
|
||||
### Changed
|
||||
- The "show hidden lines" icon has changed from the "expand" icon to an "eye".
|
||||
[#1281](https://github.com/rust-lang/mdBook/pull/1281)
|
||||
- Updated highlight.js. This adds several languages: c, c-like (effectively
|
||||
cpp), csharp (replaces cs), kotlin, less, lua, php-template, plaintext,
|
||||
python-repl, r, scss, typescript.
|
||||
[#1277](https://github.com/rust-lang/mdBook/pull/1277)
|
||||
|
||||
### Fixed
|
||||
- Fixed SUMMARY links that contained newlines.
|
||||
[#1291](https://github.com/rust-lang/mdBook/pull/1291)
|
||||
- Fixed SUMMARY links that contain `%20` spaces.
|
||||
[#1293](https://github.com/rust-lang/mdBook/pull/1293)
|
||||
- Fixed favicon so that if only the png or svg is overridden, the other is not
|
||||
automatically included in the `<link>` tag.
|
||||
[#1272](https://github.com/rust-lang/mdBook/pull/1272)
|
||||
|
||||
## mdBook 0.4.1
|
||||
[d4df7e7...649f355](https://github.com/rust-lang/mdBook/compare/d4df7e7...649f355)
|
||||
|
||||
### Changed
|
||||
- Removed several outdated dev-dependencies.
|
||||
[#1267](https://github.com/rust-lang/mdBook/pull/1267)
|
||||
|
||||
### Fixed
|
||||
- Fixed sidebar scrolling if the book includes part titles.
|
||||
[#1265](https://github.com/rust-lang/mdBook/pull/1265)
|
||||
- Don't include the default favicon if only one of the PNG or SVG is overridden.
|
||||
[#1266](https://github.com/rust-lang/mdBook/pull/1266)
|
||||
|
||||
## mdBook 0.4.0
|
||||
[99ecd4f...d4df7e7](https://github.com/rust-lang/mdBook/compare/99ecd4f...d4df7e7)
|
||||
|
||||
### Breaking Changes
|
||||
- Several of the changes in the release have altered the public API of the
|
||||
mdbook library.
|
||||
- Many dependencies have been updated or replaced.
|
||||
This also removes the `--websocket-hostname` and `--websocket-port` from
|
||||
the `serve` command.
|
||||
[#1211](https://github.com/rust-lang/mdBook/pull/1211)
|
||||
- A new "404" page is now automatically rendered. This requires knowledge of
|
||||
the base URL of your site to work properly. If you decide to use this as
|
||||
your 404 page, you should set the `site-url` setting in the book
|
||||
configuration so mdbook can generate the links correctly. Alternatively you
|
||||
can disable the 404 page generation, or set up your own 404 handling in your
|
||||
web server.
|
||||
[#1221](https://github.com/rust-lang/mdBook/pull/1221)
|
||||
- The `debug` and `output` features have been removed as they were unused.
|
||||
[#1211](https://github.com/rust-lang/mdBook/pull/1211)
|
||||
- If you are using customized themes, you may want to consider setting the
|
||||
`preferred-dark-theme` config setting, as it now defaults to "navy".
|
||||
[#1199](https://github.com/rust-lang/mdBook/pull/1199)
|
||||
- "Playpen" has been renamed to "playground". This is generally backwards
|
||||
compatible for users, but `{{#playpen}}` will now display warnings. This may
|
||||
impact books that have modified the "playpen" elements in the theme.
|
||||
[#1241](https://github.com/rust-lang/mdBook/pull/1241)
|
||||
- If a renderer is not installed, it is now treated as an error. If you want
|
||||
the old behavior of ignoring missing renderers, set the `optional` setting
|
||||
for that renderer.
|
||||
[#1122](https://github.com/rust-lang/mdBook/pull/1122)
|
||||
- If you have a custom favicon, you may need to look into adding an SVG
|
||||
version, otherwise the default SVG icon will be displayed.
|
||||
[#1230](https://github.com/rust-lang/mdBook/pull/1230)
|
||||
|
||||
### Added
|
||||
- Added a new `[rust]` configuration section to `book.toml`, which allows
|
||||
setting the default edition with `edition = "2018"`.
|
||||
[#1163](https://github.com/rust-lang/mdBook/pull/1163)
|
||||
- Renderers can now be marked as `optional`, so that they will be ignored if
|
||||
the renderer is not installed.
|
||||
[#1122](https://github.com/rust-lang/mdBook/pull/1122)
|
||||
- Added `head.hbs` to allow adding content to the `<head>` section in HTML.
|
||||
[#1206](https://github.com/rust-lang/mdBook/pull/1206)
|
||||
- Added "draft chapters". These are chapters listed without a link to indicate
|
||||
content yet to be written.
|
||||
[#1153](https://github.com/rust-lang/mdBook/pull/1153)
|
||||
- Added "parts" to split a book into different sections. Headers can be added
|
||||
to `SUMMARY.md` to signify different sections.
|
||||
[#1171](https://github.com/rust-lang/mdBook/pull/1171)
|
||||
- Added generation of a "404" page for handling missing pages and broken links.
|
||||
[#1221](https://github.com/rust-lang/mdBook/pull/1221)
|
||||
- Added configuration section for specifying URL redirects.
|
||||
[#1237](https://github.com/rust-lang/mdBook/pull/1237)
|
||||
- Added an SVG favicon that works with light and dark colors schemes.
|
||||
[#1230](https://github.com/rust-lang/mdBook/pull/1230)
|
||||
|
||||
### Changed
|
||||
- Changed default Rust attribute of `allow(unused_variables)` to `allow(unused)`.
|
||||
[#1195](https://github.com/rust-lang/mdBook/pull/1195)
|
||||
- Fonts are now served locally instead of from the Google Fonts CDN. The
|
||||
`copy-fonts` option was added to disable this if you want to supply your own
|
||||
fonts.
|
||||
[#1188](https://github.com/rust-lang/mdBook/pull/1188)
|
||||
- Switched the built-in webserver for the `serve` command to a new
|
||||
implementation. This results in some internal differences in how websockets
|
||||
are handled, which removes the separate websocket options. This should also
|
||||
make it easier to serve multiple books at once.
|
||||
[#1211](https://github.com/rust-lang/mdBook/pull/1211)
|
||||
- The default dark theme is now "navy".
|
||||
[#1199](https://github.com/rust-lang/mdBook/pull/1199)
|
||||
- "Playpen" has been renamed to "playground", matching the actual name of the
|
||||
service which was renamed many years ago.
|
||||
[#1241](https://github.com/rust-lang/mdBook/pull/1241)
|
||||
|
||||
### Fixed
|
||||
- Links with the `+` symbol should now work.
|
||||
[#1208](https://github.com/rust-lang/mdBook/pull/1208)
|
||||
- The `MDBOOK_BOOK` environment variable now correctly allows overriding the
|
||||
entire book configuration.
|
||||
[#1207](https://github.com/rust-lang/mdBook/pull/1207)
|
||||
- The sidebar can no longer be dragged outside of the window.
|
||||
[#1229](https://github.com/rust-lang/mdBook/pull/1229)
|
||||
- Hide the Rust Playground "play" button for `no_run` code samples.
|
||||
[#1249](https://github.com/rust-lang/mdBook/pull/1249)
|
||||
- Fixed the `--dest-dir` command-line option for the `serve` and `watch`
|
||||
commands.
|
||||
[#1228](https://github.com/rust-lang/mdBook/pull/1228)
|
||||
- Hotkey handlers are now disabled in `text` input fields (for example, typing
|
||||
`S` in a custom text input field).
|
||||
[#1244](https://github.com/rust-lang/mdBook/pull/1244)
|
||||
|
||||
## mdBook 0.3.7
|
||||
[88684d8...99ecd4f](https://github.com/rust-lang/mdBook/compare/88684d8...99ecd4f)
|
||||
|
||||
|
||||
3
CODE_OF_CONDUCT.md
Normal file
3
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# The Rust Code of Conduct
|
||||
|
||||
The Code of Conduct for this repository [can be found online](https://www.rust-lang.org/conduct.html).
|
||||
@@ -6,7 +6,6 @@ If you have come here to learn how to contribute to mdBook, we have some tips fo
|
||||
|
||||
First of all, don't hesitate to ask questions!
|
||||
Use the [issue tracker](https://github.com/rust-lang/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
|
||||
|
||||
@@ -46,7 +45,7 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
|
||||
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`.
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdbook` or `mdbook.exe`.
|
||||
|
||||
### Code Quality
|
||||
|
||||
@@ -79,7 +78,7 @@ For more information, such as running it from your favourite editor, please see
|
||||
|
||||
#### Finding Issues with Clippy
|
||||
|
||||
Clippy is a code analyser/linter detecting mistakes, and therfore helps to improve your code.
|
||||
Clippy is a code analyser/linter detecting mistakes, and therefore helps to improve your code.
|
||||
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will
|
||||
help us maintain awesome code.
|
||||
|
||||
@@ -106,3 +105,26 @@ and [rustfmt](https://github.com/rust-lang/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:
|
||||
|
||||
## Browser compatibility and testing
|
||||
|
||||
Currently we don't have a strict browser compatibility matrix due to our limited resources.
|
||||
We generally strive to keep mdBook compatible with a relatively recent browser on all of the most major platforms.
|
||||
That is, supporting Chrome, Safari, Firefox, Edge on Windows, macOS, Linux, iOS, and Android.
|
||||
If possible, do your best to avoid breaking older browser releases.
|
||||
|
||||
Any change to the HTML or styling is encouraged to manually check on as many browsers and platforms that you can.
|
||||
Unfortunately at this time we don't have any automated UI or browser testing, so your assistance in testing is appreciated.
|
||||
|
||||
## Updating higlight.js
|
||||
|
||||
The following are instructions for updating [highlight.js](https://highlightjs.org/).
|
||||
|
||||
1. Clone the repository at <https://github.com/highlightjs/highlight.js>
|
||||
1. Check out a tagged release (like `10.1.1`).
|
||||
1. Run `npm install`
|
||||
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx properties r scala x86asm yaml`
|
||||
1. Compare the language list that it spits out to the one in [`syntax-highlighting.md`](https://github.com/camelid/mdBook/blob/master/guide/src/format/theme/syntax-highlighting.md). If any are missing, add them to the list and rebuild (and update these docs). If any are added to the common set, add them to `syntax-highlighting.md`.
|
||||
1. Copy `build/highlight.min.js` to mdbook's directory [`highlight.js`](https://github.com/rust-lang/mdBook/blob/master/src/theme/highlight.js).
|
||||
1. Be sure to check the highlight.js [CHANGES](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md) for any breaking changes. Breaking changes that would affect users will need to wait until the next major release.
|
||||
1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. The [test_book](https://github.com/rust-lang/mdBook/tree/master/test_book) contains a chapter with many languages to examine.
|
||||
|
||||
2228
Cargo.lock
generated
2228
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
48
Cargo.toml
48
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.3.7"
|
||||
version = "0.4.20"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
]
|
||||
documentation = "http://rust-lang.github.io/mdBook/index.html"
|
||||
edition = "2018"
|
||||
exclude = ["/book-example/*"]
|
||||
exclude = ["/guide/*"]
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
@@ -16,50 +16,50 @@ repository = "https://github.com/rust-lang/mdBook"
|
||||
description = "Creates a book from markdown files"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.28"
|
||||
chrono = "0.4"
|
||||
clap = "2.24"
|
||||
env_logger = "0.6"
|
||||
error-chain = "0.12"
|
||||
handlebars = "3.0"
|
||||
itertools = "0.8"
|
||||
clap = { version = "3.0", features = ["cargo"] }
|
||||
clap_complete = "3.0"
|
||||
env_logger = "0.9.0"
|
||||
handlebars = "4.0"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4"
|
||||
memchr = "2.0"
|
||||
open = "1.1"
|
||||
pulldown-cmark = "0.6.1"
|
||||
regex = "1.0.0"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
opener = "0.5"
|
||||
pulldown-cmark = { version = "0.9.1", default-features = false }
|
||||
regex = "1.5.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
shlex = "0.1"
|
||||
shlex = "1"
|
||||
tempfile = "3.0"
|
||||
toml = "0.5.1"
|
||||
toml-query = "0.9"
|
||||
topological-sort = "0.1.0"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "4.0", optional = true }
|
||||
gitignore = { version = "1.0", optional = true }
|
||||
|
||||
# Serve feature
|
||||
iron = { version = "0.6", optional = true }
|
||||
staticfile = { version = "0.5", optional = true }
|
||||
ws = { version = "0.9", optional = true}
|
||||
futures-util = { version = "0.3.4", optional = true }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
warp = { version = "0.3.2", default-features = false, features = ["websocket"], optional = true }
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
||||
elasticlunr-rs = { version = "3.0.0", optional = true }
|
||||
ammonia = { version = "3", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
select = "0.4"
|
||||
pretty_assertions = "0.6"
|
||||
assert_cmd = "1"
|
||||
predicates = "2"
|
||||
select = "0.5"
|
||||
semver = "1.0"
|
||||
pretty_assertions = "1.2.1"
|
||||
walkdir = "2.0"
|
||||
|
||||
[features]
|
||||
default = ["output", "watch", "serve", "search"]
|
||||
debug = []
|
||||
output = []
|
||||
default = ["watch", "serve", "search"]
|
||||
watch = ["notify", "gitignore"]
|
||||
serve = ["iron", "staticfile", "ws"]
|
||||
serve = ["futures-util", "tokio", "warp"]
|
||||
search = ["elasticlunr-rs", "ammonia"]
|
||||
|
||||
[[bin]]
|
||||
|
||||
220
README.md
220
README.md
@@ -6,229 +6,15 @@
|
||||
|
||||
mdBook is a utility to create modern online books from Markdown files.
|
||||
|
||||
Check out the **[User Guide]** for a list of features and installation and usage information.
|
||||
The User Guide also serves as a demonstration to showcase what a book looks like.
|
||||
|
||||
## What does it look like?
|
||||
|
||||
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 multiple ways to install mdBook.
|
||||
|
||||
1. **Binaries**
|
||||
|
||||
Binaries are available for download [here][releases]. Make sure to put the
|
||||
path to the binary into your `PATH`.
|
||||
|
||||
2. **From Crates.io**
|
||||
|
||||
This requires at least [Rust] 1.35 and Cargo to be installed. Once you have installed
|
||||
Rust, type the following in the terminal:
|
||||
|
||||
```
|
||||
cargo install mdbook
|
||||
```
|
||||
|
||||
This will download and compile mdBook for you, the only thing left to do is
|
||||
to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
**Note for automatic deployment**
|
||||
|
||||
If you are using a script to do automatic deployments using Travis or
|
||||
another CI server, we recommend that you specify a semver version range for
|
||||
mdBook when you install it through your script!
|
||||
|
||||
This will constrain the server to install the latest **non-breaking**
|
||||
version of mdBook and will prevent your books from failing to build because
|
||||
we released a new version.
|
||||
|
||||
You can also disable default features to speed up compile time.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
cargo install mdbook --no-default-features --features output --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/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/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 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 [User Guide].
|
||||
|
||||
- `mdbook init`
|
||||
|
||||
The init command will create a directory with the minimal boilerplate to
|
||||
start with.
|
||||
|
||||
```
|
||||
book-test/
|
||||
├── book
|
||||
└── src
|
||||
├── chapter_1.md
|
||||
└── SUMMARY.md
|
||||
```
|
||||
|
||||
`book` and `src` are both directories. `src` contains the markdown files
|
||||
that will be used to render the output to the `book` directory.
|
||||
|
||||
Please, take a look at the [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.
|
||||
|
||||
- `mdbook watch`
|
||||
|
||||
When you run this command, mdbook will watch your markdown files to rebuild
|
||||
the book on every change. This avoids having to come back to the terminal
|
||||
to type `mdbook build` over and over again.
|
||||
|
||||
- `mdbook serve`
|
||||
|
||||
Does the same thing as `mdbook watch` but additionally serves the book at
|
||||
`http://localhost:3000` (port is changeable) and reloads the browser when a
|
||||
change occurs.
|
||||
|
||||
- `mdbook clean`
|
||||
|
||||
Delete directory in which generated book is located.
|
||||
|
||||
### 3rd Party Plugins
|
||||
|
||||
The way a book is loaded and rendered can be configured by the user via third
|
||||
party plugins. These plugins are just programs which will be invoked during the
|
||||
build process and are split into roughly two categories, *preprocessors* and
|
||||
*renderers*.
|
||||
|
||||
Preprocessors are used to transform a book before it is sent to a renderer.
|
||||
One example would be to replace all occurrences of
|
||||
`{{#include some_file.ext}}` with the contents of that file. Some existing
|
||||
preprocessors are:
|
||||
|
||||
- `index` - a built-in preprocessor (enabled by default) which will transform
|
||||
all `README.md` chapters to `index.md` so `foo/README.md` can be accessed via
|
||||
the url `foo/` when published to a browser
|
||||
- `links` - a built-in preprocessor (enabled by default) for expanding the
|
||||
`{{# playpen}}` and `{{# include}}` helpers in a chapter.
|
||||
|
||||
Renderers are given the final book so they can do something with it. This is
|
||||
typically used for, as the name suggests, rendering the document in a particular
|
||||
format, however there's nothing stopping a renderer from doing static analysis
|
||||
of a book in order to validate links or run tests. Some existing renderers are:
|
||||
|
||||
- `html` - the built-in renderer which will generate a HTML version of the book
|
||||
- `markdown` - the built-in renderer (disabled by default) which will run
|
||||
preprocessors then output the resulting Markdown. Useful for debugging
|
||||
preprocessors.
|
||||
- [`linkcheck`] - a backend which will check that all links are valid
|
||||
- [`epub`] - an experimental EPUB generator
|
||||
|
||||
> **Note for Developers:** Feel free to send us a PR if you've developed your
|
||||
> own plugin and want it mentioned here.
|
||||
|
||||
A preprocessor or renderer is enabled by installing the appropriate program and
|
||||
then mentioning it in the book's `book.toml` file.
|
||||
|
||||
```console
|
||||
$ cargo install mdbook-linkcheck
|
||||
$ edit book.toml && cat book.toml
|
||||
[book]
|
||||
title = "My Awesome Book"
|
||||
authors = ["Michael-F-Bryan"]
|
||||
|
||||
[output.html]
|
||||
|
||||
[output.linkcheck] # enable the "mdbook-linkcheck" renderer
|
||||
|
||||
$ mdbook build
|
||||
2018-10-20 13:57:51 [INFO] (mdbook::book): Book building has started
|
||||
2018-10-20 13:57:51 [INFO] (mdbook::book): Running the html backend
|
||||
2018-10-20 13:57:53 [INFO] (mdbook::book): Running the linkcheck backend
|
||||
```
|
||||
|
||||
For more information on the plugin system, consult the [User Guide].
|
||||
|
||||
### 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 an easy to
|
||||
use API and more!
|
||||
|
||||
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.
|
||||
|
||||
If you are just starting out with Rust, there are a series of issues 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.
|
||||
|
||||
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`.
|
||||
|
||||
If you are interested in contributing to the development of mdBook, check out the [Contribution Guide].
|
||||
|
||||
## License
|
||||
|
||||
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.github.io/mdBook/
|
||||
[API docs]: https://docs.rs/mdbook/*/mdbook/
|
||||
[E-Easy]: https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy
|
||||
[contribution guide]: https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md
|
||||
[LICENSE]: https://github.com/rust-lang/mdBook/blob/master/LICENSE
|
||||
[releases]: https://github.com/rust-lang/mdBook/releases
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[CLI docs]: http://rust-lang.github.io/mdBook/cli/init.html
|
||||
[master-docs]: http://rust-lang.github.io/mdBook/
|
||||
[`linkcheck`]: https://crates.io/crates/mdbook-linkcheck
|
||||
[`epub`]: https://crates.io/crates/mdbook-epub
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# 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).
|
||||
|
||||
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/rust-lang/mdBook). Issues and feature
|
||||
requests can be posted on the [GitHub issue
|
||||
tracker](https://github.com/rust-lang/mdBook/issues).
|
||||
|
||||
## API docs
|
||||
|
||||
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/).
|
||||
@@ -1,55 +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 From Binaries
|
||||
|
||||
Precompiled binaries are provided for major platforms on a best-effort basis.
|
||||
Visit [the releases page](https://github.com/rust-lang/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/tools/install) 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/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/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,48 +0,0 @@
|
||||
# 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.
|
||||
|
||||
***Note:*** *The `serve` command is for testing a book's HTML output, and is not
|
||||
intended to be a complete HTTP server for a website.*
|
||||
|
||||
#### 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
|
||||
default web browser after starting the server.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
@@ -1,89 +0,0 @@
|
||||
# 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.3" mdbook)
|
||||
- cargo install-update -a
|
||||
|
||||
script:
|
||||
- mdbook build path/to/mybook && mdbook test path/to/mybook
|
||||
```
|
||||
|
||||
## 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!
|
||||
|
||||
### Deploying to GitHub Pages manually
|
||||
|
||||
If your CI doesn't support GitHub pages, or you're deploying somewhere else
|
||||
with integrations such as Github Pages:
|
||||
*note: you may want to use different tmp dirs*:
|
||||
|
||||
```console
|
||||
$> git worktree add /tmp/book gh-pages
|
||||
$> mdbook build
|
||||
$> rm -rf /tmp/book/* # this won't delete the .git directory
|
||||
$> cp -rp book/* /tmp/book/
|
||||
$> cd /tmp/book
|
||||
$> git add -A
|
||||
$> git commit 'new book message'
|
||||
$> git push origin gh-pages
|
||||
$> cd -
|
||||
```
|
||||
|
||||
Or put this into a Makefile rule:
|
||||
|
||||
```makefile
|
||||
.PHONY: deploy
|
||||
deploy: book
|
||||
@echo "====> deploying to github"
|
||||
git worktree add /tmp/book gh-pages
|
||||
rm -rf /tmp/book/*
|
||||
cp -rp book/* /tmp/book/
|
||||
cd /tmp/book && \
|
||||
git add -A && \
|
||||
git commit -m "deployed on $(shell date) by ${USER}" && \
|
||||
git push origin gh-pages
|
||||
```
|
||||
@@ -1,333 +0,0 @@
|
||||
# Configuration
|
||||
|
||||
You can configure the parameters for your book in the ***book.toml*** file.
|
||||
|
||||
Here is an example of what a ***book.toml*** file might look like:
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
author = "John Doe"
|
||||
description = "The example book covers examples."
|
||||
|
||||
[build]
|
||||
build-dir = "my-example-book"
|
||||
create-missing = false
|
||||
|
||||
[preprocessor.index]
|
||||
|
||||
[preprocessor.links]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["custom.css"]
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 15
|
||||
```
|
||||
|
||||
## Supported configuration options
|
||||
|
||||
It is important to note that **any** relative path specified in the
|
||||
configuration will always be taken relative from the root of the book where the
|
||||
configuration file is located.
|
||||
|
||||
### 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.
|
||||
- **language:** The main language of the book, which is used as a language attribute `<html lang="en">` for example.
|
||||
|
||||
**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`
|
||||
language = "en"
|
||||
```
|
||||
|
||||
### 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 }}`, `{{ #include }}`, and `{{ #rustdoc_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
|
||||
|
||||
[preprocessor.links]
|
||||
|
||||
[preprocessor.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
|
||||
|
||||
```toml
|
||||
[preprocessor.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.
|
||||
|
||||
```toml
|
||||
[preprocessor.mathjax]
|
||||
renderers = ["html"] # mathjax only makes sense with the HTML renderer
|
||||
```
|
||||
|
||||
### Provide Your Own Command
|
||||
|
||||
By default when you add a `[preprocessor.foo]` table to your `book.toml` file,
|
||||
`mdbook` will try to invoke the `mdbook-foo` executable. If you want to use a
|
||||
different program name or pass in command-line arguments, this behaviour can
|
||||
be overridden by adding a `command` field.
|
||||
|
||||
```toml
|
||||
[preprocessor.random]
|
||||
command = "python random.py"
|
||||
```
|
||||
|
||||
## 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.
|
||||
- **default-theme:** The theme color scheme to select by default in the
|
||||
'Change Theme' dropdown. Defaults to `light`.
|
||||
- **preferred-dark-theme:** The default dark theme. This theme will be used if
|
||||
the browser requests the dark version of the site via the
|
||||
['prefers-color-scheme'](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
|
||||
CSS media query. Defaults to the same theme as `default-theme`.
|
||||
- **curly-quotes:** Convert straight quotes to curly quotes, except for those
|
||||
that occur in code blocks and code spans. Defaults to `false`.
|
||||
- **mathjax-support:** Adds support for [MathJax](mathjax.md). 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`.
|
||||
- **fold:** A subtable for configuring sidebar section-folding behavior.
|
||||
- **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).
|
||||
- **git-repository-url:** A url to the git repository for the book. If provided
|
||||
an icon link will be output in the menu bar of the book.
|
||||
- **git-repository-icon:** The FontAwesome icon class to use for the git
|
||||
repository link. Defaults to `fa-github`.
|
||||
|
||||
Available configuration options for the `[output.html.fold]` table:
|
||||
|
||||
- **enable:** Enable section-folding. When off, all folds are open.
|
||||
Defaults to `false`.
|
||||
- **level:** The higher the more folded regions are open. When level is 0, all
|
||||
folds are closed. Defaults to `0`.
|
||||
|
||||
Available configuration options for the `[output.html.playpen]` table:
|
||||
|
||||
- **editable:** Allow editing the source code. Defaults to `false`.
|
||||
- **copyable:** Display the copy button on code snippets. Defaults to `true`.
|
||||
- **copy-js:** Copy JavaScript files for the editor to the output directory.
|
||||
Defaults to `true`.
|
||||
- **line-numbers** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`.
|
||||
|
||||
[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 `false`.
|
||||
- **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 HTML output options in the **book.toml**:
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
[output.html]
|
||||
theme = "my-theme"
|
||||
default-theme = "light"
|
||||
preferred-dark-theme = "navy"
|
||||
curly-quotes = true
|
||||
mathjax-support = false
|
||||
google-analytics = "123456"
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
no-section-label = false
|
||||
git-repository-url = "https://github.com/rust-lang/mdBook"
|
||||
git-repository-icon = "fa-github"
|
||||
|
||||
[output.html.fold]
|
||||
enable = false
|
||||
level = 0
|
||||
|
||||
[output.html.playpen]
|
||||
editable = false
|
||||
copy-js = true
|
||||
line-numbers = false
|
||||
|
||||
[output.html.search]
|
||||
enable = true
|
||||
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
|
||||
```
|
||||
|
||||
### Markdown Renderer
|
||||
|
||||
The Markdown renderer will run preprocessors and then output the resulting
|
||||
Markdown. This is mostly useful for debugging preprocessors, especially in
|
||||
conjunction with `mdbook test` to see the Markdown that `mdbook` is passing
|
||||
to `rustdoc`.
|
||||
|
||||
The Markdown renderer is included with `mdbook` but disabled by default.
|
||||
Enable it by adding an emtpy table to your `book.toml` as follows:
|
||||
|
||||
```toml
|
||||
[output.markdown]
|
||||
```
|
||||
|
||||
There are no configuration options for the Markdown renderer at this time;
|
||||
only whether it is enabled or disabled.
|
||||
|
||||
See [the preprocessors documentation](#configuring-preprocessors) for how to
|
||||
specify which preprocessors should run before the Markdown renderer.
|
||||
|
||||
### Custom Renderers
|
||||
|
||||
A custom renderer can be enabled by adding a `[output.foo]` table to your
|
||||
`book.toml`. Similar to [preprocessors](#configuring-preprocessors) this will
|
||||
instruct `mdbook` to pass a representation of the book to `mdbook-foo` for
|
||||
rendering.
|
||||
|
||||
Custom renderers will have access to all configuration within their table
|
||||
(i.e. anything under `[output.foo]`), and the command to be invoked can be
|
||||
manually specified with the `command` field.
|
||||
|
||||
## 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
|
||||
>
|
||||
> ```shell
|
||||
> $ 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,6 +0,0 @@
|
||||
fn main() {
|
||||
println!("Hello World!");
|
||||
#
|
||||
# // You can even hide lines! :D
|
||||
# println!("I am hidden! Expand the code snippet to see me");
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
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.)
|
||||
```markdown
|
||||
- [Title of the Chapter](relative/path/to/markdown.md)
|
||||
```
|
||||
You can either use `-` or `*` to indicate a numbered chapter.
|
||||
|
||||
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.
|
||||
@@ -1,34 +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 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.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Introduction
|
||||
|
||||
A frontmatter chapter.
|
||||
@@ -21,4 +21,4 @@ case $1 in
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "##[add-path]$PWD/hub/bin"
|
||||
echo "$PWD/hub/bin" >> $GITHUB_PATH
|
||||
|
||||
@@ -12,7 +12,7 @@ TOOLCHAIN="$1"
|
||||
|
||||
rustup set profile minimal
|
||||
rustup component remove --toolchain=$TOOLCHAIN rust-docs || echo "already removed"
|
||||
rustup update $TOOLCHAIN
|
||||
rustup update --no-self-update $TOOLCHAIN
|
||||
rustup default $TOOLCHAIN
|
||||
rustup -V
|
||||
rustc -Vv
|
||||
|
||||
@@ -11,13 +11,24 @@ fi
|
||||
TAG=${GITHUB_REF#*/tags/}
|
||||
|
||||
host=$(rustc -Vv | grep ^host: | sed -e "s/host: //g")
|
||||
cargo rustc --bin mdbook --release -- -C lto
|
||||
export CARGO_PROFILE_RELEASE_LTO=true
|
||||
cargo build --bin mdbook --release
|
||||
cd target/release
|
||||
case $1 in
|
||||
ubuntu* | macos*)
|
||||
ubuntu*)
|
||||
asset="mdbook-$TAG-$host.tar.gz"
|
||||
tar czf ../../$asset mdbook
|
||||
;;
|
||||
macos*)
|
||||
asset="mdbook-$TAG-$host.tar.gz"
|
||||
# There is a bug with BSD tar on macOS where the first 8MB of the file are
|
||||
# sometimes all NUL bytes. See https://github.com/actions/cache/issues/403
|
||||
# and https://github.com/rust-lang/cargo/issues/8603 for some more
|
||||
# information. An alternative solution here is to install GNU tar, but
|
||||
# flushing the disk cache seems to work, too.
|
||||
sudo /usr/sbin/purge
|
||||
tar czf ../../$asset mdbook
|
||||
;;
|
||||
windows*)
|
||||
asset="mdbook-$TAG-$host.zip"
|
||||
7z a ../../$asset mdbook.exe
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
use crate::nop_lib::Nop;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use clap::{App, Arg, ArgMatches};
|
||||
use mdbook::book::Book;
|
||||
use mdbook::errors::Error;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use semver::{Version, VersionReq};
|
||||
use std::io;
|
||||
use std::process;
|
||||
|
||||
pub fn make_app() -> App<'static, 'static> {
|
||||
pub fn make_app() -> App<'static> {
|
||||
App::new("nop-preprocessor")
|
||||
.about("A mdbook preprocessor which does precisely nothing")
|
||||
.subcommand(
|
||||
SubCommand::with_name("supports")
|
||||
.arg(Arg::with_name("renderer").required(true))
|
||||
App::new("supports")
|
||||
.arg(Arg::new("renderer").required(true))
|
||||
.about("Check whether a renderer is supported by this preprocessor"),
|
||||
)
|
||||
}
|
||||
@@ -33,9 +34,10 @@ fn main() {
|
||||
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
|
||||
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
|
||||
|
||||
if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
|
||||
// We should probably use the `semver` crate to check compatibility
|
||||
// here...
|
||||
let book_version = Version::parse(&ctx.mdbook_version)?;
|
||||
let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
|
||||
|
||||
if !version_req.matches(&book_version) {
|
||||
eprintln!(
|
||||
"Warning: The {} plugin was built against version {} of mdbook, \
|
||||
but we're being called from version {}",
|
||||
@@ -53,7 +55,7 @@ fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
|
||||
|
||||
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
|
||||
let renderer = sub_args.value_of("renderer").expect("Required argument");
|
||||
let supported = pre.supports_renderer(&renderer);
|
||||
let supported = pre.supports_renderer(renderer);
|
||||
|
||||
// Signal whether the renderer is supported by exiting with 1 or 0.
|
||||
if supported {
|
||||
@@ -87,7 +89,7 @@ mod nop_lib {
|
||||
// particular config value
|
||||
if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
|
||||
if nop_cfg.contains_key("blow-up") {
|
||||
return Err("Boom!!1!".into());
|
||||
anyhow::bail!("Boom!!1!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,16 @@ description = "Create book from markdown files. Like Gitbook but implemented in
|
||||
authors = ["Mathieu David", "Michael-F-Bryan"]
|
||||
language = "en"
|
||||
|
||||
[rust]
|
||||
edition = "2018"
|
||||
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
site-url = "/mdBook/"
|
||||
git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide"
|
||||
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||
|
||||
[output.html.playpen]
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
line-numbers = true
|
||||
|
||||
@@ -19,3 +25,6 @@ boost-hierarchy = 2
|
||||
boost-paragraph = 1
|
||||
expand = true
|
||||
heading-split-level = 2
|
||||
|
||||
[output.html.redirect]
|
||||
"/format/config.html" = "configuration/index.html"
|
||||
3
guide/src/404.md
Normal file
3
guide/src/404.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Document not found (404)
|
||||
|
||||
This URL is invalid, sorry. Try the search instead!
|
||||
41
guide/src/README.md
Normal file
41
guide/src/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Introduction
|
||||
|
||||
**mdBook** is a command line tool to create books with Markdown.
|
||||
It is ideal for creating product or API documentation, tutorials, course materials or anything that requires a clean,
|
||||
easily navigable and customizable presentation.
|
||||
|
||||
* Lightweight [Markdown] syntax helps you focus more on your content
|
||||
* Integrated [search] support
|
||||
* Color [syntax highlighting] for code blocks for many different languages
|
||||
* [Theme] files allow customizing the formatting of the output
|
||||
* [Preprocessors] can provide extensions for custom syntax and modifying content
|
||||
* [Backends] can render the output to multiple formats
|
||||
* Written in [Rust] for speed, safety, and simplicity
|
||||
* Automated testing of [Rust code samples]
|
||||
|
||||
This guide is an example of what mdBook produces.
|
||||
mdBook is used by the Rust programming language project, and [The Rust Programming Language][trpl] book is another fine example of mdBook in action.
|
||||
|
||||
[Markdown]: format/markdown.md
|
||||
[search]: guide/reading.md#search
|
||||
[syntax highlighting]: format/theme/syntax-highlighting.md
|
||||
[theme]: format/theme/index.html
|
||||
[preprocessors]: format/configuration/preprocessors.md
|
||||
[backends]: format/configuration/renderers.md
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[trpl]: https://doc.rust-lang.org/book/
|
||||
[Rust code samples]: cli/test.md
|
||||
|
||||
## Contributing
|
||||
|
||||
mdBook is free and open source. You can find the source code on
|
||||
[GitHub](https://github.com/rust-lang/mdBook) and issues and feature requests can be posted on
|
||||
the [GitHub issue tracker](https://github.com/rust-lang/mdBook/issues). mdBook relies on the community to fix bugs and
|
||||
add features: if you'd like to contribute, please read
|
||||
the [CONTRIBUTING](https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md) guide and consider opening
|
||||
a [pull request](https://github.com/rust-lang/mdBook/pulls).
|
||||
|
||||
## License
|
||||
|
||||
The mdBook source and documentation are released under
|
||||
the [Mozilla Public License v2.0](https://www.mozilla.org/MPL/2.0/).
|
||||
@@ -1,6 +1,15 @@
|
||||
# Summary
|
||||
|
||||
- [mdBook](README.md)
|
||||
[Introduction](README.md)
|
||||
|
||||
# User Guide
|
||||
|
||||
- [Installation](guide/installation.md)
|
||||
- [Reading Books](guide/reading.md)
|
||||
- [Creating a Book](guide/creating.md)
|
||||
|
||||
# Reference Guide
|
||||
|
||||
- [Command Line Tool](cli/README.md)
|
||||
- [init](cli/init.md)
|
||||
- [build](cli/build.md)
|
||||
@@ -8,15 +17,22 @@
|
||||
- [serve](cli/serve.md)
|
||||
- [test](cli/test.md)
|
||||
- [clean](cli/clean.md)
|
||||
- [completions](cli/completions.md)
|
||||
- [Format](format/README.md)
|
||||
- [SUMMARY.md](format/summary.md)
|
||||
- [Configuration](format/config.md)
|
||||
- [Draft chapter]()
|
||||
- [Configuration](format/configuration/README.md)
|
||||
- [General](format/configuration/general.md)
|
||||
- [Preprocessors](format/configuration/preprocessors.md)
|
||||
- [Renderers](format/configuration/renderers.md)
|
||||
- [Environment Variables](format/configuration/environment-variables.md)
|
||||
- [Theme](format/theme/README.md)
|
||||
- [index.hbs](format/theme/index-hbs.md)
|
||||
- [Syntax highlighting](format/theme/syntax-highlighting.md)
|
||||
- [Editor](format/theme/editor.md)
|
||||
- [MathJax Support](format/mathjax.md)
|
||||
- [mdBook specific features](format/mdbook.md)
|
||||
- [mdBook-specific features](format/mdbook.md)
|
||||
- [Markdown](format/markdown.md)
|
||||
- [Continuous Integration](continuous-integration.md)
|
||||
- [For Developers](for_developers/README.md)
|
||||
- [Preprocessors](for_developers/preprocessors.md)
|
||||
14
guide/src/cli/README.md
Normal file
14
guide/src/cli/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Command Line Tool
|
||||
|
||||
The `mdbook` command-line tool is used to create and build books.
|
||||
After you have [installed](../guide/installation.md) `mdbook`, you can run the `mdbook help` command in your terminal to view the available commands.
|
||||
|
||||
This following sections provide in-depth information on the different commands available.
|
||||
|
||||
* [`mdbook init <directory>`](init.md) — Creates a new book with minimal boilerplate to start with.
|
||||
* [`mdbook build`](build.md) — Renders the book.
|
||||
* [`mdbook watch`](watch.md) — Rebuilds the book any time a source file changes.
|
||||
* [`mdbook serve`](serve.md) — Runs a web server to view the book, and rebuilds on changes.
|
||||
* [`mdbook test`](test.md) — Tests Rust code samples.
|
||||
* [`mdbook clean`](clean.md) — Deletes the rendered output.
|
||||
* [`mdbook completions`](completions.md) — Support for shell auto-completion.
|
||||
@@ -7,7 +7,8 @@ mdbook build
|
||||
```
|
||||
|
||||
It will try to parse your `SUMMARY.md` file to understand the structure of your
|
||||
book and fetch the corresponding files.
|
||||
book and fetch the corresponding files. Note that files mentioned in `SUMMARY.md`
|
||||
but not present will be created.
|
||||
|
||||
The rendered output will maintain the same directory structure as the source for
|
||||
convenience. Large books will therefore remain structured when rendered.
|
||||
16
guide/src/cli/completions.md
Normal file
16
guide/src/cli/completions.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# The completions command
|
||||
|
||||
The completions command is used to generate auto-completions for some common shells.
|
||||
This means when you type `mdbook` in your shell, you can then press your shell's auto-complete key (usually the Tab key) and it may display what the valid options are, or finish partial input.
|
||||
|
||||
The completions first need to be installed for your shell:
|
||||
|
||||
```bash
|
||||
mdbook completions bash > ~/.local/share/bash-completion/completions/mdbook
|
||||
```
|
||||
|
||||
The command prints a completion script for the given shell.
|
||||
Run `mdbook completions --help` for a list of supported shells.
|
||||
|
||||
Where to place the completions depend on which shell you are using and your operating system.
|
||||
Consult your shell's documentation for more information one where to place the script.
|
||||
@@ -25,9 +25,9 @@ book-test/
|
||||
- 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.md)
|
||||
- The `SUMMARY.md` is the skeleton of your
|
||||
book, and is discussed in more detail [in another
|
||||
chapter](../format/summary.md).
|
||||
|
||||
#### Tip: Generate chapters from SUMMARY.md
|
||||
|
||||
@@ -52,3 +52,19 @@ 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.
|
||||
|
||||
#### --title
|
||||
|
||||
Specify a title for the book. If not supplied, an interactive prompt will ask for
|
||||
a title.
|
||||
|
||||
```bash
|
||||
mdbook init --title="my amazing book"
|
||||
```
|
||||
|
||||
#### --ignore
|
||||
|
||||
Create a `.gitignore` file configured to ignore the `book` directory created when [building] a book.
|
||||
If not supplied, an interactive prompt will ask whether it should be created.
|
||||
|
||||
[building]: build.md
|
||||
56
guide/src/cli/serve.md
Normal file
56
guide/src/cli/serve.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# The serve command
|
||||
|
||||
The serve command is used to preview a book by serving it via HTTP at
|
||||
`localhost:3000` by default:
|
||||
|
||||
```bash
|
||||
mdbook serve
|
||||
```
|
||||
|
||||
The `serve` command watches the book's `src` directory for
|
||||
changes, rebuilding the book and refreshing clients for each change; this includes
|
||||
re-creating deleted files still mentioned in `SUMMARY.md`! A websocket
|
||||
connection is used to trigger the client-side refresh.
|
||||
|
||||
***Note:*** *The `serve` command is for testing a book's HTML output, and is not
|
||||
intended to be a complete HTTP server for a website.*
|
||||
|
||||
#### 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
|
||||
|
||||
The `serve` hostname defaults to `localhost`, and the port defaults to `3000`. Either option can be specified on the command line:
|
||||
|
||||
```bash
|
||||
mdbook serve path/to/book -p 8000 -n 127.0.0.1
|
||||
```
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the book in 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. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
#### Specify exclude patterns
|
||||
|
||||
The `serve` command will not automatically trigger a build for files listed in
|
||||
the `.gitignore` file in the book root directory. The `.gitignore` file may
|
||||
contain file patterns described in the [gitignore
|
||||
documentation](https://git-scm.com/docs/gitignore). This can be useful for
|
||||
ignoring temporary files created by some editors.
|
||||
|
||||
***Note:*** *Only the `.gitignore` from the book root directory is used. Global
|
||||
`$HOME/.gitignore` or `.gitignore` files in parent directories are not used.*
|
||||
@@ -43,7 +43,17 @@ mdbook test path/to/book
|
||||
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`).
|
||||
comma-delimited list (`-L foo,bar`). The path should point to the Cargo
|
||||
[build cache](https://doc.rust-lang.org/cargo/guide/build-cache.html) `deps` directory that
|
||||
contains the build output of your project. For example, if your Rust project's book is in a directory
|
||||
named `my-book`, the following command would include the crate's dependencies when running `test`:
|
||||
|
||||
```shell
|
||||
mdbook test my-book -L target/debug/deps/
|
||||
```
|
||||
|
||||
See the `rustdoc` command-line [documentation](https://doc.rust-lang.org/rustdoc/command-line-arguments.html#-l--library-path-where-to-look-for-dependencies)
|
||||
for more information.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
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.
|
||||
build automatically whenever you modify a file; this includes re-creating
|
||||
deleted files still mentioned in `SUMMARY.md`!
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
@@ -25,3 +26,15 @@ The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
|
||||
#### Specify exclude patterns
|
||||
|
||||
The `watch` command will not automatically trigger a build for files listed in
|
||||
the `.gitignore` file in the book root directory. The `.gitignore` file may
|
||||
contain file patterns described in the [gitignore
|
||||
documentation](https://git-scm.com/docs/gitignore). This can be useful for
|
||||
ignoring temporary files created by some editors.
|
||||
|
||||
_Note: Only `.gitignore` from book root directory is used. Global
|
||||
`$HOME/.gitignore` or `.gitignore` files in parent directories are not used._
|
||||
121
guide/src/continuous-integration.md
Normal file
121
guide/src/continuous-integration.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Running `mdbook` in Continuous Integration
|
||||
|
||||
There are a variety of services such as [GitHub Actions] or [GitLab CI/CD] which can be used to test and deploy your book automatically.
|
||||
|
||||
The following provides some general guidelines on how to configure your service to run mdBook.
|
||||
Specific recipes can be found at the [Automated Deployment] wiki page.
|
||||
|
||||
[GitHub Actions]: https://docs.github.com/en/actions
|
||||
[GitLab CI/CD]: https://docs.gitlab.com/ee/ci/
|
||||
[Automated Deployment]: https://github.com/rust-lang/mdBook/wiki/Automated-Deployment
|
||||
|
||||
## Installing mdBook
|
||||
|
||||
There are several different strategies for installing mdBook.
|
||||
The particular method depends on your needs and preferences.
|
||||
|
||||
### Pre-compiled binaries
|
||||
|
||||
Perhaps the easiest method is to use the pre-compiled binaries found on the [GitHub Releases page][releases].
|
||||
A simple approach would be to use the popular `curl` CLI tool to download the executable:
|
||||
|
||||
```sh
|
||||
mkdir bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.20/mdbook-v0.4.20-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
bin/mdbook build
|
||||
```
|
||||
|
||||
Some considerations for this approach:
|
||||
|
||||
* This is relatively fast, and does not necessarily require dealing with caching.
|
||||
* This does not require installing Rust.
|
||||
* Specifying a specific URL means you have to manually update your script to get a new version.
|
||||
This may be a benefit if you want to lock to a specific version.
|
||||
However, some users prefer to automatically get a newer version when they are published.
|
||||
* You are reliant on the GitHub CDN being available.
|
||||
|
||||
[releases]: https://github.com/rust-lang/mdBook/releases
|
||||
|
||||
### Building from source
|
||||
|
||||
Building from source will require having Rust installed.
|
||||
Some services have Rust pre-installed, but if your service does not, you will need to add a step to install it.
|
||||
|
||||
After Rust is installed, `cargo install` can be used to build and install mdBook.
|
||||
We recommend using a SemVer version specifier so that you get the latest **non-breaking** version of mdBook.
|
||||
For example:
|
||||
|
||||
```sh
|
||||
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
||||
```
|
||||
|
||||
This includes several recommended options:
|
||||
|
||||
* `--no-default-features` — Disables features like the HTTP server used by `mdbook serve` that is likely not needed on CI.
|
||||
This will speed up the build time significantly.
|
||||
* `--features search` — Disabling default features means you should then manually enable features that you want, such as the built-in [search] capability.
|
||||
* `--vers "^0.4"` — This will install the most recent version of the `0.4` series.
|
||||
However, versions after like `0.5.0` won't be installed, as they may break your build.
|
||||
Cargo will automatically upgrade mdBook if you have an older version already installed.
|
||||
* `--locked` — This will use the dependencies that were used when mdBook was released.
|
||||
Without `--locked`, it will use the latest version of all dependencies, which may include some fixes since the last release, but may also (rarely) cause build problems.
|
||||
|
||||
You will likely want to investigate caching options, as building mdBook can be somewhat slow.
|
||||
|
||||
[search]: guide/reading.md#search
|
||||
|
||||
## Running tests
|
||||
|
||||
You may want to run tests using [`mdbook test`] every time you push a change or create a pull request.
|
||||
This can be used to validate Rust code examples in the book.
|
||||
|
||||
This will require having Rust installed.
|
||||
Some services have Rust pre-installed, but if your service does not, you will need to add a step to install it.
|
||||
|
||||
Other than making sure the appropriate version of Rust is installed, there's not much more than just running `mdbook test` from the book directory.
|
||||
|
||||
You may also want to consider running other kinds of tests, like [mdbook-linkcheck] which will check for broken links.
|
||||
Or if you have your own style checks, spell checker, or any other tests it might be good to run them in CI.
|
||||
|
||||
[`mdbook test`]: cli/test.md
|
||||
[mdbook-linkcheck]: https://github.com/Michael-F-Bryan/mdbook-linkcheck#continuous-integration
|
||||
|
||||
## Deploying
|
||||
|
||||
You may want to automatically deploy your book.
|
||||
Some may want to do this with every time a change is pushed, and others may want to only deploy when a specific release is tagged.
|
||||
|
||||
You'll also need to understand the specifics on how to push a change to your web service.
|
||||
For example, [GitHub Pages] just requires committing the output onto a specific git branch.
|
||||
Other services may require using something like SSH to connect to a remote server.
|
||||
|
||||
The basic outline is that you need to run `mdbook build` to generate the output, and then transfer the files (which are in the `book` directory) to the correct location.
|
||||
|
||||
You may then want to consider if you need to invalidate any caches on your web service.
|
||||
|
||||
See the [Automated Deployment] wiki page for examples of various different services.
|
||||
|
||||
[GitHub Pages]: https://docs.github.com/en/pages
|
||||
|
||||
### 404 handling
|
||||
|
||||
mdBook automatically generates a 404 page to be used for broken links.
|
||||
The default output is a file named `404.html` at the root of the book.
|
||||
Some services like [GitHub Pages] will automatically use this page for broken links.
|
||||
For other services, you may want to consider configuring the web server to use this page as it will provide the reader navigation to get back to the book.
|
||||
|
||||
If your book is not deployed at the root of the domain, then you should set the [`output.html.site-url`] setting so that the 404 page works correctly.
|
||||
It needs to know where the book is deployed in order to load the static files (like CSS) correctly.
|
||||
For example, this guide is deployed at <https://rust-lang.github.io/mdBook/>, and the `site-url` setting is configured like this:
|
||||
|
||||
```toml
|
||||
# book.toml
|
||||
[output.html]
|
||||
site-url = "/mdBook/"
|
||||
```
|
||||
|
||||
You can customize the look of the 404 page by creating a file named `src/404.md` in your book.
|
||||
If you want to use a different filename, you can set [`output.html.input-404`] to a different filename.
|
||||
|
||||
[`output.html.site-url`]: format/configuration/renderers.md#html-renderer-options
|
||||
[`output.html.input-404`]: format/configuration/renderers.md#html-renderer-options
|
||||
@@ -24,8 +24,9 @@ The process of rendering a book project goes through several steps.
|
||||
exist
|
||||
- Load the book chapters into memory
|
||||
- Discover which preprocessors/backends should be used
|
||||
2. Run the preprocessors
|
||||
3. Call each backend in turn
|
||||
2. For each backend:
|
||||
1. Run all the preprocessors.
|
||||
2. Call the backend to render the processed result.
|
||||
|
||||
|
||||
## Using `mdbook` as a Library
|
||||
@@ -5,22 +5,17 @@ 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 alternative backends on GitHub which can be used as a
|
||||
rough example of how this is accomplished in practice.
|
||||
See [Configuring Renderers](../format/configuration/renderers.md) for more information about using backends.
|
||||
|
||||
- [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`)
|
||||
The community has developed several backends.
|
||||
See the [Third Party Plugins] wiki page for a list of available backends.
|
||||
|
||||
## Setting Up
|
||||
|
||||
This page will step you through creating your own alternative 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.
|
||||
|
||||
@@ -329,7 +324,6 @@ 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
|
||||
@@ -342,10 +336,7 @@ 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
|
||||
[Third Party Plugins]: https://github.com/rust-lang/mdBook/wiki/Third-party-plugins
|
||||
[`RenderContext`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html
|
||||
[`RenderContext::from_json()`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html#method.from_json
|
||||
[`semver`]: https://crates.io/crates/semver
|
||||
@@ -5,37 +5,27 @@ 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
|
||||
|
||||
See [Configuring Preprocessors](../format/configuration/preprocessors.md) for more information about using preprocessors.
|
||||
|
||||
## Hooking Into MDBook
|
||||
|
||||
MDBook uses a fairly simple mechanism for discovering third party plugins.
|
||||
A new table is added to `book.toml` (e.g. `preprocessor.foo` for the `foo`
|
||||
A new table is added to `book.toml` (e.g. `[preprocessor.foo]` for the `foo`
|
||||
preprocessor) and then `mdbook` will try to invoke the `mdbook-foo` program as
|
||||
part of the build process.
|
||||
|
||||
While preprocessors can be hard-coded to specify which backend it should be run
|
||||
for (e.g. it doesn't make sense for MathJax to be used for non-HTML renderers)
|
||||
with the `preprocessor.foo.renderer` key.
|
||||
Once the preprocessor has been defined and the build process starts, mdBook executes the command defined in the `preprocessor.foo.command` key twice.
|
||||
The first time it runs the preprocessor to determine if it supports the given renderer.
|
||||
mdBook passes two arguments to the process: the first argument is the string `supports` and the second argument is the renderer name.
|
||||
The preprocessor should exit with a status code 0 if it supports the given renderer, or return a non-zero exit code if it does not.
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "My Book"
|
||||
authors = ["Michael-F-Bryan"]
|
||||
If the preprocessor supports the renderer, then mdbook runs it a second time, passing JSON data into stdin.
|
||||
The JSON consists of an array of `[context, book]` where `context` is the serialized object [`PreprocessorContext`] and `book` is a [`Book`] object containing the content of the book.
|
||||
|
||||
[preprocessor.foo]
|
||||
# The command can also be specified manually
|
||||
command = "python3 /path/to/foo.py"
|
||||
# Only run the `foo` preprocessor for the HTML and EPUB renderer
|
||||
renderer = ["html", "epub"]
|
||||
```
|
||||
|
||||
In typical unix style, all inputs to the plugin will be written to `stdin` as
|
||||
JSON and `mdbook` will read from `stdout` if it is expecting output.
|
||||
The preprocessor should return the JSON format of the [`Book`] object to stdout, with any modifications it wishes to perform.
|
||||
|
||||
The easiest way to get started is by creating your own implementation of the
|
||||
`Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
|
||||
@@ -71,7 +61,7 @@ The `chapter.content` is just a string which happens to be markdown. While it's
|
||||
entirely possible to use regular expressions or do a manual find & replace,
|
||||
you'll probably want to process the input into something more computer-friendly.
|
||||
The [`pulldown-cmark`][pc] crate implements a production-quality event-based
|
||||
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] allowing you to
|
||||
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] crate allowing you to
|
||||
translate events back into markdown text.
|
||||
|
||||
The following code block shows how to remove all emphasis from markdown,
|
||||
@@ -106,6 +96,33 @@ fn remove_emphasis(
|
||||
|
||||
For everything else, have a look [at the complete example][example].
|
||||
|
||||
## Implementing a preprocessor with a different language
|
||||
|
||||
The fact that mdBook utilizes stdin and stdout to communicate with the preprocessors makes it easy to implement them in a language other than Rust.
|
||||
The following code shows how to implement a simple preprocessor in Python, which will modify the content of the first chapter.
|
||||
The example below follows the configuration shown above with `preprocessor.foo.command` actually pointing to a Python script.
|
||||
|
||||
```python
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1: # we check if we received any argument
|
||||
if sys.argv[1] == "supports":
|
||||
# then we are good to return an exit status code of 0, since the other argument will just be the renderer's name
|
||||
sys.exit(0)
|
||||
|
||||
# load both the context and the book representations from stdin
|
||||
context, book = json.load(sys.stdin)
|
||||
# and now, we can just modify the content of the first chapter
|
||||
book['sections'][0]['Chapter']['content'] = '# Hello'
|
||||
# we are done with the book's modification, we can just print it to stdout,
|
||||
print(json.dumps(book))
|
||||
```
|
||||
|
||||
|
||||
|
||||
[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
|
||||
@@ -113,3 +130,5 @@ For everything else, have a look [at the complete example][example].
|
||||
[an example no-op preprocessor]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||
[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input
|
||||
[`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut
|
||||
[`PreprocessorContext`]: https://docs.rs/mdbook/latest/mdbook/preprocess/struct.PreprocessorContext.html
|
||||
[`Book`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html
|
||||
12
guide/src/format/configuration/README.md
Normal file
12
guide/src/format/configuration/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Configuration
|
||||
|
||||
This section details the configuration options available in the ***book.toml***:
|
||||
- **[General]** configuration including the `book`, `rust`, `build` sections
|
||||
- **[Preprocessor]** configuration for default and custom book preprocessors
|
||||
- **[Renderer]** configuration for the HTML, Markdown and custom renderers
|
||||
- **[Environment Variable]** configuration for overriding configuration options in your environment
|
||||
|
||||
[General]: general.md
|
||||
[Preprocessor]: preprocessors.md
|
||||
[Renderer]: renderers.md
|
||||
[Environment Variable]: environment-variables.md
|
||||
38
guide/src/format/configuration/environment-variables.md
Normal file
38
guide/src/format/configuration/environment-variables.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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
|
||||
>
|
||||
> ```shell
|
||||
> $ 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.
|
||||
110
guide/src/format/configuration/general.md
Normal file
110
guide/src/format/configuration/general.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# General Configuration
|
||||
|
||||
You can configure the parameters for your book in the ***book.toml*** file.
|
||||
|
||||
Here is an example of what a ***book.toml*** file might look like:
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
authors = ["John Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
[rust]
|
||||
edition = "2018"
|
||||
|
||||
[build]
|
||||
build-dir = "my-example-book"
|
||||
create-missing = false
|
||||
|
||||
[preprocessor.index]
|
||||
|
||||
[preprocessor.links]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["custom.css"]
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 15
|
||||
```
|
||||
|
||||
## Supported configuration options
|
||||
|
||||
It is important to note that **any** relative path specified in the
|
||||
configuration will always be taken relative from the root of the book where the
|
||||
configuration file is located.
|
||||
|
||||
### 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.
|
||||
- **language:** The main language of the book, which is used as a language attribute `<html lang="en">` for example.
|
||||
|
||||
**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`
|
||||
language = "en"
|
||||
```
|
||||
|
||||
### Rust options
|
||||
|
||||
Options for the Rust language, relevant to running tests and playground
|
||||
integration.
|
||||
|
||||
```toml
|
||||
[rust]
|
||||
edition = "2015" # the default edition for code blocks
|
||||
```
|
||||
|
||||
- **edition**: Rust edition to use by default for the code snippets. Default
|
||||
is "2015". Individual code blocks can be controlled with the `edition2015`,
|
||||
`edition2018` or `edition2021` annotations, such as:
|
||||
|
||||
~~~text
|
||||
```rust,edition2015
|
||||
// This only works in 2015.
|
||||
let try = true;
|
||||
```
|
||||
~~~
|
||||
|
||||
### Build options
|
||||
|
||||
This controls the build process of your book.
|
||||
|
||||
```toml
|
||||
[build]
|
||||
build-dir = "book" # the directory where the output is placed
|
||||
create-missing = true # whether or not to create missing pages
|
||||
use-default-preprocessors = true # use the default preprocessors
|
||||
```
|
||||
|
||||
- **build-dir:** The directory to put the rendered book in. By default this is
|
||||
`book/` in the book's root directory.
|
||||
This can overridden with the `--dest-dir` CLI option.
|
||||
- **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.
|
||||
87
guide/src/format/configuration/preprocessors.md
Normal file
87
guide/src/format/configuration/preprocessors.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Configuring Preprocessors
|
||||
|
||||
Preprocessors are extensions that can modify the raw Markdown source before it gets sent to the renderer.
|
||||
|
||||
The following preprocessors are built-in and included by default:
|
||||
|
||||
- `links`: Expands the `{{ #playground }}`, `{{ #include }}`, and `{{ #rustdoc_include }}` handlebars
|
||||
helpers in a chapter to include the contents of a file.
|
||||
See [Including files] for more.
|
||||
- `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.
|
||||
|
||||
The built-in preprocessors can be disabled with the [`build.use-default-preprocessors`] config option.
|
||||
|
||||
The community has developed several preprocessors.
|
||||
See the [Third Party Plugins] wiki page for a list of available preprocessors.
|
||||
|
||||
For information on how to create a new preprocessor, see the [Preprocessors for Developers] chapter.
|
||||
|
||||
[Including files]: ../mdbook.md#including-files
|
||||
[`build.use-default-preprocessors`]: general.md#build-options
|
||||
[Third Party Plugins]: https://github.com/rust-lang/mdBook/wiki/Third-party-plugins
|
||||
[Preprocessors for Developers]: ../../for_developers/preprocessors.md
|
||||
|
||||
## Custom Preprocessor Configuration
|
||||
|
||||
Preprocessors can be added by including a `preprocessor` table in `book.toml` with the name of the preprocessor.
|
||||
For example, if you have a preprocessor called `mdbook-example`, then you can include it with:
|
||||
|
||||
```toml
|
||||
[preprocessor.example]
|
||||
```
|
||||
|
||||
With this table, mdBook will execute the `mdbook-example` preprocessor.
|
||||
|
||||
This table can include additional key-value pairs that are specific to the preprocessor.
|
||||
For example, if our example prepocessor needed some extra configuration options:
|
||||
|
||||
```toml
|
||||
[preprocessor.example]
|
||||
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.
|
||||
|
||||
```toml
|
||||
[preprocessor.example]
|
||||
renderers = ["html"] # example preprocessor only runs with the HTML renderer
|
||||
```
|
||||
|
||||
## Provide Your Own Command
|
||||
|
||||
By default when you add a `[preprocessor.foo]` table to your `book.toml` file,
|
||||
`mdbook` will try to invoke the `mdbook-foo` executable. If you want to use a
|
||||
different program name or pass in command-line arguments, this behaviour can
|
||||
be overridden by adding a `command` field.
|
||||
|
||||
```toml
|
||||
[preprocessor.random]
|
||||
command = "python random.py"
|
||||
```
|
||||
|
||||
## Require A Certain Order
|
||||
|
||||
The order in which preprocessors are run can be controlled with the `before` and `after` fields.
|
||||
For example, suppose you want your `linenos` preprocessor to process lines that may have been `{{#include}}`d; then you want it to run after the built-in `links` preprocessor, which you can require using either the `before` or `after` field:
|
||||
|
||||
```toml
|
||||
[preprocessor.linenos]
|
||||
after = [ "links" ]
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```toml
|
||||
[preprocessor.links]
|
||||
before = [ "linenos" ]
|
||||
```
|
||||
|
||||
It would also be possible, though redundant, to specify both of the above in the same config file.
|
||||
|
||||
Preprocessors having the same priority specified through `before` and `after` are sorted by name.
|
||||
Any infinite loops will be detected and produce an error.
|
||||
299
guide/src/format/configuration/renderers.md
Normal file
299
guide/src/format/configuration/renderers.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# Configuring Renderers
|
||||
|
||||
Renderers (also called "backends") are responsible for creating the output of the book.
|
||||
|
||||
The following backends are built-in:
|
||||
|
||||
* [`html`](#html-renderer-options) — This renders the book to HTML.
|
||||
This is enabled by default if no other `[output]` tables are defined in `book.toml`.
|
||||
* [`markdown`](#markdown-renderer) — This outputs the book as markdown after running the preprocessors.
|
||||
This is useful for debugging preprocessors.
|
||||
|
||||
The community has developed several backends.
|
||||
See the [Third Party Plugins] wiki page for a list of available backends.
|
||||
|
||||
For information on how to create a new backend, see the [Backends for Developers] chapter.
|
||||
|
||||
[Third Party Plugins]: https://github.com/rust-lang/mdBook/wiki/Third-party-plugins
|
||||
[Backends for Developers]: ../../for_developers/backends.md
|
||||
|
||||
## Output tables
|
||||
|
||||
Backends can be added by including a `output` table in `book.toml` with the name of the backend.
|
||||
For example, if you have a backend called `mdbook-wordcount`, then you can include it with:
|
||||
|
||||
```toml
|
||||
[output.wordcount]
|
||||
```
|
||||
|
||||
With this table, mdBook will execute the `mdbook-wordcount` backend.
|
||||
|
||||
This table can include additional key-value pairs that are specific to the backend.
|
||||
For example, if our example backend needed some extra configuration options:
|
||||
|
||||
```toml
|
||||
[output.wordcount]
|
||||
ignores = ["Example Chapter"]
|
||||
```
|
||||
|
||||
If you define any `[output]` tables, then the `html` backend is not enabled by default.
|
||||
If you want to keep the `html` backend running, then just include it in the `book.toml` file.
|
||||
For example:
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "My Awesome Book"
|
||||
|
||||
[output.wordcount]
|
||||
|
||||
[output.html]
|
||||
```
|
||||
|
||||
If more than one `output` table is included, this changes the behavior for the layout of the output directory.
|
||||
If there is only one backend, then it places its output directly in the `book` directory (see [`build.build-dir`] to override this location).
|
||||
If there is more than one backend, then each backend is placed in a separate directory underneath `book`.
|
||||
For example, the above would have directories `book/html` and `book/wordcount`.
|
||||
|
||||
[`build.build-dir`]: general.md#build-options
|
||||
|
||||
### Custom backend commands
|
||||
|
||||
By default when you add an `[output.foo]` table to your `book.toml` file,
|
||||
`mdbook` will try to invoke the `mdbook-foo` executable.
|
||||
If you want to use a different program name or pass in command-line arguments,
|
||||
this behaviour can be overridden by adding a `command` field.
|
||||
|
||||
```toml
|
||||
[output.random]
|
||||
command = "python random.py"
|
||||
```
|
||||
|
||||
### Optional backends
|
||||
|
||||
If you enable a backend that isn't installed, the default behavior is to throw an error.
|
||||
This behavior can be changed by marking the backend as optional:
|
||||
|
||||
```toml
|
||||
[output.wordcount]
|
||||
optional = true
|
||||
```
|
||||
|
||||
This demotes the error to a warning.
|
||||
|
||||
|
||||
## HTML renderer options
|
||||
|
||||
The HTML renderer has a variety of options detailed below.
|
||||
They should be specified in the `[output.html]` table of the `book.toml` file.
|
||||
|
||||
```toml
|
||||
# Example book.toml file with all output options.
|
||||
[book]
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
[output.html]
|
||||
theme = "my-theme"
|
||||
default-theme = "light"
|
||||
preferred-dark-theme = "navy"
|
||||
curly-quotes = true
|
||||
mathjax-support = false
|
||||
copy-fonts = true
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
no-section-label = false
|
||||
git-repository-url = "https://github.com/rust-lang/mdBook"
|
||||
git-repository-icon = "fa-github"
|
||||
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||
site-url = "/example-book/"
|
||||
cname = "myproject.rs"
|
||||
input-404 = "not-found.md"
|
||||
```
|
||||
|
||||
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.
|
||||
- **default-theme:** The theme color scheme to select by default in the
|
||||
'Change Theme' dropdown. Defaults to `light`.
|
||||
- **preferred-dark-theme:** The default dark theme. This theme will be used if
|
||||
the browser requests the dark version of the site via the
|
||||
['prefers-color-scheme'](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
|
||||
CSS media query. Defaults to `navy`.
|
||||
- **curly-quotes:** Convert straight quotes to curly quotes, except for those
|
||||
that occur in code blocks and code spans. Defaults to `false`.
|
||||
- **mathjax-support:** Adds support for [MathJax](../mathjax.md). Defaults to
|
||||
`false`.
|
||||
- **copy-fonts:** Copies fonts.css and respective font files to the output directory and use them in the default theme. Defaults to `true`.
|
||||
- **google-analytics:** This field has been deprecated and will be removed in a future release.
|
||||
Use the `theme/head.hbs` file to add the appropriate Google Analytics code instead.
|
||||
- **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 numeric section labels in the table of
|
||||
contents column. For example, "1.", "2.1". Set this option to true to disable
|
||||
those labels. Defaults to `false`.
|
||||
- **git-repository-url:** A url to the git repository for the book. If provided
|
||||
an icon link will be output in the menu bar of the book.
|
||||
- **git-repository-icon:** The FontAwesome icon class to use for the git
|
||||
repository link. Defaults to `fa-github` which looks like <i class="fa fa-github"></i>.
|
||||
If you are not using GitHub, another option to consider is `fa-code-fork` which looks like <i class="fa fa-code-fork"></i>.
|
||||
- **edit-url-template:** Edit url template, when provided shows a
|
||||
"Suggest an edit" button (which looks like <i class="fa fa-edit"></i>) for directly jumping to editing the currently
|
||||
viewed page. For e.g. GitHub projects set this to
|
||||
`https://github.com/<owner>/<repo>/edit/master/{path}` or for
|
||||
Bitbucket projects set it to
|
||||
`https://bitbucket.org/<owner>/<repo>/src/master/{path}?mode=edit`
|
||||
where {path} will be replaced with the full path of the file in the
|
||||
repository.
|
||||
- **input-404:** The name of the markdown file used for missing files.
|
||||
The corresponding output file will be the same, with the extension replaced with `html`.
|
||||
Defaults to `404.md`.
|
||||
- **site-url:** The url where the book will be hosted. This is required to ensure
|
||||
navigation links and script/css imports in the 404 file work correctly, even when accessing
|
||||
urls in subdirectories. Defaults to `/`.
|
||||
- **cname:** The DNS subdomain or apex domain at which your book will be hosted.
|
||||
This string will be written to a file named CNAME in the root of your site, as
|
||||
required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages
|
||||
site*][custom domain]).
|
||||
|
||||
[custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
||||
|
||||
### `[output.html.print]`
|
||||
|
||||
The `[output.html.print]` table provides options for controlling the printable output.
|
||||
By default, mdBook will include an icon on the top right of the book (which looks like <i class="fa fa-print"></i>) that will print the book as a single page.
|
||||
|
||||
```toml
|
||||
[output.html.print]
|
||||
enable = true # include support for printable output
|
||||
page-break = true # insert page-break after each chapter
|
||||
```
|
||||
|
||||
- **enable:** Enable print support. When `false`, all print support will not be
|
||||
rendered. Defaults to `true`.
|
||||
- **page-break** Insert page breaks between chapters. Defaults to `true`.
|
||||
|
||||
### `[output.html.fold]`
|
||||
|
||||
The `[output.html.fold]` table provides options for controlling folding of the chapter listing in the navigation sidebar.
|
||||
|
||||
```toml
|
||||
[output.html.fold]
|
||||
enable = false # whether or not to enable section folding
|
||||
level = 0 # the depth to start folding
|
||||
```
|
||||
|
||||
- **enable:** Enable section-folding. When off, all folds are open.
|
||||
Defaults to `false`.
|
||||
- **level:** The higher the more folded regions are open. When level is 0, all
|
||||
folds are closed. Defaults to `0`.
|
||||
|
||||
### `[output.html.playground]`
|
||||
|
||||
The `[output.html.playground]` table provides options for controlling Rust sample code blocks, and their integration with the [Rust Playground].
|
||||
|
||||
[Rust Playground]: https://play.rust-lang.org/
|
||||
|
||||
```toml
|
||||
[output.html.playground]
|
||||
editable = false # allows editing the source code
|
||||
copyable = true # include the copy button for copying code snippets
|
||||
copy-js = true # includes the JavaScript for the code editor
|
||||
line-numbers = false # displays line numbers for editable code
|
||||
runnable = true # displays a run button for rust code
|
||||
```
|
||||
|
||||
- **editable:** Allow editing the source code. Defaults to `false`.
|
||||
- **copyable:** Display the copy button on code snippets. Defaults to `true`.
|
||||
- **copy-js:** Copy JavaScript files for the editor to the output directory.
|
||||
Defaults to `true`.
|
||||
- **line-numbers** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`.
|
||||
- **runnable** Displays a run button for rust code snippets. Changing this to `false` will disable the run in playground feature globally. Defaults to `true`.
|
||||
|
||||
[Ace]: https://ace.c9.io/
|
||||
|
||||
### `[output.html.search]`
|
||||
|
||||
The `[output.html.search]` table provides options for controlling the built-in text [search].
|
||||
mdBook must be compiled with the `search` feature enabled (on by default).
|
||||
|
||||
[search]: ../../guide/reading.md#search
|
||||
|
||||
```toml
|
||||
[output.html.search]
|
||||
enable = true # enables the search feature
|
||||
limit-results = 30 # maximum number of search results
|
||||
teaser-word-count = 30 # number of words used for a search result teaser
|
||||
use-boolean-and = true # multiple search terms must all match
|
||||
boost-title = 2 # ranking boost factor for matches in headers
|
||||
boost-hierarchy = 1 # ranking boost factor for matches in page names
|
||||
boost-paragraph = 1 # ranking boost factor for matches in text
|
||||
expand = true # partial words will match longer terms
|
||||
heading-split-level = 3 # link results to heading levels
|
||||
copy-js = true # include Javascript code for search
|
||||
```
|
||||
|
||||
- **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 `false`.
|
||||
- **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`.
|
||||
|
||||
### `[output.html.redirect]`
|
||||
|
||||
The `[output.html.redirect]` table provides a way to add redirects.
|
||||
This is useful when you move, rename, or remove a page to ensure that links to the old URL will go to the new location.
|
||||
|
||||
```toml
|
||||
[output.html.redirect]
|
||||
"/appendices/bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html"
|
||||
"/other-installation-methods.html" = "../infra/other-installation-methods.html"
|
||||
```
|
||||
|
||||
The table contains key-value pairs where the key is where the redirect file needs to be created, as an absolute path from the build directory, (e.g. `/appendices/bibliography.html`).
|
||||
The value can be any valid URI the browser should navigate to (e.g. `https://rust-lang.org/`, `/overview.html`, or `../bibliography.html`).
|
||||
|
||||
This will generate an HTML page which will automatically redirect to the given location.
|
||||
Note that the source location does not support `#` anchor redirects.
|
||||
|
||||
## Markdown Renderer
|
||||
|
||||
The Markdown renderer will run preprocessors and then output the resulting
|
||||
Markdown. This is mostly useful for debugging preprocessors, especially in
|
||||
conjunction with `mdbook test` to see the Markdown that `mdbook` is passing
|
||||
to `rustdoc`.
|
||||
|
||||
The Markdown renderer is included with `mdbook` but disabled by default.
|
||||
Enable it by adding an empty table to your `book.toml` as follows:
|
||||
|
||||
```toml
|
||||
[output.markdown]
|
||||
```
|
||||
|
||||
There are no configuration options for the Markdown renderer at this time;
|
||||
only whether it is enabled or disabled.
|
||||
|
||||
See [the preprocessors documentation](preprocessors.md) for how to
|
||||
specify which preprocessors should run before the Markdown renderer.
|
||||
6
guide/src/format/example.rs
Normal file
6
guide/src/format/example.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
fn main() {
|
||||
println!("Hello World!");
|
||||
#
|
||||
# // You can even hide lines! :D
|
||||
# println!("I am hidden! Expand the code snippet to see me");
|
||||
}
|
||||
1
guide/src/format/images/rust-logo-blk.svg
Normal file
1
guide/src/format/images/rust-logo-blk.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="144" width="144" xmlns="http://www.w3.org/2000/svg"><path d="m71.05 23.68c-26.06 0-47.27 21.22-47.27 47.27s21.22 47.27 47.27 47.27 47.27-21.22 47.27-47.27-21.22-47.27-47.27-47.27zm-.07 4.2a3.1 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm7.12 5.12a38.27 38.27 0 0 1 26.2 18.66l-3.67 8.28c-.63 1.43.02 3.11 1.44 3.75l7.06 3.13a38.27 38.27 0 0 1 .08 6.64h-3.93c-.39 0-.55.26-.55.64v1.8c0 4.24-2.39 5.17-4.49 5.4-2 .23-4.21-.84-4.49-2.06-1.18-6.63-3.14-8.04-6.24-10.49 3.85-2.44 7.85-6.05 7.85-10.87 0-5.21-3.57-8.49-6-10.1-3.42-2.25-7.2-2.7-8.22-2.7h-40.6a38.27 38.27 0 0 1 21.41-12.08l4.79 5.02c1.08 1.13 2.87 1.18 4 .09zm-44.2 23.02a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm74.15.14a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm-68.29.5h5.42v24.44h-10.94a38.27 38.27 0 0 1 -1.24-14.61l6.7-2.98c1.43-.64 2.08-2.31 1.44-3.74zm22.62.26h12.91c.67 0 4.71.77 4.71 3.8 0 2.51-3.1 3.41-5.65 3.41h-11.98zm0 17.56h9.89c.9 0 4.83.26 6.08 5.28.39 1.54 1.26 6.56 1.85 8.17.59 1.8 2.98 5.4 5.53 5.4h16.14a38.27 38.27 0 0 1 -3.54 4.1l-6.57-1.41c-1.53-.33-3.04.65-3.37 2.18l-1.56 7.28a38.27 38.27 0 0 1 -31.91-.15l-1.56-7.28c-.33-1.53-1.83-2.51-3.36-2.18l-6.43 1.38a38.27 38.27 0 0 1 -3.32-3.92h31.27c.35 0 .59-.06.59-.39v-11.06c0-.32-.24-.39-.59-.39h-9.15zm-14.43 25.33a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm46.05.14a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11z"/><path d="m115.68 70.95a44.63 44.63 0 0 1 -44.63 44.63 44.63 44.63 0 0 1 -44.63-44.63 44.63 44.63 0 0 1 44.63-44.63 44.63 44.63 0 0 1 44.63 44.63zm-.84-4.31 6.96 4.31-6.96 4.31 5.98 5.59-7.66 2.87 4.78 6.65-8.09 1.32 3.4 7.46-8.19-.29 1.88 7.98-7.98-1.88.29 8.19-7.46-3.4-1.32 8.09-6.65-4.78-2.87 7.66-5.59-5.98-4.31 6.96-4.31-6.96-5.59 5.98-2.87-7.66-6.65 4.78-1.32-8.09-7.46 3.4.29-8.19-7.98 1.88 1.88-7.98-8.19.29 3.4-7.46-8.09-1.32 4.78-6.65-7.66-2.87 5.98-5.59-6.96-4.31 6.96-4.31-5.98-5.59 7.66-2.87-4.78-6.65 8.09-1.32-3.4-7.46 8.19.29-1.88-7.98 7.98 1.88-.29-8.19 7.46 3.4 1.32-8.09 6.65 4.78 2.87-7.66 5.59 5.98 4.31-6.96 4.31 6.96 5.59-5.98 2.87 7.66 6.65-4.78 1.32 8.09 7.46-3.4-.29 8.19 7.98-1.88-1.88 7.98 8.19-.29-3.4 7.46 8.09 1.32-4.78 6.65 7.66 2.87z" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
222
guide/src/format/markdown.md
Normal file
222
guide/src/format/markdown.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# Markdown
|
||||
|
||||
mdBook's [parser](https://github.com/raphlinus/pulldown-cmark) adheres to the [CommonMark](https://commonmark.org/) specification with some extensions described below.
|
||||
You can take a quick [tutorial](https://commonmark.org/help/tutorial/),
|
||||
or [try out](https://spec.commonmark.org/dingus/) CommonMark in real time. A complete Markdown overview is out of scope for
|
||||
this documentation, but below is a high level overview of some of the basics. For a more in-depth experience, check out the
|
||||
[Markdown Guide](https://www.markdownguide.org).
|
||||
|
||||
## Text and Paragraphs
|
||||
|
||||
Text is rendered relatively predictably:
|
||||
|
||||
```markdown
|
||||
Here is a line of text.
|
||||
|
||||
This is a new line.
|
||||
```
|
||||
|
||||
Will look like you might expect:
|
||||
|
||||
Here is a line of text.
|
||||
|
||||
This is a new line.
|
||||
|
||||
## Headings
|
||||
|
||||
Headings use the `#` marker and should be on a line by themselves. More `#` mean smaller headings:
|
||||
|
||||
```markdown
|
||||
### A heading
|
||||
|
||||
Some text.
|
||||
|
||||
#### A smaller heading
|
||||
|
||||
More text.
|
||||
```
|
||||
|
||||
### A heading
|
||||
|
||||
Some text.
|
||||
|
||||
#### A smaller heading
|
||||
|
||||
More text.
|
||||
|
||||
## Lists
|
||||
|
||||
Lists can be unordered or ordered. Ordered lists will order automatically:
|
||||
|
||||
```markdown
|
||||
* milk
|
||||
* eggs
|
||||
* butter
|
||||
|
||||
1. carrots
|
||||
1. celery
|
||||
1. radishes
|
||||
```
|
||||
|
||||
* milk
|
||||
* eggs
|
||||
* butter
|
||||
|
||||
1. carrots
|
||||
1. celery
|
||||
1. radishes
|
||||
|
||||
## Links
|
||||
|
||||
Linking to a URL or local file is easy:
|
||||
|
||||
```markdown
|
||||
Use [mdBook](https://github.com/rust-lang/mdBook).
|
||||
|
||||
Read about [mdBook](mdBook.md).
|
||||
|
||||
A bare url: <https://www.rust-lang.org>.
|
||||
```
|
||||
|
||||
Use [mdBook](https://github.com/rust-lang/mdBook).
|
||||
|
||||
Read about [mdBook](mdBook.md).
|
||||
|
||||
A bare url: <https://www.rust-lang.org>.
|
||||
|
||||
----
|
||||
|
||||
Relative links that end with `.md` will be converted to the `.html` extension.
|
||||
It is recommended to use `.md` links when possible.
|
||||
This is useful when viewing the Markdown file outside of mdBook, for example on GitHub or GitLab which render Markdown automatically.
|
||||
|
||||
Links to `README.md` will be converted to `index.html`.
|
||||
This is done since some services like GitHub render README files automatically, but web servers typically expect the root file to be called `index.html`.
|
||||
|
||||
You can link to individual headings with `#` fragments.
|
||||
For example, `mdbook.md#text-and-paragraphs` would link to the [Text and Paragraphs](#text-and-paragraphs) section above.
|
||||
The ID is created by transforming the heading such as converting to lowercase and replacing spaces with dashes.
|
||||
You can click on any heading and look at the URL in your browser to see what the fragment looks like.
|
||||
|
||||
## Images
|
||||
|
||||
Including images is simply a matter of including a link to them, much like in the _Links_ section above. The following markdown
|
||||
includes the Rust logo SVG image found in the `images` directory at the same level as this file:
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
Produces the following HTML when built with mdBook:
|
||||
|
||||
```html
|
||||
<p><img src="images/rust-logo-blk.svg" alt="The Rust Logo" /></p>
|
||||
```
|
||||
|
||||
Which, of course displays the image like so:
|
||||
|
||||

|
||||
|
||||
## Extensions
|
||||
|
||||
mdBook has several extensions beyond the standard CommonMark specification.
|
||||
|
||||
### Strikethrough
|
||||
|
||||
Text may be rendered with a horizontal line through the center by wrapping the
|
||||
text with two tilde characters on each side:
|
||||
|
||||
```text
|
||||
An example of ~~strikethrough text~~.
|
||||
```
|
||||
|
||||
This example will render as:
|
||||
|
||||
> An example of ~~strikethrough text~~.
|
||||
|
||||
This follows the [GitHub Strikethrough extension][strikethrough].
|
||||
|
||||
### Footnotes
|
||||
|
||||
A footnote generates a small numbered link in the text which when clicked
|
||||
takes the reader to the footnote text at the bottom of the item. The footnote
|
||||
label is written similarly to a link reference with a caret at the front. The
|
||||
footnote text is written like a link reference definition, with the text
|
||||
following the label. Example:
|
||||
|
||||
```text
|
||||
This is an example of a footnote[^note].
|
||||
|
||||
[^note]: This text is the contents of the footnote, which will be rendered
|
||||
towards the bottom.
|
||||
```
|
||||
|
||||
This example will render as:
|
||||
|
||||
> This is an example of a footnote[^note].
|
||||
>
|
||||
> [^note]: This text is the contents of the footnote, which will be rendered
|
||||
> towards the bottom.
|
||||
|
||||
The footnotes are automatically numbered based on the order the footnotes are
|
||||
written.
|
||||
|
||||
### Tables
|
||||
|
||||
Tables can be written using pipes and dashes to draw the rows and columns of
|
||||
the table. These will be translated to HTML table matching the shape. Example:
|
||||
|
||||
```text
|
||||
| Header1 | Header2 |
|
||||
|---------|---------|
|
||||
| abc | def |
|
||||
```
|
||||
|
||||
This example will render similarly to this:
|
||||
|
||||
| Header1 | Header2 |
|
||||
|---------|---------|
|
||||
| abc | def |
|
||||
|
||||
See the specification for the [GitHub Tables extension][tables] for more
|
||||
details on the exact syntax supported.
|
||||
|
||||
### Task lists
|
||||
|
||||
Task lists can be used as a checklist of items that have been completed.
|
||||
Example:
|
||||
|
||||
```md
|
||||
- [x] Complete task
|
||||
- [ ] Incomplete task
|
||||
```
|
||||
|
||||
This will render as:
|
||||
|
||||
> - [x] Complete task
|
||||
> - [ ] Incomplete task
|
||||
|
||||
See the specification for the [task list extension] for more details.
|
||||
|
||||
### Smart punctuation
|
||||
|
||||
Some ASCII punctuation sequences will be automatically turned into fancy Unicode
|
||||
characters:
|
||||
|
||||
| ASCII sequence | Unicode |
|
||||
|----------------|---------|
|
||||
| `--` | – |
|
||||
| `---` | — |
|
||||
| `...` | … |
|
||||
| `"` | “ or ”, depending on context |
|
||||
| `'` | ‘ or ’, depending on context |
|
||||
|
||||
So, no need to manually enter those Unicode characters!
|
||||
|
||||
This feature is disabled by default.
|
||||
To enable it, see the [`output.html.curly-quotes`] config option.
|
||||
|
||||
[strikethrough]: https://github.github.com/gfm/#strikethrough-extension-
|
||||
[tables]: https://github.github.com/gfm/#tables-extension-
|
||||
[task list extension]: https://github.github.com/gfm/#task-list-items-extension-
|
||||
[`output.html.curly-quotes`]: configuration/renderers.md#html-renderer-options
|
||||
@@ -1,9 +1,10 @@
|
||||
# mdBook-specific markdown
|
||||
# mdBook-specific features
|
||||
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them
|
||||
with a `#` [in the same way that Rustdoc does][rustdoc-hide].
|
||||
with a `#` [like you would with Rustdoc][rustdoc-hide].
|
||||
This currently only works with Rust language code blocks.
|
||||
|
||||
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example
|
||||
|
||||
@@ -21,12 +22,73 @@ Will render as
|
||||
```rust
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 7;
|
||||
let y = 6;
|
||||
|
||||
println!("{}", x + y);
|
||||
# }
|
||||
```
|
||||
|
||||
The code block has an eyeball icon (<i class="fa fa-eye"></i>) which will toggle the visibility of the hidden lines.
|
||||
|
||||
## Rust Playground
|
||||
|
||||
Rust language code blocks will automatically get a play button (<i class="fa fa-play"></i>) which will execute the code and display the output just below the code block.
|
||||
This works by sending the code to the [Rust Playground].
|
||||
|
||||
```rust
|
||||
println!("Hello, World!");
|
||||
```
|
||||
|
||||
If there is no `main` function, then the code is automatically wrapped inside one.
|
||||
|
||||
If you wish to disable the play button for a code block, you can include the `noplayground` option on the code block like this:
|
||||
|
||||
~~~markdown
|
||||
```rust,noplayground
|
||||
let mut name = String::new();
|
||||
std::io::stdin().read_line(&mut name).expect("failed to read line");
|
||||
println!("Hello {}!", name);
|
||||
```
|
||||
~~~
|
||||
|
||||
Or, if you wish to disable the play button for all code blocks in your book, you can write the config to the `book.toml` like this.
|
||||
|
||||
```toml
|
||||
[output.html.playground]
|
||||
runnable = false
|
||||
```
|
||||
|
||||
## Rust code block attributes
|
||||
|
||||
Additional attributes can be included in Rust code blocks with comma, space, or tab-separated terms just after the language term. For example:
|
||||
|
||||
~~~markdown
|
||||
```rust,ignore
|
||||
# This example won't be tested.
|
||||
panic!("oops!");
|
||||
```
|
||||
~~~
|
||||
|
||||
These are particularly important when using [`mdbook test`] to test Rust examples.
|
||||
These use the same attributes as [rustdoc attributes], with a few additions:
|
||||
|
||||
* `editable` — Enables the [editor].
|
||||
* `noplayground` — Removes the play button, but will still be tested.
|
||||
* `mdbook-runnable` — Forces the play button to be displayed.
|
||||
This is intended to be combined with the `ignore` attribute for examples that should not be tested, but you want to allow the reader to run.
|
||||
* `ignore` — Will not be tested and no play button is shown, but it is still highlighted as Rust syntax.
|
||||
* `should_panic` — When executed, it should produce a panic.
|
||||
* `no_run` — The code is compiled when tested, but it is not run.
|
||||
The play button is also not shown.
|
||||
* `compile_fail` — The code should fail to compile.
|
||||
* `edition2015`, `edition2018`, `edition2021` — Forces the use of a specific Rust edition.
|
||||
See [`rust.edition`] to set this globally.
|
||||
|
||||
[`mdbook test`]: ../cli/test.md
|
||||
[rustdoc attributes]: https://doc.rust-lang.org/rustdoc/documentation-tests.html#attributes
|
||||
[editor]: theme/editor.md
|
||||
[`rust.edition`]: configuration/general.md#rust-options
|
||||
|
||||
## Including files
|
||||
|
||||
With the following syntax, you can include files into your book:
|
||||
@@ -37,10 +99,10 @@ With the following syntax, you can include files into your book:
|
||||
|
||||
The path to the file has to be relative from the current source file.
|
||||
|
||||
mdBook will interpret included files as markdown. Since the include command
|
||||
mdBook will interpret included files as Markdown. Since the include command
|
||||
is usually used for inserting code snippets and examples, you will often
|
||||
wrap the command with ```` ``` ```` to display the file contents without
|
||||
interpretting them.
|
||||
interpreting them.
|
||||
|
||||
````hbs
|
||||
```
|
||||
@@ -49,7 +111,7 @@ interpretting them.
|
||||
````
|
||||
|
||||
## Including portions of a file
|
||||
Often you only need a specific part of the file e.g. relevant lines for an
|
||||
Often you only need a specific part of the file, e.g. relevant lines for an
|
||||
example. We support four different modes of partial includes:
|
||||
|
||||
```hbs
|
||||
@@ -68,8 +130,8 @@ consisting of lines 2 to 10.
|
||||
To avoid breaking your book when modifying included files, you can also
|
||||
include a specific section using anchors instead of line numbers.
|
||||
An anchor is a pair of matching lines. The line beginning an anchor must
|
||||
match the regex "ANCHOR:\s*[\w_-]+" and similarly the ending line must match
|
||||
the regex "ANCHOR_END:\s*[\w_-]+". This allows you to put anchors in
|
||||
match the regex `ANCHOR:\s*[\w_-]+` and similarly the ending line must match
|
||||
the regex `ANCHOR_END:\s*[\w_-]+`. This allows you to put anchors in
|
||||
any kind of commented line.
|
||||
|
||||
Consider the following file to include:
|
||||
@@ -92,17 +154,17 @@ impl System for MySystem { ... }
|
||||
Then in the book, all you have to do is:
|
||||
````hbs
|
||||
Here is a component:
|
||||
```rust,no_run,noplaypen
|
||||
```rust,no_run,noplayground
|
||||
\{{#include file.rs:component}}
|
||||
```
|
||||
|
||||
Here is a system:
|
||||
```rust,no_run,noplaypen
|
||||
```rust,no_run,noplayground
|
||||
\{{#include file.rs:system}}
|
||||
```
|
||||
|
||||
This is the full file.
|
||||
```rust,no_run,noplaypen
|
||||
```rust,no_run,noplayground
|
||||
\{{#include file.rs:all}}
|
||||
```
|
||||
````
|
||||
@@ -156,7 +218,7 @@ To call the `add_one` function, we pass it an `i32` and bind the returned value
|
||||
#
|
||||
# fn add_one(num: i32) -> i32 {
|
||||
# num + 1
|
||||
#}
|
||||
# }
|
||||
```
|
||||
````
|
||||
|
||||
@@ -170,7 +232,7 @@ That is, it looks like this (click the "expand" icon to see the rest of the file
|
||||
#
|
||||
# fn add_one(num: i32) -> i32 {
|
||||
# num + 1
|
||||
#}
|
||||
# }
|
||||
```
|
||||
|
||||
## Inserting runnable Rust files
|
||||
@@ -178,17 +240,37 @@ That is, it looks like this (click the "expand" icon to see the rest of the file
|
||||
With the following syntax, you can insert runnable Rust files into your book:
|
||||
|
||||
```hbs
|
||||
\{{#playpen file.rs}}
|
||||
\{{#playground 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
|
||||
When play is clicked, the code snippet will be sent to the [Rust Playground] 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}}
|
||||
{{#playground example.rs}}
|
||||
|
||||
[Rust Playpen]: https://play.rust-lang.org/
|
||||
Any additional values passed after the filename will be included as attributes of the code block.
|
||||
For example `\{{#playground example.rs editable}}` will create the code block like the following:
|
||||
|
||||
~~~markdown
|
||||
```rust,editable
|
||||
# Contents of example.rs here.
|
||||
```
|
||||
~~~
|
||||
|
||||
And the `editable` attribute will enable the [editor] as described at [Rust code block attributes](#rust-code-block-attributes).
|
||||
|
||||
[Rust Playground]: https://play.rust-lang.org/
|
||||
|
||||
## Controlling page \<title\>
|
||||
|
||||
A chapter can set a \<title\> that is different from its entry in the table of
|
||||
contents (sidebar) by including a `\{{#title ...}}` near the top of the page.
|
||||
|
||||
```hbs
|
||||
\{{#title My Title}}
|
||||
```
|
||||
99
guide/src/format/summary.md
Normal file
99
guide/src/format/summary.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 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.
|
||||
|
||||
This markdown file must be named `SUMMARY.md`. Its formatting
|
||||
is very strict and must follow the structure outlined below to allow for easy
|
||||
parsing. Any element not specified below, be it formatting or textual, is likely
|
||||
to be ignored at best, or may cause an error when attempting to build the book.
|
||||
|
||||
### Structure
|
||||
|
||||
1. ***Title*** - While optional, it's common practice to begin with a title, generally <code
|
||||
class="language-markdown"># Summary</code>. This is ignored by the parser however, and
|
||||
can be omitted.
|
||||
```markdown
|
||||
# Summary
|
||||
```
|
||||
|
||||
1. ***Prefix Chapter*** - Before the main numbered chapters, prefix chapters can be added
|
||||
that will not be numbered. This is useful for forewords,
|
||||
introductions, etc. There are, however, some constraints. Prefix chapters cannot be
|
||||
nested; they should all be on the root level. And you cannot add
|
||||
prefix chapters once you have added numbered chapters.
|
||||
```markdown
|
||||
[A Prefix Chapter](relative/path/to/markdown.md)
|
||||
|
||||
- [First Chapter](relative/path/to/markdown2.md)
|
||||
```
|
||||
|
||||
1. ***Part Title*** - Headers can be used as a title for the following numbered
|
||||
chapters. This can be used to logically separate different sections
|
||||
of the book. The title is rendered as unclickable text.
|
||||
Titles are optional, and the numbered chapters can be broken into as many
|
||||
parts as desired.
|
||||
```markdown
|
||||
# My Part Title
|
||||
|
||||
- [First Chapter](relative/path/to/markdown.md)
|
||||
```
|
||||
|
||||
1. ***Numbered Chapter*** - Numbered chapters outline the main content of the book
|
||||
and can be nested, resulting in a nice hierarchy
|
||||
(chapters, sub-chapters, etc.).
|
||||
```markdown
|
||||
# Title of Part
|
||||
|
||||
- [First Chapter](relative/path/to/markdown.md)
|
||||
- [Second Chapter](relative/path/to/markdown2.md)
|
||||
- [Sub Chapter](relative/path/to/markdown3.md)
|
||||
|
||||
# Title of Another Part
|
||||
|
||||
- [Another Chapter](relative/path/to/markdown4.md)
|
||||
```
|
||||
Numbered chapters can be denoted with either `-` or `*` (do not mix delimiters).
|
||||
|
||||
1. ***Suffix Chapter*** - Like prefix chapters, suffix chapters are unnumbered, but they come after
|
||||
numbered chapters.
|
||||
```markdown
|
||||
- [Last Chapter](relative/path/to/markdown.md)
|
||||
|
||||
[Title of Suffix Chapter](relative/path/to/markdown2.md)
|
||||
```
|
||||
|
||||
1. ***Draft chapters*** - Draft chapters are chapters without a file and thus content.
|
||||
The purpose of a draft chapter is to signal future chapters still to be written.
|
||||
Or when still laying out the structure of the book to avoid creating the files
|
||||
while you are still changing the structure of the book a lot.
|
||||
Draft chapters will be rendered in the HTML renderer as disabled links in the table
|
||||
of contents, as you can see for the next chapter in the table of contents on the left.
|
||||
Draft chapters are written like normal chapters but without writing the path to the file.
|
||||
```markdown
|
||||
- [Draft Chapter]()
|
||||
```
|
||||
|
||||
1. ***Separators*** - Separators can be added before, in between, and after any other element. They result
|
||||
in an HTML rendered line in the built table of contents. A separator is
|
||||
a line containing exclusively dashes and at least three of them: `---`.
|
||||
```markdown
|
||||
# My Part Title
|
||||
|
||||
[A Prefix Chapter](relative/path/to/markdown.md)
|
||||
|
||||
---
|
||||
|
||||
- [First Chapter](relative/path/to/markdown2.md)
|
||||
```
|
||||
|
||||
|
||||
### Example
|
||||
|
||||
Below is the markdown source for the `SUMMARY.md` for this guide, with the resulting table
|
||||
of contents as rendered to the left.
|
||||
|
||||
```markdown
|
||||
{{#include ../SUMMARY.md}}
|
||||
```
|
||||
50
guide/src/format/theme/README.md
Normal file
50
guide/src/format/theme/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 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.
|
||||
- **_head.hbs_** is appended to the HTML `<head>` section.
|
||||
- **_header.hbs_** content is appended on top of every book page.
|
||||
- **_css/_** contains the CSS files for styling the book.
|
||||
- **_css/chrome.css_** is for UI elements.
|
||||
- **_css/general.css_** is the base styles.
|
||||
- **_css/print.css_** is the style for printer output.
|
||||
- **_css/variables.css_** contains variables used in other CSS files.
|
||||
- **_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.svg_** and **_favicon.png_** the favicon that will be used. The SVG
|
||||
version is used by [newer browsers].
|
||||
|
||||
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` and just
|
||||
remove the files you don't want to override.
|
||||
|
||||
`mdbook init --theme` will not create every file listed above.
|
||||
Some files, such as `head.hbs`, do not have built-in equivalents.
|
||||
Just create the file if you need it.
|
||||
|
||||
If you completely replace all built-in themes, be sure to also set
|
||||
[`output.html.preferred-dark-theme`] in the config, which defaults to the
|
||||
built-in `navy` theme.
|
||||
|
||||
[`output.html.preferred-dark-theme`]: ../configuration/renderers.md#html-renderer-options
|
||||
[newer browsers]: https://caniuse.com/#feat=link-icon-svg
|
||||
@@ -1,25 +1,27 @@
|
||||
# Editor
|
||||
|
||||
In addition to providing runnable code playpens, mdBook optionally allows them
|
||||
In addition to providing runnable code playgrounds, 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]
|
||||
[output.html.playground]
|
||||
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
|
||||
~~~markdown
|
||||
```rust,editable
|
||||
fn main() {
|
||||
let number = 5;
|
||||
print!("{}", number);
|
||||
}
|
||||
```</code></pre>
|
||||
```
|
||||
~~~
|
||||
|
||||
The above will result in this editable playpen:
|
||||
The above will result in this editable playground:
|
||||
|
||||
```rust,editable
|
||||
fn main() {
|
||||
@@ -28,19 +30,19 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
Note the new `Undo Changes` button in the editable playpens.
|
||||
Note the new `Undo Changes` button in the editable playgrounds.
|
||||
|
||||
## 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:
|
||||
the functionality may be overridden by providing a different folder:
|
||||
|
||||
```toml
|
||||
[output.html.playpen]
|
||||
[output.html.playground]
|
||||
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
|
||||
the `theme` folder will need to be overridden as it has some couplings with the
|
||||
default Ace editor.
|
||||
@@ -19,7 +19,8 @@ Here is a list of the properties that are exposed:
|
||||
|
||||
- ***language*** Language of the book in the form `en`, as specified in `book.toml` (if not specified, defaults to `en`). To use in <code
|
||||
class="language-html">\<html lang="{{ language }}"></code> for example.
|
||||
- ***title*** Title of the book, as specified in `book.toml`
|
||||
- ***title*** Title used for the current page. This is identical to `{{ chapter_title }} - {{ book_title }}` unless `book_title` is not set in which case it just defaults to the `chapter_title`.
|
||||
- ***book_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
|
||||
@@ -1,16 +1,67 @@
|
||||
# Syntax Highlighting
|
||||
|
||||
For syntax highlighting I use [Highlight.js](https://highlightjs.org) with a
|
||||
custom theme.
|
||||
mdBook uses [Highlight.js](https://highlightjs.org) with a custom theme
|
||||
for syntax highlighting.
|
||||
|
||||
Automatic language detection has been turned off, so you will probably want to
|
||||
specify the programming language you use like this
|
||||
specify the programming language you use like this:
|
||||
|
||||
<pre><code class="language-markdown">```rust
|
||||
~~~markdown
|
||||
```rust
|
||||
fn main() {
|
||||
// Some code
|
||||
}
|
||||
```</code></pre>
|
||||
```
|
||||
~~~
|
||||
|
||||
## Supported languages
|
||||
|
||||
These languages are supported by default, but you can add more by supplying
|
||||
your own `highlight.js` file:
|
||||
|
||||
- apache
|
||||
- armasm
|
||||
- bash
|
||||
- c
|
||||
- coffeescript
|
||||
- cpp
|
||||
- csharp
|
||||
- css
|
||||
- d
|
||||
- diff
|
||||
- go
|
||||
- handlebars
|
||||
- haskell
|
||||
- http
|
||||
- ini
|
||||
- java
|
||||
- javascript
|
||||
- json
|
||||
- julia
|
||||
- kotlin
|
||||
- less
|
||||
- lua
|
||||
- makefile
|
||||
- markdown
|
||||
- nginx
|
||||
- objectivec
|
||||
- perl
|
||||
- php
|
||||
- plaintext
|
||||
- properties
|
||||
- python
|
||||
- r
|
||||
- ruby
|
||||
- rust
|
||||
- scala
|
||||
- scss
|
||||
- shell
|
||||
- sql
|
||||
- swift
|
||||
- typescript
|
||||
- x86asm
|
||||
- xml
|
||||
- yaml
|
||||
|
||||
## Custom theme
|
||||
Like the rest of the theme, the files used for syntax highlighting can be
|
||||
@@ -61,10 +112,10 @@ 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
|
||||
or could be improved, feel free to [submit a new
|
||||
issue](https://github.com/rust-lang/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.
|
||||
|
||||
Overall the theme should be light and sober, without to many flashy colors.
|
||||
Overall the theme should be light and sober, without too many flashy colors.
|
||||
7
guide/src/guide/README.md
Normal file
7
guide/src/guide/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# User Guide
|
||||
|
||||
This user guide provides an introduction to basic concepts of using mdBook.
|
||||
|
||||
- [Installation](installation.md)
|
||||
- [Reading Books](reading.md)
|
||||
- [Creating a Book](creating.md)
|
||||
109
guide/src/guide/creating.md
Normal file
109
guide/src/guide/creating.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Creating a Book
|
||||
|
||||
Once you have the `mdbook` CLI tool installed, you can use it to create and render a book.
|
||||
|
||||
## Initializing a book
|
||||
|
||||
The `mdbook init` command will create a new directory containing an empty book for you to get started.
|
||||
Give it the name of the directory that you want to create:
|
||||
|
||||
```sh
|
||||
mdbook init my-first-book
|
||||
```
|
||||
|
||||
It will ask a few questions before generating the book.
|
||||
After answering the questions, you can change the current directory into the new book:
|
||||
|
||||
```sh
|
||||
cd my-first-book
|
||||
```
|
||||
|
||||
There are several ways to render a book, but one of the easiest methods is to use the `serve` command, which will build your book and start a local webserver:
|
||||
|
||||
```sh
|
||||
mdbook serve --open
|
||||
```
|
||||
|
||||
The `--open` option will open your default web browser to view your new book.
|
||||
You can leave the server running even while you edit the content of the book, and `mdbook` will automatically rebuild the output *and* automatically refresh your web browser.
|
||||
|
||||
Check out the [CLI Guide](../cli/index.html) for more information about other `mdbook` commands and CLI options.
|
||||
|
||||
## Anatomy of a book
|
||||
|
||||
A book is built from several files which define the settings and layout of the book.
|
||||
|
||||
### `book.toml`
|
||||
|
||||
In the root of your book, there is a `book.toml` file which contains settings for describing how to build your book.
|
||||
This is written in the [TOML markup language](https://toml.io/).
|
||||
The default settings are usually good enough to get you started.
|
||||
When you are interested in exploring more features and options that mdBook provides, check out the [Configuration chapter](../format/configuration/index.html) for more details.
|
||||
|
||||
A very basic `book.toml` can be as simple as this:
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "My First Book"
|
||||
```
|
||||
|
||||
### `SUMMARY.md`
|
||||
|
||||
The next major part of a book is the summary file located at `src/SUMMARY.md`.
|
||||
This file contains a list of all the chapters in the book.
|
||||
Before a chapter can be viewed, it must be added to this list.
|
||||
|
||||
Here's a basic summary file with a few chapters:
|
||||
|
||||
```md
|
||||
# Summary
|
||||
|
||||
[Introduction](README.md)
|
||||
|
||||
- [My First Chapter](my-first-chapter.md)
|
||||
- [Nested example](nested/README.md)
|
||||
- [Sub-chapter](nested/sub-chapter.md)
|
||||
```
|
||||
|
||||
Try opening up `src/SUMMARY.md` in your editor and adding a few chapters.
|
||||
If any of the chapter files do not exist, `mdbook` will automatically create them for you.
|
||||
|
||||
For more details on other formatting options for the summary file, check out the [Summary chapter](../format/summary.md).
|
||||
|
||||
### Source files
|
||||
|
||||
The content of your book is all contained in the `src` directory.
|
||||
Each chapter is a separate Markdown file.
|
||||
Typically, each chapter starts with a level 1 heading with the title of the chapter.
|
||||
|
||||
```md
|
||||
# My First Chapter
|
||||
|
||||
Fill out your content here.
|
||||
```
|
||||
|
||||
The precise layout of the files is up to you.
|
||||
The organization of the files will correspond to the HTML files generated, so keep in mind that the file layout is part of the URL of each chapter.
|
||||
|
||||
While the `mdbook serve` command is running, you can open any of the chapter files and start editing them.
|
||||
Each time you save the file, `mdbook` will rebuild the book and refresh your web browser.
|
||||
|
||||
Check out the [Markdown chapter](../format/markdown.md) for more information on formatting the content of your chapters.
|
||||
|
||||
All other files in the `src` directory will be included in the output.
|
||||
So if you have images or other static files, just include them somewhere in the `src` directory.
|
||||
|
||||
## Publishing a book
|
||||
|
||||
Once you've written your book, you may want to host it somewhere for others to view.
|
||||
The first step is to build the output of the book.
|
||||
This can be done with the `mdbook build` command in the same directory where the `book.toml` file is located:
|
||||
|
||||
```sh
|
||||
mdbook build
|
||||
```
|
||||
|
||||
This will generate a directory named `book` which contains the HTML content of your book.
|
||||
You can then place this directory on any web server to host it.
|
||||
|
||||
For more information about publishing and deploying, check out the [Continuous Integration chapter](../continuous-integration.md) for more.
|
||||
52
guide/src/guide/installation.md
Normal file
52
guide/src/guide/installation.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Installation
|
||||
|
||||
There are multiple ways to install the mdBook CLI tool.
|
||||
Choose any one of the methods below that best suit your needs.
|
||||
If you are installing mdBook for automatic deployment, check out the [continuous integration] chapter for more examples on how to install.
|
||||
|
||||
[continuous integration]: ../continuous-integration.md
|
||||
|
||||
## Pre-compiled binaries
|
||||
|
||||
Executable binaries are available for download on the [GitHub Releases page][releases].
|
||||
Download the binary for your platform (Windows, macOS, or Linux) and extract the archive.
|
||||
The archive contains an `mdbook` executable which you can run to build your books.
|
||||
|
||||
To make it easier to run, put the path to the binary into your `PATH`.
|
||||
|
||||
[releases]: https://github.com/rust-lang/mdBook/releases
|
||||
|
||||
## Build from source using Rust
|
||||
|
||||
To build the `mdbook` executable from source, you will first need to install Rust and Cargo.
|
||||
Follow the instructions on the [Rust installation page].
|
||||
mdBook currently requires at least Rust version 1.54.
|
||||
|
||||
Once you have installed Rust, the following command can be used to build and install mdBook:
|
||||
|
||||
```sh
|
||||
cargo install mdbook
|
||||
```
|
||||
|
||||
This will automatically download mdBook from [crates.io], build it, and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
|
||||
|
||||
To uninstall, run the command `cargo uninstall mdbook`.
|
||||
|
||||
[Rust installation page]: https://www.rust-lang.org/tools/install
|
||||
[crates.io]: https://crates.io/
|
||||
|
||||
### Installing the latest master version
|
||||
|
||||
The version published to crates.io will ever so slightly be behind the version hosted on GitHub.
|
||||
If you need the latest version you can build the git version of mdBook yourself.
|
||||
Cargo makes this ***super easy***!
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/rust-lang/mdBook.git mdbook
|
||||
```
|
||||
|
||||
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
If you are interested in making modifications to mdBook itself, check out the [Contributing Guide] for more information.
|
||||
|
||||
[Contributing Guide]: https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md
|
||||
74
guide/src/guide/reading.md
Normal file
74
guide/src/guide/reading.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Reading Books
|
||||
|
||||
This chapter gives an introduction on how to interact with a book produced by mdBook.
|
||||
This assumes you are reading an HTML book.
|
||||
The options and formatting will be different for other output formats such as PDF.
|
||||
|
||||
A book is organized into *chapters*.
|
||||
Each chapter is a separate page.
|
||||
Chapters can be nested into a hierarchy of sub-chapters.
|
||||
Typically, each chapter will be organized into a series of *headings* to subdivide a chapter.
|
||||
|
||||
## Navigation
|
||||
|
||||
There are several methods for navigating through the chapters of a book.
|
||||
|
||||
The **sidebar** on the left provides a list of all chapters.
|
||||
Clicking on any of the chapter titles will load that page.
|
||||
|
||||
The sidebar may not automatically appear if the window is too narrow, particularly on mobile displays.
|
||||
In that situation, the menu icon (three horizontal bars) at the top-left of the page can be pressed to open and close the sidebar.
|
||||
|
||||
The **arrow buttons** at the bottom of the page can be used to navigate to the previous or the next chapter.
|
||||
|
||||
The **left and right arrow keys** on the keyboard can be used to navigate to the previous or the next chapter.
|
||||
|
||||
## Top menu bar
|
||||
|
||||
The menu bar at the top of the page provides some icons for interacting with the book.
|
||||
The icons displayed will depend on the settings of how the book was generated.
|
||||
|
||||
| Icon | Description |
|
||||
|------|-------------|
|
||||
| <i class="fa fa-bars"></i> | Opens and closes the chapter listing sidebar. |
|
||||
| <i class="fa fa-paint-brush"></i> | Opens a picker to choose a different color theme. |
|
||||
| <i class="fa fa-search"></i> | Opens a search bar for searching within the book. |
|
||||
| <i class="fa fa-print"></i> | Instructs the web browser to print the entire book. |
|
||||
| <i class="fa fa-github"></i> | Opens a link to the website that hosts the source code of the book. |
|
||||
| <i class="fa fa-edit"></i> | Opens a page to directly edit the source of the page you are currently reading. |
|
||||
|
||||
Tapping the menu bar will scroll the page to the top.
|
||||
|
||||
## Search
|
||||
|
||||
Each book has a built-in search system.
|
||||
Pressing the search icon (<i class="fa fa-search"></i>) in the menu bar, or pressing the `S` key on the keyboard will open an input box for entering search terms.
|
||||
Typing some terms will show matching chapters and sections in real time.
|
||||
|
||||
Clicking any of the results will jump to that section.
|
||||
The up and down arrow keys can be used to navigate the results, and enter will open the highlighted section.
|
||||
|
||||
After loading a search result, the matching search terms will be highlighted in the text.
|
||||
Clicking a highlighted word or pressing the `Esc` key will remove the highlighting.
|
||||
|
||||
## Code blocks
|
||||
|
||||
mdBook books are often used for programming projects, and thus support highlighting code blocks and samples.
|
||||
Code blocks may contain several different icons for interacting with them:
|
||||
|
||||
| Icon | Description |
|
||||
|------|-------------|
|
||||
| <i class="fa fa-copy"></i> | Copies the code block into your local clipboard, to allow pasting into another application. |
|
||||
| <i class="fa fa-play"></i> | For Rust code examples, this will execute the sample code and display the compiler output just below the example (see [playground]). |
|
||||
| <i class="fa fa-eye"></i> | For Rust code examples, this will toggle visibility of "hidden" lines. Sometimes, larger examples will hide lines which are not particularly relevant to what is being illustrated (see [hiding code lines]). |
|
||||
| <i class="fa fa-history"></i> | For [editable code examples][editor], this will undo any changes you have made. |
|
||||
|
||||
Here's an example:
|
||||
|
||||
```rust
|
||||
println!("Hello, World!");
|
||||
```
|
||||
|
||||
[editor]: ../format/theme/editor.md
|
||||
[playground]: ../format/mdbook.md#rust-playground
|
||||
[hiding code lines]: ../format/mdbook.md#hiding-code-lines
|
||||
@@ -15,6 +15,10 @@ shout-out to them!
|
||||
- [projektir](https://github.com/projektir)
|
||||
- [Phaiax](https://github.com/Phaiax)
|
||||
- Matt Ickstadt ([mattico](https://github.com/mattico))
|
||||
- Weihang Lo ([@weihanglo](https://github.com/weihanglo))
|
||||
- Weihang Lo ([weihanglo](https://github.com/weihanglo))
|
||||
- Avision Ho ([avisionh](https://github.com/avisionh))
|
||||
- Vivek Akupatni ([apatniv](https://github.com/apatniv))
|
||||
- Eric Huss ([ehuss](https://github.com/ehuss))
|
||||
- Josh Rotenberg ([joshrotenberg](https://github.com/joshrotenberg))
|
||||
|
||||
If you feel you're missing from this list, feel free to add yourself in a PR.
|
||||
If you feel you're missing from this list, feel free to add yourself in a PR.
|
||||
166
src/book/book.rs
166
src/book/book.rs
@@ -7,6 +7,9 @@ use std::path::{Path, PathBuf};
|
||||
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
use crate::config::BuildConfig;
|
||||
use crate::errors::*;
|
||||
use crate::utils::bracket_escape;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Load a book into memory from its `src/` directory.
|
||||
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
|
||||
@@ -14,14 +17,15 @@ pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book>
|
||||
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")?
|
||||
File::open(&summary_md)
|
||||
.with_context(|| format!("Couldn't open SUMMARY.md in {:?} directory", src_dir))?
|
||||
.read_to_string(&mut summary_content)?;
|
||||
|
||||
let summary = parse_summary(&summary_content).chain_err(|| "Summary parsing failed")?;
|
||||
let summary = parse_summary(&summary_content)
|
||||
.with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
|
||||
|
||||
if cfg.create_missing {
|
||||
create_missing(&src_dir, &summary).chain_err(|| "Unable to create missing chapters")?;
|
||||
create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
|
||||
}
|
||||
|
||||
load_book_from_disk(&summary, src_dir)
|
||||
@@ -39,17 +43,21 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||
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)?;
|
||||
if let Some(ref location) = link.location {
|
||||
let filename = src_dir.join(location);
|
||||
if !filename.exists() {
|
||||
if let Some(parent) = filename.parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("Creating missing file {}", filename.display());
|
||||
debug!("Creating missing file {}", filename.display());
|
||||
|
||||
let mut f = File::create(&filename)?;
|
||||
writeln!(f, "# {}", link.name)?;
|
||||
let mut f = File::create(&filename).with_context(|| {
|
||||
format!("Unable to create missing file: {}", filename.display())
|
||||
})?;
|
||||
writeln!(f, "# {}", bracket_escape(&link.name))?;
|
||||
}
|
||||
}
|
||||
|
||||
items.extend(&link.nested_items);
|
||||
@@ -61,7 +69,7 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||
|
||||
/// A dumb tree structure representing a book.
|
||||
///
|
||||
/// For the moment a book is just a collection of `BookItems` which are
|
||||
/// 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()`].
|
||||
@@ -131,6 +139,8 @@ pub enum BookItem {
|
||||
Chapter(Chapter),
|
||||
/// A section separator.
|
||||
Separator,
|
||||
/// A part title.
|
||||
PartTitle(String),
|
||||
}
|
||||
|
||||
impl From<Chapter> for BookItem {
|
||||
@@ -152,8 +162,10 @@ pub struct Chapter {
|
||||
/// 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 path: Option<PathBuf>,
|
||||
/// The chapter's source file, relative to the `SUMMARY.md` file.
|
||||
pub source_path: Option<PathBuf>,
|
||||
/// An ordered list of the names of each chapter above this one in the hierarchy.
|
||||
pub parent_names: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -162,17 +174,37 @@ impl Chapter {
|
||||
pub fn new<P: Into<PathBuf>>(
|
||||
name: &str,
|
||||
content: String,
|
||||
path: P,
|
||||
p: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Chapter {
|
||||
let path: PathBuf = p.into();
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content,
|
||||
path: path.into(),
|
||||
path: Some(path.clone()),
|
||||
source_path: Some(path),
|
||||
parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new draft chapter that is not attached to a source markdown file (and thus
|
||||
/// has no content).
|
||||
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content: String::new(),
|
||||
path: None,
|
||||
source_path: None,
|
||||
parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
|
||||
pub fn is_draft_chapter(&self) -> bool {
|
||||
self.path.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the provided `Summary` to load a `Book` from disk.
|
||||
@@ -202,16 +234,17 @@ pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P)
|
||||
})
|
||||
}
|
||||
|
||||
fn load_summary_item<P: AsRef<Path>>(
|
||||
fn load_summary_item<P: AsRef<Path> + Clone>(
|
||||
item: &SummaryItem,
|
||||
src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Result<BookItem> {
|
||||
match *item {
|
||||
match item {
|
||||
SummaryItem::Separator => Ok(BookItem::Separator),
|
||||
SummaryItem::Link(ref link) => {
|
||||
load_chapter(link, src_dir, parent_names).map(BookItem::Chapter)
|
||||
}
|
||||
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,28 +253,40 @@ fn load_chapter<P: AsRef<Path>>(
|
||||
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()
|
||||
let mut ch = if let Some(ref link_location) = link.location {
|
||||
debug!("Loading {} ({})", link.name, link_location.display());
|
||||
|
||||
let location = if link_location.is_absolute() {
|
||||
link_location.clone()
|
||||
} else {
|
||||
src_dir.join(link_location)
|
||||
};
|
||||
|
||||
let mut f = File::open(&location)
|
||||
.with_context(|| format!("Chapter file not found, {}", link_location.display()))?;
|
||||
|
||||
let mut content = String::new();
|
||||
f.read_to_string(&mut content).with_context(|| {
|
||||
format!("Unable to read \"{}\" ({})", link.name, location.display())
|
||||
})?;
|
||||
|
||||
if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
|
||||
content.replace_range(..3, "");
|
||||
}
|
||||
|
||||
let stripped = location
|
||||
.strip_prefix(&src_dir)
|
||||
.expect("Chapters are always inside a book");
|
||||
|
||||
Chapter::new(&link.name, content, stripped, parent_names.clone())
|
||||
} else {
|
||||
src_dir.join(&link.location)
|
||||
Chapter::new_draft(&link.name, parent_names.clone())
|
||||
};
|
||||
|
||||
let mut f = File::open(&location)
|
||||
.chain_err(|| format!("Chapter file not found, {}", link.location.display()))?;
|
||||
let mut sub_item_parents = parent_names;
|
||||
|
||||
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());
|
||||
@@ -262,8 +307,6 @@ fn load_chapter<P: AsRef<Path>>(
|
||||
///
|
||||
/// 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>,
|
||||
}
|
||||
@@ -341,7 +384,7 @@ And here is some \
|
||||
|
||||
root.nested_items.push(second.clone().into());
|
||||
root.nested_items.push(SummaryItem::Separator);
|
||||
root.nested_items.push(second.clone().into());
|
||||
root.nested_items.push(second.into());
|
||||
|
||||
(root, temp_dir)
|
||||
}
|
||||
@@ -360,6 +403,29 @@ And here is some \
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_a_single_chapter_with_utf8_bom_from_disk() {
|
||||
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
|
||||
|
||||
let chapter_path = temp_dir.path().join("chapter_1.md");
|
||||
File::create(&chapter_path)
|
||||
.unwrap()
|
||||
.write_all(("\u{feff}".to_owned() + DUMMY_SRC).as_bytes())
|
||||
.unwrap();
|
||||
|
||||
let link = Link::new("Chapter 1", chapter_path);
|
||||
|
||||
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");
|
||||
@@ -376,7 +442,8 @@ And here is some \
|
||||
name: String::from("Nested Chapter 1"),
|
||||
content: String::from("Hello World!"),
|
||||
number: Some(SectionNumber(vec![1, 2])),
|
||||
path: PathBuf::from("second.md"),
|
||||
path: Some(PathBuf::from("second.md")),
|
||||
source_path: Some(PathBuf::from("second.md")),
|
||||
parent_names: vec![String::from("Chapter 1")],
|
||||
sub_items: Vec::new(),
|
||||
};
|
||||
@@ -384,12 +451,13 @@ And here is some \
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: PathBuf::from("chapter_1.md"),
|
||||
path: Some(PathBuf::from("chapter_1.md")),
|
||||
source_path: Some(PathBuf::from("chapter_1.md")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(nested.clone()),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(nested.clone()),
|
||||
BookItem::Chapter(nested),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -408,7 +476,8 @@ And here is some \
|
||||
sections: vec![BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
path: PathBuf::from("chapter_1.md"),
|
||||
path: Some(PathBuf::from("chapter_1.md")),
|
||||
source_path: Some(PathBuf::from("chapter_1.md")),
|
||||
..Default::default()
|
||||
})],
|
||||
..Default::default()
|
||||
@@ -448,7 +517,8 @@ And here is some \
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: PathBuf::from("Chapter_1/index.md"),
|
||||
path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
source_path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
@@ -500,7 +570,8 @@ And here is some \
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: PathBuf::from("Chapter_1/index.md"),
|
||||
path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
source_path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
@@ -537,9 +608,10 @@ And here is some \
|
||||
let summary = Summary {
|
||||
numbered_chapters: vec![SummaryItem::Link(Link {
|
||||
name: String::from("Empty"),
|
||||
location: PathBuf::from(""),
|
||||
location: Some(PathBuf::from("")),
|
||||
..Default::default()
|
||||
})],
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -556,7 +628,7 @@ And here is some \
|
||||
let summary = Summary {
|
||||
numbered_chapters: vec![SummaryItem::Link(Link {
|
||||
name: String::from("nested"),
|
||||
location: dir,
|
||||
location: Some(dir),
|
||||
..Default::default()
|
||||
})],
|
||||
..Default::default()
|
||||
|
||||
@@ -28,7 +28,7 @@ impl BookBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the `Config` to be used.
|
||||
/// Set the [`Config`] to be used.
|
||||
pub fn with_config(&mut self, cfg: Config) -> &mut BookBuilder {
|
||||
self.config = cfg;
|
||||
self
|
||||
@@ -64,19 +64,19 @@ impl BookBuilder {
|
||||
info!("Creating a new book with stub content");
|
||||
|
||||
self.create_directory_structure()
|
||||
.chain_err(|| "Unable to create directory structure")?;
|
||||
.with_context(|| "Unable to create directory structure")?;
|
||||
|
||||
self.create_stub_files()
|
||||
.chain_err(|| "Unable to create stub files")?;
|
||||
.with_context(|| "Unable to create stub files")?;
|
||||
|
||||
if self.create_gitignore {
|
||||
self.build_gitignore()
|
||||
.chain_err(|| "Unable to create .gitignore")?;
|
||||
.with_context(|| "Unable to create .gitignore")?;
|
||||
}
|
||||
|
||||
if self.copy_theme {
|
||||
self.copy_across_theme()
|
||||
.chain_err(|| "Unable to copy across the theme")?;
|
||||
.with_context(|| "Unable to copy across the theme")?;
|
||||
}
|
||||
|
||||
self.write_book_toml()?;
|
||||
@@ -97,24 +97,20 @@ impl BookBuilder {
|
||||
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")?;
|
||||
let cfg = toml::to_vec(&self.config).with_context(|| "Unable to serialize the config")?;
|
||||
|
||||
File::create(book_toml)
|
||||
.chain_err(|| "Couldn't create book.toml")?
|
||||
.with_context(|| "Couldn't create book.toml")?
|
||||
.write_all(&cfg)
|
||||
.chain_err(|| "Unable to write config to book.toml")?;
|
||||
.with_context(|| "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);
|
||||
let html_config = self.config.html_config().unwrap_or_default();
|
||||
let themedir = html_config.theme_dir(&self.root);
|
||||
|
||||
if !themedir.exists() {
|
||||
debug!(
|
||||
@@ -128,7 +124,9 @@ impl BookBuilder {
|
||||
index.write_all(theme::INDEX)?;
|
||||
|
||||
let cssdir = themedir.join("css");
|
||||
fs::create_dir(&cssdir)?;
|
||||
if !cssdir.exists() {
|
||||
fs::create_dir(&cssdir)?;
|
||||
}
|
||||
|
||||
let mut general_css = File::create(cssdir.join("general.css"))?;
|
||||
general_css.write_all(theme::GENERAL_CSS)?;
|
||||
@@ -136,14 +134,19 @@ impl BookBuilder {
|
||||
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)?;
|
||||
if html_config.print.enable {
|
||||
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)?;
|
||||
favicon.write_all(theme::FAVICON_PNG)?;
|
||||
|
||||
let mut favicon = File::create(themedir.join("favicon.svg"))?;
|
||||
favicon.write_all(theme::FAVICON_SVG)?;
|
||||
|
||||
let mut js = File::create(themedir.join("book.js"))?;
|
||||
js.write_all(theme::JS)?;
|
||||
@@ -174,13 +177,14 @@ impl BookBuilder {
|
||||
let summary = src_dir.join("SUMMARY.md");
|
||||
if !summary.exists() {
|
||||
trace!("No summary found creating stub summary and chapter_1.md.");
|
||||
let mut f = File::create(&summary).chain_err(|| "Unable to create SUMMARY.md")?;
|
||||
let mut f = File::create(&summary).with_context(|| "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")?;
|
||||
let mut f =
|
||||
File::create(&chapter_1).with_context(|| "Unable to create chapter_1.md")?;
|
||||
writeln!(f, "# Chapter 1")?;
|
||||
} else {
|
||||
trace!("Existing summary found, no need to create stub files.");
|
||||
|
||||
366
src/book/mod.rs
366
src/book/mod.rs
@@ -5,6 +5,7 @@
|
||||
//!
|
||||
//! [1]: ../index.html
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod book;
|
||||
mod init;
|
||||
mod summary;
|
||||
@@ -19,6 +20,7 @@ use std::process::Command;
|
||||
use std::string::ToString;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use toml::Value;
|
||||
use topological_sort::TopologicalSort;
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::preprocess::{
|
||||
@@ -27,7 +29,7 @@ use crate::preprocess::{
|
||||
use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
|
||||
use crate::utils;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::config::{Config, RustEdition};
|
||||
|
||||
/// The object used to manage and build a book.
|
||||
pub struct MDBook {
|
||||
@@ -39,7 +41,7 @@ pub struct MDBook {
|
||||
pub book: Book,
|
||||
renderers: Vec<Box<dyn Renderer>>,
|
||||
|
||||
/// List of pre-processors to be run on the book
|
||||
/// List of pre-processors to be run on the book.
|
||||
preprocessors: Vec<Box<dyn Preprocessor>>,
|
||||
}
|
||||
|
||||
@@ -68,6 +70,20 @@ impl MDBook {
|
||||
|
||||
config.update_from_env();
|
||||
|
||||
if config
|
||||
.html_config()
|
||||
.map_or(false, |html| html.google_analytics.is_some())
|
||||
{
|
||||
warn!(
|
||||
"The output.html.google-analytics field has been deprecated; \
|
||||
it will be removed in a future release.\n\
|
||||
Consider placing the appropriate site tag code into the \
|
||||
theme/head.hbs file instead.\n\
|
||||
The tracking code may be found in the Google Analytics Admin page.\n\
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
if log_enabled!(log::Level::Trace) {
|
||||
for line in format!("Config: {:#?}", config).lines() {
|
||||
trace!("{}", line);
|
||||
@@ -77,7 +93,7 @@ impl MDBook {
|
||||
MDBook::load_with_config(book_root, config)
|
||||
}
|
||||
|
||||
/// Load a book from its root directory using a custom 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();
|
||||
|
||||
@@ -96,7 +112,7 @@ impl MDBook {
|
||||
})
|
||||
}
|
||||
|
||||
/// Load a book from its root directory using a custom config and a custom summary.
|
||||
/// Load a book from its root directory using a custom `Config` and a custom summary.
|
||||
pub fn load_with_config_and_summary<P: Into<PathBuf>>(
|
||||
book_root: P,
|
||||
config: Config,
|
||||
@@ -120,19 +136,18 @@ impl MDBook {
|
||||
}
|
||||
|
||||
/// Returns a flat depth-first iterator over the elements of the book,
|
||||
/// it returns an [BookItem enum](bookitem.html):
|
||||
/// it returns a [`BookItem`] enum:
|
||||
/// `(section: String, bookitem: &BookItem)`
|
||||
///
|
||||
/// ```no_run
|
||||
/// # 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 => {},
|
||||
/// BookItem::PartTitle(ref title) => {}
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
@@ -143,7 +158,6 @@ impl MDBook {
|
||||
/// // 2. Chapter 2
|
||||
/// //
|
||||
/// // etc.
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn iter(&self) -> BookItems<'_> {
|
||||
self.book.iter()
|
||||
@@ -181,7 +195,7 @@ impl MDBook {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run the entire build process for a particular `Renderer`.
|
||||
/// Run the entire build process for a particular [`Renderer`].
|
||||
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
||||
let mut preprocessed_book = self.book.clone();
|
||||
let preprocess_ctx = PreprocessorContext::new(
|
||||
@@ -197,37 +211,34 @@ impl MDBook {
|
||||
}
|
||||
}
|
||||
|
||||
info!("Running the {} backend", renderer.name());
|
||||
self.render(&preprocessed_book, renderer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, preprocessed_book: &Book, renderer: &dyn Renderer) -> Result<()> {
|
||||
let name = renderer.name();
|
||||
let build_dir = self.build_dir_for(name);
|
||||
|
||||
let render_context = RenderContext::new(
|
||||
let mut render_context = RenderContext::new(
|
||||
self.root.clone(),
|
||||
preprocessed_book.clone(),
|
||||
preprocessed_book,
|
||||
self.config.clone(),
|
||||
build_dir,
|
||||
);
|
||||
render_context
|
||||
.chapter_titles
|
||||
.extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
|
||||
|
||||
info!("Running the {} backend", renderer.name());
|
||||
renderer
|
||||
.render(&render_context)
|
||||
.chain_err(|| "Rendering failed")
|
||||
.with_context(|| "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)
|
||||
/// The only requirement is that your renderer implement the [`Renderer`]
|
||||
/// trait.
|
||||
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.
|
||||
/// Register a [`Preprocessor`] to be used when rendering the book.
|
||||
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
||||
self.preprocessors.push(Box::new(preprocessor));
|
||||
self
|
||||
@@ -251,32 +262,55 @@ impl MDBook {
|
||||
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
||||
// actual markdown files.
|
||||
|
||||
let mut failed = false;
|
||||
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);
|
||||
info!("Testing file: {:?}", path);
|
||||
let chapter_path = match ch.path {
|
||||
Some(ref path) if !path.as_os_str().is_empty() => path,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// write preprocessed file to tempdir
|
||||
let path = temp_dir.path().join(&ch.path);
|
||||
let mut tmpf = utils::fs::create_file(&path)?;
|
||||
tmpf.write_all(ch.content.as_bytes())?;
|
||||
let path = self.source_dir().join(&chapter_path);
|
||||
info!("Testing file: {:?}", path);
|
||||
|
||||
let output = Command::new("rustdoc")
|
||||
.arg(&path)
|
||||
.arg("--test")
|
||||
.args(&library_args)
|
||||
.output()?;
|
||||
// write preprocessed file to tempdir
|
||||
let path = temp_dir.path().join(&chapter_path);
|
||||
let mut tmpf = utils::fs::create_file(&path)?;
|
||||
tmpf.write_all(ch.content.as_bytes())?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(ErrorKind::Subprocess(
|
||||
"Rustdoc returned an error".to_string(),
|
||||
output
|
||||
));
|
||||
let mut cmd = Command::new("rustdoc");
|
||||
cmd.arg(&path).arg("--test").args(&library_args);
|
||||
|
||||
if let Some(edition) = self.config.rust.edition {
|
||||
match edition {
|
||||
RustEdition::E2015 => {
|
||||
cmd.args(&["--edition", "2015"]);
|
||||
}
|
||||
RustEdition::E2018 => {
|
||||
cmd.args(&["--edition", "2018"]);
|
||||
}
|
||||
RustEdition::E2021 => {
|
||||
cmd.args(&["--edition", "2021"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
failed = true;
|
||||
error!(
|
||||
"rustdoc returned an error:\n\
|
||||
\n--- stdout\n{}\n--- stderr\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
bail!("One or more tests failed");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -284,7 +318,7 @@ impl MDBook {
|
||||
/// 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
|
||||
/// `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):
|
||||
@@ -352,12 +386,7 @@ fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
|
||||
renderers
|
||||
}
|
||||
|
||||
fn default_preprocessors() -> Vec<Box<dyn Preprocessor>> {
|
||||
vec![
|
||||
Box::new(LinkPreprocessor::new()),
|
||||
Box::new(IndexPreprocessor::new()),
|
||||
]
|
||||
}
|
||||
const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
|
||||
|
||||
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
|
||||
let name = pre.name();
|
||||
@@ -366,36 +395,127 @@ fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
|
||||
|
||||
/// Look at the `MDBook` and try to figure out what preprocessors to run.
|
||||
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
|
||||
let mut preprocessors = Vec::new();
|
||||
// Collect the names of all preprocessors intended to be run, and the order
|
||||
// in which they should be run.
|
||||
let mut preprocessor_names = TopologicalSort::<String>::new();
|
||||
|
||||
if config.build.use_default_preprocessors {
|
||||
preprocessors.extend(default_preprocessors());
|
||||
for name in DEFAULT_PREPROCESSORS {
|
||||
preprocessor_names.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
|
||||
for key in preprocessor_table.keys() {
|
||||
match key.as_ref() {
|
||||
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
|
||||
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
|
||||
name => preprocessors.push(interpret_custom_preprocessor(
|
||||
name,
|
||||
&preprocessor_table[name],
|
||||
)),
|
||||
for (name, table) in preprocessor_table.iter() {
|
||||
preprocessor_names.insert(name.to_string());
|
||||
|
||||
let exists = |name| {
|
||||
(config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
|
||||
|| preprocessor_table.contains_key(name)
|
||||
};
|
||||
|
||||
if let Some(before) = table.get("before") {
|
||||
let before = before.as_array().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.before to be an array",
|
||||
name
|
||||
))
|
||||
})?;
|
||||
for after in before {
|
||||
let after = after.as_str().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.before to contain strings",
|
||||
name
|
||||
))
|
||||
})?;
|
||||
|
||||
if !exists(after) {
|
||||
// Only warn so that preprocessors can be toggled on and off (e.g. for
|
||||
// troubleshooting) without having to worry about order too much.
|
||||
warn!(
|
||||
"preprocessor.{}.after contains \"{}\", which was not found",
|
||||
name, after
|
||||
);
|
||||
} else {
|
||||
preprocessor_names.add_dependency(name, after);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(after) = table.get("after") {
|
||||
let after = after.as_array().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.after to be an array",
|
||||
name
|
||||
))
|
||||
})?;
|
||||
for before in after {
|
||||
let before = before.as_str().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.after to contain strings",
|
||||
name
|
||||
))
|
||||
})?;
|
||||
|
||||
if !exists(before) {
|
||||
// See equivalent warning above for rationale
|
||||
warn!(
|
||||
"preprocessor.{}.before contains \"{}\", which was not found",
|
||||
name, before
|
||||
);
|
||||
} else {
|
||||
preprocessor_names.add_dependency(before, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(preprocessors)
|
||||
// Now that all links have been established, queue preprocessors in a suitable order
|
||||
let mut preprocessors = Vec::with_capacity(preprocessor_names.len());
|
||||
// `pop_all()` returns an empty vector when no more items are not being depended upon
|
||||
for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
|
||||
.take_while(|names| !names.is_empty())
|
||||
{
|
||||
// The `topological_sort` crate does not guarantee a stable order for ties, even across
|
||||
// runs of the same program. Thus, we break ties manually by sorting.
|
||||
// Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
|
||||
// values ([1]), which may not be an alphabetical sort.
|
||||
// As mentioned in [1], doing so depends on locale, which is not desirable for deciding
|
||||
// preprocessor execution order.
|
||||
// [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
|
||||
names.sort();
|
||||
for name in names {
|
||||
let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
|
||||
"links" => Box::new(LinkPreprocessor::new()),
|
||||
"index" => Box::new(IndexPreprocessor::new()),
|
||||
_ => {
|
||||
// The only way to request a custom preprocessor is through the `preprocessor`
|
||||
// table, so it must exist, be a table, and contain the key.
|
||||
let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name];
|
||||
let command = get_custom_preprocessor_cmd(&name, table);
|
||||
Box::new(CmdPreprocessor::new(name, command))
|
||||
}
|
||||
};
|
||||
preprocessors.push(preprocessor);
|
||||
}
|
||||
}
|
||||
|
||||
// "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
|
||||
// Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
|
||||
if preprocessor_names.is_empty() {
|
||||
Ok(preprocessors)
|
||||
} else {
|
||||
Err(Error::msg("Cyclic dependency detected in preprocessors"))
|
||||
}
|
||||
}
|
||||
|
||||
fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocessor> {
|
||||
let command = table
|
||||
fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
|
||||
table
|
||||
.get("command")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
|
||||
Box::new(CmdPreprocessor::new(key.to_string(), command.to_string()))
|
||||
.unwrap_or_else(|| format!("mdbook-{}", key))
|
||||
}
|
||||
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
@@ -408,7 +528,7 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
|
||||
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
|
||||
Box::new(CmdRenderer::new(key.to_string(), command.to_string()))
|
||||
Box::new(CmdRenderer::new(key.to_string(), command))
|
||||
}
|
||||
|
||||
/// Check whether we should run a particular `Preprocessor` in combination
|
||||
@@ -495,8 +615,8 @@ mod tests {
|
||||
|
||||
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");
|
||||
assert_eq!(got.as_ref().unwrap()[0].name(), "index");
|
||||
assert_eq!(got.as_ref().unwrap()[1].name(), "links");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -543,9 +663,121 @@ mod tests {
|
||||
|
||||
// make sure the `preprocessor.random` table exists
|
||||
let random = cfg.get_preprocessor("random").unwrap();
|
||||
let random = interpret_custom_preprocessor("random", &Value::Table(random.clone()));
|
||||
let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
|
||||
|
||||
assert_eq!(random.cmd(), "python random.py");
|
||||
assert_eq!(random, "python random.py");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_before_must_be_array() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
before = 0
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
assert!(determine_preprocessors(&cfg).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_after_must_be_array() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
after = 0
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
assert!(determine_preprocessors(&cfg).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_order_is_honored() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
before = [ "last" ]
|
||||
after = [ "index" ]
|
||||
|
||||
[preprocessor.last]
|
||||
after = [ "links", "index" ]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let preprocessors = determine_preprocessors(&cfg).unwrap();
|
||||
let index = |name| {
|
||||
preprocessors
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, preprocessor)| preprocessor.name() == name)
|
||||
.unwrap()
|
||||
.0
|
||||
};
|
||||
let assert_before = |before, after| {
|
||||
if index(before) >= index(after) {
|
||||
eprintln!("Preprocessor order:");
|
||||
for preprocessor in &preprocessors {
|
||||
eprintln!(" {}", preprocessor.name());
|
||||
}
|
||||
panic!("{} should come before {}", before, after);
|
||||
}
|
||||
};
|
||||
|
||||
assert_before("index", "random");
|
||||
assert_before("index", "last");
|
||||
assert_before("random", "last");
|
||||
assert_before("links", "last");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cyclic_dependencies_are_detected() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.links]
|
||||
before = [ "index" ]
|
||||
|
||||
[preprocessor.index]
|
||||
before = [ "links" ]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
assert!(determine_preprocessors(&cfg).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependencies_dont_register_undefined_preprocessors() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.links]
|
||||
before = [ "random" ]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let preprocessors = determine_preprocessors(&cfg).unwrap();
|
||||
|
||||
assert!(!preprocessors
|
||||
.iter()
|
||||
.any(|preprocessor| preprocessor.name() == "random"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
before = [ "links" ]
|
||||
|
||||
[build]
|
||||
use-default-preprocessors = false
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let preprocessors = determine_preprocessors(&cfg).unwrap();
|
||||
|
||||
assert!(!preprocessors
|
||||
.iter()
|
||||
.any(|preprocessor| preprocessor.name() == "links"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::errors::*;
|
||||
use memchr::{self, Memchr};
|
||||
use pulldown_cmark::{self, Event, Tag};
|
||||
use pulldown_cmark::{self, Event, HeadingLevel, Tag};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::iter::FromIterator;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
@@ -25,12 +26,17 @@ use std::path::{Path, PathBuf};
|
||||
/// [Title of prefix element](relative/path/to/markdown.md)
|
||||
/// ```
|
||||
///
|
||||
/// **Part Title:** An optional title for the next collect of numbered chapters. The numbered
|
||||
/// chapters can be broken into as many parts as desired.
|
||||
///
|
||||
/// **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 Part
|
||||
///
|
||||
/// - [Title of the Chapter](relative/path/to/markdown.md)
|
||||
/// ```
|
||||
///
|
||||
@@ -55,7 +61,7 @@ pub struct Summary {
|
||||
pub title: Option<String>,
|
||||
/// Chapters before the main text (e.g. an introduction).
|
||||
pub prefix_chapters: Vec<SummaryItem>,
|
||||
/// The main chapters in the document.
|
||||
/// The main numbered chapters of the book, broken into one or more possibly named parts.
|
||||
pub numbered_chapters: Vec<SummaryItem>,
|
||||
/// Items which come after the main document (e.g. a conclusion).
|
||||
pub suffix_chapters: Vec<SummaryItem>,
|
||||
@@ -71,7 +77,7 @@ pub struct Link {
|
||||
pub name: String,
|
||||
/// The location of the chapter's source file, taking the book's `src`
|
||||
/// directory as the root.
|
||||
pub location: PathBuf,
|
||||
pub location: Option<PathBuf>,
|
||||
/// The section number, if this chapter is in the numbered section.
|
||||
pub number: Option<SectionNumber>,
|
||||
/// Any nested items this chapter may contain.
|
||||
@@ -83,7 +89,7 @@ impl Link {
|
||||
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(),
|
||||
location: Some(location.as_ref().to_path_buf()),
|
||||
number: None,
|
||||
nested_items: Vec::new(),
|
||||
}
|
||||
@@ -94,7 +100,7 @@ impl Default for Link {
|
||||
fn default() -> Self {
|
||||
Link {
|
||||
name: String::new(),
|
||||
location: PathBuf::new(),
|
||||
location: Some(PathBuf::new()),
|
||||
number: None,
|
||||
nested_items: Vec::new(),
|
||||
}
|
||||
@@ -108,6 +114,8 @@ pub enum SummaryItem {
|
||||
Link(Link),
|
||||
/// A separator (`---`).
|
||||
Separator,
|
||||
/// A part title.
|
||||
PartTitle(String),
|
||||
}
|
||||
|
||||
impl SummaryItem {
|
||||
@@ -134,12 +142,13 @@ impl From<Link> for SummaryItem {
|
||||
///
|
||||
/// ```text
|
||||
/// summary ::= title prefix_chapters numbered_chapters
|
||||
/// suffix_chapters
|
||||
/// suffix_chapters
|
||||
/// title ::= "# " TEXT
|
||||
/// | EPSILON
|
||||
/// prefix_chapters ::= item*
|
||||
/// suffix_chapters ::= item*
|
||||
/// numbered_chapters ::= dotted_item+
|
||||
/// numbered_chapters ::= part+
|
||||
/// part ::= title dotted_item+
|
||||
/// dotted_item ::= INDENT* DOT_POINT item
|
||||
/// item ::= link
|
||||
/// | separator
|
||||
@@ -153,15 +162,19 @@ impl From<Link> for SummaryItem {
|
||||
/// > match the following regex: "[^<>\n[]]+".
|
||||
struct SummaryParser<'a> {
|
||||
src: &'a str,
|
||||
stream: pulldown_cmark::OffsetIter<'a>,
|
||||
stream: pulldown_cmark::OffsetIter<'a, 'a>,
|
||||
offset: usize,
|
||||
|
||||
/// We can't actually put an event back into the `OffsetIter` stream, so instead we store it
|
||||
/// here until somebody calls `next_event` again.
|
||||
back: Option<Event<'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
|
||||
/// `$stream.take_while(|e| e != $delimiter).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 {
|
||||
@@ -203,6 +216,7 @@ impl<'a> SummaryParser<'a> {
|
||||
src: text,
|
||||
stream: pulldown_parser,
|
||||
offset: 0,
|
||||
back: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,13 +237,13 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
let prefix_chapters = self
|
||||
.parse_affix(true)
|
||||
.chain_err(|| "There was an error parsing the prefix chapters")?;
|
||||
.with_context(|| "There was an error parsing the prefix chapters")?;
|
||||
let numbered_chapters = self
|
||||
.parse_numbered()
|
||||
.chain_err(|| "There was an error parsing the numbered chapters")?;
|
||||
.parse_parts()
|
||||
.with_context(|| "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")?;
|
||||
.with_context(|| "There was an error parsing the suffix chapters")?;
|
||||
|
||||
Ok(Summary {
|
||||
title,
|
||||
@@ -239,8 +253,7 @@ impl<'a> SummaryParser<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the affix chapters. This expects the first event (start of
|
||||
/// paragraph) to have already been consumed by the previous parser.
|
||||
/// Parse the affix chapters.
|
||||
fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
|
||||
let mut items = Vec::new();
|
||||
debug!(
|
||||
@@ -250,17 +263,19 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::List(..))) => {
|
||||
Some(ev @ Event::Start(Tag::List(..)))
|
||||
| Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
if is_prefix {
|
||||
// we've finished prefix chapters and are at the start
|
||||
// of the numbered section.
|
||||
self.back(ev);
|
||||
break;
|
||||
} else {
|
||||
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
|
||||
}
|
||||
}
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => {
|
||||
let link = self.parse_link(href.to_string())?;
|
||||
let link = self.parse_link(href.to_string());
|
||||
items.push(SummaryItem::Link(link));
|
||||
}
|
||||
Some(Event::Rule) => items.push(SummaryItem::Separator),
|
||||
@@ -272,52 +287,111 @@ impl<'a> SummaryParser<'a> {
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn parse_link(&mut self, href: String) -> Result<Link> {
|
||||
fn parse_parts(&mut self) -> Result<Vec<SummaryItem>> {
|
||||
let mut parts = vec![];
|
||||
|
||||
// We want the section numbers to be continues through all parts.
|
||||
let mut root_number = SectionNumber::default();
|
||||
let mut root_items = 0;
|
||||
|
||||
loop {
|
||||
// Possibly match a title or the end of the "numbered chapters part".
|
||||
let title = match self.next_event() {
|
||||
Some(ev @ Event::Start(Tag::Paragraph)) => {
|
||||
// we're starting the suffix chapters
|
||||
self.back(ev);
|
||||
break;
|
||||
}
|
||||
|
||||
Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..));
|
||||
Some(stringify_events(tags))
|
||||
}
|
||||
|
||||
Some(ev) => {
|
||||
self.back(ev);
|
||||
None
|
||||
}
|
||||
|
||||
None => break, // EOF, bail...
|
||||
};
|
||||
|
||||
// Parse the rest of the part.
|
||||
let numbered_chapters = self
|
||||
.parse_numbered(&mut root_items, &mut root_number)
|
||||
.with_context(|| "There was an error parsing the numbered chapters")?;
|
||||
|
||||
if let Some(title) = title {
|
||||
parts.push(SummaryItem::PartTitle(title));
|
||||
}
|
||||
parts.extend(numbered_chapters);
|
||||
}
|
||||
|
||||
Ok(parts)
|
||||
}
|
||||
|
||||
/// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened.
|
||||
fn parse_link(&mut self, href: String) -> Link {
|
||||
let href = href.replace("%20", " ");
|
||||
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."))
|
||||
let path = if href.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Ok(Link {
|
||||
name,
|
||||
location: PathBuf::from(href.to_string()),
|
||||
number: None,
|
||||
nested_items: Vec::new(),
|
||||
})
|
||||
Some(PathBuf::from(href))
|
||||
};
|
||||
|
||||
Link {
|
||||
name,
|
||||
location: path,
|
||||
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>> {
|
||||
/// Parse the numbered chapters.
|
||||
fn parse_numbered(
|
||||
&mut self,
|
||||
root_items: &mut u32,
|
||||
root_number: &mut SectionNumber,
|
||||
) -> Result<Vec<SummaryItem>> {
|
||||
let mut items = Vec::new();
|
||||
let mut root_items = 0;
|
||||
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 :)
|
||||
// For the first iteration, we want to just skip any opening paragraph tags, as that just
|
||||
// marks the start of the list. But after that, another opening paragraph indicates that we
|
||||
// have started a new part or the suffix chapters.
|
||||
let mut first = true;
|
||||
|
||||
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, root_items);
|
||||
root_items += bunch_of_items.len() as u32;
|
||||
items.extend(bunch_of_items);
|
||||
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Paragraph)) => {
|
||||
// we're starting the suffix chapters
|
||||
Some(ev @ Event::Start(Tag::Paragraph)) => {
|
||||
if !first {
|
||||
// we're starting the suffix chapters
|
||||
self.back(ev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The expectation is that pulldown cmark will terminate a paragraph before a new
|
||||
// heading, so we can always count on this to return without skipping headings.
|
||||
Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
// we're starting a new part
|
||||
self.back(ev);
|
||||
break;
|
||||
}
|
||||
Some(ev @ Event::Start(Tag::List(..))) => {
|
||||
self.back(ev);
|
||||
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, *root_items);
|
||||
*root_items += bunch_of_items.len() as u32;
|
||||
items.extend(bunch_of_items);
|
||||
}
|
||||
Some(Event::Start(other_tag)) => {
|
||||
trace!("Skipping contents of {:?}", other_tag);
|
||||
|
||||
@@ -327,40 +401,42 @@ impl<'a> SummaryParser<'a> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Event::Start(Tag::List(..))) = self.next_event() {
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(Event::Rule) => {
|
||||
items.push(SummaryItem::Separator);
|
||||
if let Some(Event::Start(Tag::List(..))) = self.next_event() {
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
// something else... ignore
|
||||
continue;
|
||||
}
|
||||
|
||||
// something else... ignore
|
||||
Some(_) => {}
|
||||
|
||||
// EOF, bail...
|
||||
None => {
|
||||
// EOF, bail...
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// From now on, we cannot accept any new paragraph opening tags.
|
||||
first = false;
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// Push an event back to the tail of the stream.
|
||||
fn back(&mut self, ev: Event<'a>) {
|
||||
assert!(self.back.is_none());
|
||||
trace!("Back: {:?}", ev);
|
||||
self.back = Some(ev);
|
||||
}
|
||||
|
||||
fn next_event(&mut self) -> Option<Event<'a>> {
|
||||
let next = self.stream.next().map(|(ev, range)| {
|
||||
self.offset = range.start;
|
||||
ev
|
||||
let next = self.back.take().or_else(|| {
|
||||
self.stream.next().map(|(ev, range)| {
|
||||
self.offset = range.start;
|
||||
ev
|
||||
})
|
||||
});
|
||||
|
||||
trace!("Next event: {:?}", next);
|
||||
|
||||
next
|
||||
@@ -410,7 +486,7 @@ impl<'a> SummaryParser<'a> {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Paragraph)) => continue,
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => {
|
||||
let mut link = self.parse_link(href.to_string())?;
|
||||
let mut link = self.parse_link(href.to_string());
|
||||
|
||||
let mut number = parent.clone();
|
||||
number.0.push(num_existing_items as u32 + 1);
|
||||
@@ -418,7 +494,10 @@ impl<'a> SummaryParser<'a> {
|
||||
"Found chapter: {} {} ({})",
|
||||
number,
|
||||
link.name,
|
||||
link.location.display()
|
||||
link.location
|
||||
.as_ref()
|
||||
.map(|p| p.to_str().unwrap_or(""))
|
||||
.unwrap_or("[draft]")
|
||||
);
|
||||
|
||||
link.number = Some(number);
|
||||
@@ -437,19 +516,33 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
fn parse_error<D: Display>(&self, msg: D) -> Error {
|
||||
let (line, col) = self.current_location();
|
||||
|
||||
ErrorKind::ParseError(line, col, msg.to_string()).into()
|
||||
anyhow::anyhow!(
|
||||
"failed to parse SUMMARY.md line {}, column {}: {}",
|
||||
line,
|
||||
col,
|
||||
msg
|
||||
)
|
||||
}
|
||||
|
||||
/// Try to parse the title line.
|
||||
fn parse_title(&mut self) -> Option<String> {
|
||||
if let Some(Event::Start(Tag::Heading(1))) = self.next_event() {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(1));
|
||||
Some(stringify_events(tags))
|
||||
} else {
|
||||
None
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..));
|
||||
return Some(stringify_events(tags));
|
||||
}
|
||||
// Skip a HTML element such as a comment line.
|
||||
Some(Event::Html(_)) => {}
|
||||
// Otherwise, no title.
|
||||
Some(ev) => {
|
||||
self.back(ev);
|
||||
return None;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -475,10 +568,9 @@ fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
|
||||
.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()
|
||||
})
|
||||
.ok_or_else(||
|
||||
anyhow::anyhow!("Unable to get last link because the list of SummaryItems doesn't contain any Links")
|
||||
)
|
||||
}
|
||||
|
||||
/// Removes the styling from a list of Markdown events and returns just the
|
||||
@@ -488,6 +580,7 @@ fn stringify_events(events: Vec<Event<'_>>) -> String {
|
||||
.into_iter()
|
||||
.filter_map(|t| match t {
|
||||
Event::Text(text) | Event::Code(text) => Some(text.into_string()),
|
||||
Event::SoftBreak => Some(String::from(" ")),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
@@ -559,6 +652,18 @@ mod tests {
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_initial_title() {
|
||||
let src = "[Link]()";
|
||||
let mut parser = SummaryParser::new(src);
|
||||
|
||||
assert!(parser.parse_title().is_none());
|
||||
assert!(matches!(
|
||||
parser.next_event(),
|
||||
Some(Event::Start(Tag::Paragraph))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_title_with_styling() {
|
||||
let src = "# My **Awesome** Summary";
|
||||
@@ -589,17 +694,16 @@ mod tests {
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
..Default::default()
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: PathBuf::from("./second.md"),
|
||||
location: Some(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);
|
||||
@@ -610,7 +714,6 @@ mod tests {
|
||||
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);
|
||||
@@ -622,7 +725,6 @@ mod tests {
|
||||
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());
|
||||
@@ -633,19 +735,19 @@ mod tests {
|
||||
let src = "[First](./first.md)";
|
||||
let should_be = Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let _ = parser.stream.next(); // skip past start of paragraph
|
||||
let _ = parser.stream.next(); // Discard opening paragraph
|
||||
|
||||
let href = match parser.stream.next() {
|
||||
Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(),
|
||||
other => panic!("Unreachable, {:?}", other),
|
||||
};
|
||||
|
||||
let got = parser.parse_link(href).unwrap();
|
||||
let got = parser.parse_link(href);
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
@@ -654,16 +756,16 @@ mod tests {
|
||||
let src = "- [First](./first.md)\n";
|
||||
let link = Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
location: Some(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();
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
@@ -675,27 +777,27 @@ mod tests {
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: vec![SummaryItem::Link(Link {
|
||||
name: String::from("Nested"),
|
||||
location: PathBuf::from("./nested.md"),
|
||||
location: Some(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"),
|
||||
location: Some(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();
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
@@ -707,22 +809,60 @@ mod tests {
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
location: Some(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"),
|
||||
location: Some(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(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
|
||||
let got = parser.parse_numbered().unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_titled_parts() {
|
||||
let src = "- [First](./first.md)\n- [Second](./second.md)\n\
|
||||
# Title 2\n- [Third](./third.md)\n\t- [Fourth](./fourth.md)";
|
||||
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: Some(PathBuf::from("./second.md")),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::PartTitle(String::from("Title 2")),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Third"),
|
||||
location: Some(PathBuf::from("./third.md")),
|
||||
number: Some(SectionNumber(vec![3])),
|
||||
nested_items: vec![SummaryItem::Link(Link {
|
||||
name: String::from("Fourth"),
|
||||
location: Some(PathBuf::from("./fourth.md")),
|
||||
number: Some(SectionNumber(vec![3, 1])),
|
||||
nested_items: Vec::new(),
|
||||
})],
|
||||
}),
|
||||
];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser.parse_parts().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
@@ -737,34 +877,41 @@ mod tests {
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
location: Some(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"),
|
||||
location: Some(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();
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_link_location_is_an_error() {
|
||||
fn an_empty_link_location_is_a_draft_chapter() {
|
||||
let src = "- [Empty]()\n";
|
||||
let mut parser = SummaryParser::new(src);
|
||||
parser.stream.next();
|
||||
|
||||
let got = parser.parse_numbered();
|
||||
assert!(got.is_err());
|
||||
let got = parser.parse_numbered(&mut 0, &mut SectionNumber::default());
|
||||
let should_be = vec![SummaryItem::Link(Link {
|
||||
name: String::from("Empty"),
|
||||
location: None,
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
})];
|
||||
|
||||
assert!(got.is_ok());
|
||||
assert_eq!(got.unwrap(), should_be);
|
||||
}
|
||||
|
||||
/// Regression test for https://github.com/rust-lang/mdBook/issues/779
|
||||
@@ -776,31 +923,175 @@ mod tests {
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Separator,
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: PathBuf::from("./second.md"),
|
||||
location: Some(PathBuf::from("./second.md")),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Separator,
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Third"),
|
||||
location: PathBuf::from("./third.md"),
|
||||
location: Some(PathBuf::from("./third.md")),
|
||||
number: Some(SectionNumber(vec![3])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let _ = parser.stream.next();
|
||||
|
||||
let got = parser.parse_numbered().unwrap();
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
/// Regression test for https://github.com/rust-lang/mdBook/issues/1218
|
||||
/// Ensure chapter names spread across multiple lines have spaces between all the words.
|
||||
#[test]
|
||||
fn add_space_for_multi_line_chapter_names() {
|
||||
let src = "- [Chapter\ntitle](./chapter.md)";
|
||||
let should_be = vec![SummaryItem::Link(Link {
|
||||
name: String::from("Chapter title"),
|
||||
location: Some(PathBuf::from("./chapter.md")),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
})];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_space_in_link_destination() {
|
||||
let src = "- [test1](./test%20link1.md)\n- [test2](<./test link2.md>)";
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("test1"),
|
||||
location: Some(PathBuf::from("./test link1.md")),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("test2"),
|
||||
location: Some(PathBuf::from("./test link2.md")),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
];
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_html_comments() {
|
||||
let src = r#"<!--
|
||||
# Title - En
|
||||
-->
|
||||
# Title - Local
|
||||
|
||||
<!--
|
||||
[Prefix 00-01 - En](ch00-01.md)
|
||||
[Prefix 00-02 - En](ch00-02.md)
|
||||
-->
|
||||
[Prefix 00-01 - Local](ch00-01.md)
|
||||
[Prefix 00-02 - Local](ch00-02.md)
|
||||
|
||||
<!--
|
||||
## Section Title - En
|
||||
-->
|
||||
## Section Title - Localized
|
||||
|
||||
<!--
|
||||
- [Ch 01-00 - En](ch01-00.md)
|
||||
- [Ch 01-01 - En](ch01-01.md)
|
||||
- [Ch 01-02 - En](ch01-02.md)
|
||||
-->
|
||||
- [Ch 01-00 - Local](ch01-00.md)
|
||||
- [Ch 01-01 - Local](ch01-01.md)
|
||||
- [Ch 01-02 - Local](ch01-02.md)
|
||||
|
||||
<!--
|
||||
- [Ch 02-00 - En](ch02-00.md)
|
||||
-->
|
||||
- [Ch 02-00 - Local](ch02-00.md)
|
||||
|
||||
<!--
|
||||
[Appendix A - En](appendix-01.md)
|
||||
[Appendix B - En](appendix-02.md)
|
||||
-->`
|
||||
[Appendix A - Local](appendix-01.md)
|
||||
[Appendix B - Local](appendix-02.md)
|
||||
"#;
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
|
||||
// ---- Title ----
|
||||
let title = parser.parse_title();
|
||||
assert_eq!(title, Some(String::from("Title - Local")));
|
||||
|
||||
// ---- Prefix Chapters ----
|
||||
|
||||
let new_affix_item = |name, location| {
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from(name),
|
||||
location: Some(PathBuf::from(location)),
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
|
||||
let should_be = vec![
|
||||
new_affix_item("Prefix 00-01 - Local", "ch00-01.md"),
|
||||
new_affix_item("Prefix 00-02 - Local", "ch00-02.md"),
|
||||
];
|
||||
|
||||
let got = parser.parse_affix(true).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
// ---- Numbered Chapters ----
|
||||
|
||||
let new_numbered_item = |name, location, numbers: &[u32], nested_items| {
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from(name),
|
||||
location: Some(PathBuf::from(location)),
|
||||
number: Some(SectionNumber(numbers.to_vec())),
|
||||
nested_items,
|
||||
})
|
||||
};
|
||||
|
||||
let ch01_nested = vec![
|
||||
new_numbered_item("Ch 01-01 - Local", "ch01-01.md", &[1, 1], vec![]),
|
||||
new_numbered_item("Ch 01-02 - Local", "ch01-02.md", &[1, 2], vec![]),
|
||||
];
|
||||
|
||||
let should_be = vec![
|
||||
new_numbered_item("Ch 01-00 - Local", "ch01-00.md", &[1], ch01_nested),
|
||||
new_numbered_item("Ch 02-00 - Local", "ch02-00.md", &[2], vec![]),
|
||||
];
|
||||
let got = parser.parse_parts().unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
// ---- Suffix Chapters ----
|
||||
|
||||
let should_be = vec![
|
||||
new_affix_item("Appendix A - Local", "appendix-01.md"),
|
||||
new_affix_item("Appendix B - Local", "appendix-02.md"),
|
||||
];
|
||||
|
||||
let got = parser.parse_affix(false).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("build")
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("build")
|
||||
.about("Builds a book from its markdown files")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
.arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook 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'")
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(arg!(-o --open "Opens the compiled book in a web browser"))
|
||||
}
|
||||
|
||||
// Build command implementation
|
||||
@@ -32,7 +38,12 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
|
||||
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"));
|
||||
let path = book.build_dir_for("html").join("index.html");
|
||||
if !path.exists() {
|
||||
error!("No chapter available to open");
|
||||
std::process::exit(1)
|
||||
}
|
||||
open(path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
use crate::get_book_dir;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::errors::*;
|
||||
use anyhow::Context;
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::MDBook;
|
||||
use std::fs;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("clean")
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("clean")
|
||||
.about("Deletes a built book")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
Running this command deletes this directory.{n}\
|
||||
If omitted, mdBook 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::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
|
||||
),
|
||||
)
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
}
|
||||
|
||||
// Clean command implementation
|
||||
@@ -31,7 +36,8 @@ pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
|
||||
};
|
||||
|
||||
if dir_to_remove.exists() {
|
||||
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;
|
||||
fs::remove_dir_all(&dir_to_remove)
|
||||
.with_context(|| "Unable to remove the build directory")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::get_book_dir;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::config;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
@@ -8,16 +8,31 @@ 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")
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("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(arg!([dir]
|
||||
"Directory to create the book in{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(arg!(--theme "Copies the default theme into your source folder"))
|
||||
.arg(arg!(--force "Skips confirmation prompts"))
|
||||
.arg(
|
||||
Arg::new("title")
|
||||
.long("title")
|
||||
.takes_value(true)
|
||||
.help("Sets the book title")
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ignore")
|
||||
.long("ignore")
|
||||
.takes_value(true)
|
||||
.possible_values(&["none", "git"])
|
||||
.help("Creates a VCS ignore file (i.e. .gitignore)")
|
||||
.required(false),
|
||||
)
|
||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
||||
.arg_from_usage("--force 'Skips confirmation prompts'")
|
||||
}
|
||||
|
||||
// Init command implementation
|
||||
@@ -25,18 +40,13 @@ 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")?;
|
||||
let theme_dir = book_dir.join("theme");
|
||||
println!();
|
||||
println!("Copying the default theme to {}", theme_dir.display());
|
||||
// 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()
|
||||
);
|
||||
if !args.is_present("force") && theme_dir.exists() {
|
||||
println!("This could potentially overwrite files already present in that directory.");
|
||||
print!("\nAre you sure you want to continue? (y/n) ");
|
||||
|
||||
@@ -49,13 +59,23 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||
|
||||
if confirm() {
|
||||
builder.create_gitignore(true);
|
||||
if let Some(ignore) = args.value_of("ignore") {
|
||||
match ignore {
|
||||
"git" => builder.create_gitignore(true),
|
||||
_ => builder.create_gitignore(false),
|
||||
};
|
||||
} else {
|
||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||
if confirm() {
|
||||
builder.create_gitignore(true);
|
||||
}
|
||||
}
|
||||
|
||||
config.book.title = request_book_title();
|
||||
config.book.title = if args.is_present("title") {
|
||||
args.value_of("title").map(String::from)
|
||||
} else {
|
||||
request_book_title()
|
||||
};
|
||||
|
||||
if let Some(author) = get_author_name() {
|
||||
debug!("Obtained user name from gitconfig: {:?}", author);
|
||||
@@ -102,8 +122,5 @@ 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,
|
||||
}
|
||||
matches!(&*s.trim(), "Y" | "y" | "yes" | "Yes")
|
||||
}
|
||||
|
||||
202
src/cmd/serve.rs
202
src/cmd/serve.rs
@@ -1,107 +1,105 @@
|
||||
#[cfg(feature = "watch")]
|
||||
use super::watch;
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use iron::headers;
|
||||
use iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use futures_util::sink::SinkExt;
|
||||
use futures_util::StreamExt;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils;
|
||||
use mdbook::utils::fs::get_404_output_file;
|
||||
use mdbook::MDBook;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::broadcast;
|
||||
use warp::ws::Message;
|
||||
use warp::Filter;
|
||||
|
||||
struct ErrorRecover;
|
||||
|
||||
struct NoCache;
|
||||
/// The HTTP endpoint for the websocket used to trigger reloads when a file changes.
|
||||
const LIVE_RELOAD_ENDPOINT: &str = "__livereload";
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("serve")
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("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}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook 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")
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
|
||||
),
|
||||
)
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(
|
||||
Arg::new("hostname")
|
||||
.short('n')
|
||||
.long("hostname")
|
||||
.takes_value(true)
|
||||
.default_value("localhost")
|
||||
.empty_values(false)
|
||||
.forbid_empty_values(true)
|
||||
.help("Hostname to listen on for HTTP connections"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("port")
|
||||
.short("p")
|
||||
Arg::new("port")
|
||||
.short('p')
|
||||
.long("port")
|
||||
.takes_value(true)
|
||||
.default_value("3000")
|
||||
.empty_values(false)
|
||||
.forbid_empty_values(true)
|
||||
.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'")
|
||||
.arg(arg!(-o --open "Opens the compiled book in a web browser"))
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
// Serve 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-hostname").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();
|
||||
}
|
||||
|
||||
let update_config = |book: &mut MDBook| {
|
||||
book.config
|
||||
.set("output.html.live-reload-endpoint", &LIVE_RELOAD_ENDPOINT)
|
||||
.expect("live-reload-endpoint update failed");
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
// Override site-url for local serving of the 404 file
|
||||
book.config.set("output.html.site-url", "/").unwrap();
|
||||
};
|
||||
update_config(&mut book);
|
||||
book.build()?;
|
||||
|
||||
let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html")));
|
||||
chain.link_after(NoCache);
|
||||
chain.link_after(ErrorRecover);
|
||||
let _iron = Iron::new(chain)
|
||||
.http(&*address)
|
||||
.chain_err(|| "Unable to launch the server")?;
|
||||
let sockaddr: SocketAddr = address
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("no address found for {}", address))?;
|
||||
let build_dir = book.build_dir_for("html");
|
||||
let input_404 = book
|
||||
.config
|
||||
.get("output.html.input-404")
|
||||
.map(toml::Value::as_str)
|
||||
.and_then(std::convert::identity) // flatten
|
||||
.map(ToString::to_string);
|
||||
let file_404 = get_404_output_file(&input_404);
|
||||
|
||||
let ws_server =
|
||||
ws::WebSocket::new(|_| |_| Ok(())).chain_err(|| "Unable to start the websocket")?;
|
||||
// A channel used to broadcast to any websockets to reload when a file changes.
|
||||
let (tx, _rx) = tokio::sync::broadcast::channel::<Message>(100);
|
||||
|
||||
let broadcaster = ws_server.broadcaster();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
ws_server.listen(&*ws_address).unwrap();
|
||||
let reload_tx = tx.clone();
|
||||
let thread_handle = std::thread::spawn(move || {
|
||||
serve(build_dir, sockaddr, reload_tx, &file_404);
|
||||
});
|
||||
|
||||
let serving_url = format!("http://{}", address);
|
||||
@@ -117,43 +115,63 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
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());
|
||||
let result = MDBook::load(&book_dir).and_then(|mut b| {
|
||||
update_config(&mut b);
|
||||
b.build()
|
||||
});
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to load the book");
|
||||
utils::log_backtrace(&e);
|
||||
} else {
|
||||
let _ = broadcaster.send("reload");
|
||||
let _ = tx.send(Message::text("reload"));
|
||||
}
|
||||
});
|
||||
|
||||
let _ = thread_handle.join();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl AfterMiddleware for NoCache {
|
||||
fn after(&self, _: &mut Request, mut res: Response) -> IronResult<Response> {
|
||||
res.headers.set(headers::CacheControl(vec![
|
||||
headers::CacheDirective::NoStore,
|
||||
headers::CacheDirective::MaxAge(0u32),
|
||||
]));
|
||||
#[tokio::main]
|
||||
async fn serve(
|
||||
build_dir: PathBuf,
|
||||
address: SocketAddr,
|
||||
reload_tx: broadcast::Sender<Message>,
|
||||
file_404: &str,
|
||||
) {
|
||||
// A warp Filter which captures `reload_tx` and provides an `rx` copy to
|
||||
// receive reload messages.
|
||||
let sender = warp::any().map(move || reload_tx.subscribe());
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
// A warp Filter to handle the livereload endpoint. This upgrades to a
|
||||
// websocket, and then waits for any filesystem change notifications, and
|
||||
// relays them over the websocket.
|
||||
let livereload = warp::path(LIVE_RELOAD_ENDPOINT)
|
||||
.and(warp::ws())
|
||||
.and(sender)
|
||||
.map(|ws: warp::ws::Ws, mut rx: broadcast::Receiver<Message>| {
|
||||
ws.on_upgrade(move |ws| async move {
|
||||
let (mut user_ws_tx, _user_ws_rx) = ws.split();
|
||||
trace!("websocket got connection");
|
||||
if let Ok(m) = rx.recv().await {
|
||||
trace!("notify of reload");
|
||||
let _ = user_ws_tx.send(m).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
// A warp Filter that serves from the filesystem.
|
||||
let book_route = warp::fs::dir(build_dir.clone());
|
||||
// The fallback route for 404 errors
|
||||
let fallback_route = warp::fs::file(build_dir.join(file_404))
|
||||
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
|
||||
let routes = livereload.or(book_route).or(fallback_route);
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// exit if serve panics
|
||||
error!("Unable to serve: {}", panic_info);
|
||||
std::process::exit(1);
|
||||
}));
|
||||
|
||||
warp::serve(routes).run(address).await;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
use crate::get_book_dir;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("test")
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("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}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
.arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook 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")
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(Arg::new("library-path")
|
||||
.short('L')
|
||||
.long("library-path")
|
||||
.value_name("dir")
|
||||
.takes_value(true)
|
||||
.use_delimiter(true)
|
||||
.require_delimiter(true)
|
||||
.multiple(true)
|
||||
.empty_values(false)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.forbid_empty_values(true)
|
||||
.help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
@@ -10,34 +10,55 @@ use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("watch")
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("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}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
.arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook 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'")
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(arg!(-o --open "Opens 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)?;
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
let update_config = |book: &mut MDBook| {
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
};
|
||||
update_config(&mut book);
|
||||
|
||||
if args.is_present("open") {
|
||||
book.build()?;
|
||||
open(book.build_dir_for("html").join("index.html"));
|
||||
let path = book.build_dir_for("html").join("index.html");
|
||||
if !path.exists() {
|
||||
error!("No chapter available to open");
|
||||
std::process::exit(1)
|
||||
}
|
||||
open(path);
|
||||
}
|
||||
|
||||
trigger_on_change(&book, |paths, book_dir| {
|
||||
info!("Files changed: {:?}\nBuilding book...\n", paths);
|
||||
let result = MDBook::load(&book_dir).and_then(|b| b.build());
|
||||
let result = MDBook::load(&book_dir).and_then(|mut b| {
|
||||
update_config(&mut b);
|
||||
b.build()
|
||||
});
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to build the book");
|
||||
@@ -48,7 +69,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_ignored_files(book_root: &PathBuf, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
if paths.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
@@ -71,7 +92,7 @@ fn remove_ignored_files(book_root: &PathBuf, paths: &[PathBuf]) -> Vec<PathBuf>
|
||||
}
|
||||
}
|
||||
|
||||
fn find_gitignore(book_root: &PathBuf) -> Option<PathBuf> {
|
||||
fn find_gitignore(book_root: &Path) -> Option<PathBuf> {
|
||||
book_root
|
||||
.ancestors()
|
||||
.map(|p| p.join(".gitignore"))
|
||||
|
||||
469
src/config.rs
469
src/config.rs
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! The main entrypoint of the `config` module is the `Config` struct. This acts
|
||||
//! essentially as a bag of configuration information, with a couple
|
||||
//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support
|
||||
//! pre-determined tables ([`BookConfig`] and [`BuildConfig`]) as well as support
|
||||
//! for arbitrary data which is exposed to plugins and alternative backends.
|
||||
//!
|
||||
//!
|
||||
@@ -44,12 +44,13 @@
|
||||
//! assert_eq!(got, Some(PathBuf::from("./themes")));
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # fn main() { run().unwrap() }
|
||||
//! # run().unwrap()
|
||||
//! ```
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
@@ -57,12 +58,9 @@ use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use toml::value::Table;
|
||||
use toml::{self, Value};
|
||||
use toml_query::delete::TomlValueDeleteExt;
|
||||
use toml_query::insert::TomlValueInsertExt;
|
||||
use toml_query::read::TomlValueReadExt;
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::utils;
|
||||
use crate::utils::{self, toml_ext::TomlExt};
|
||||
|
||||
/// The overall configuration object for MDBook, essentially an in-memory
|
||||
/// representation of `book.toml`.
|
||||
@@ -72,6 +70,8 @@ pub struct Config {
|
||||
pub book: BookConfig,
|
||||
/// Information about the build environment.
|
||||
pub build: BuildConfig,
|
||||
/// Information about Rust language support.
|
||||
pub rust: RustConfig,
|
||||
rest: Value,
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ impl FromStr for Config {
|
||||
|
||||
/// Load a `Config` from some string.
|
||||
fn from_str(src: &str) -> Result<Self> {
|
||||
toml::from_str(src).chain_err(|| Error::from("Invalid configuration file"))
|
||||
toml::from_str(src).with_context(|| "Invalid configuration file")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,9 +89,9 @@ impl Config {
|
||||
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")?
|
||||
.with_context(|| "Unable to open the configuration file")?
|
||||
.read_to_string(&mut buffer)
|
||||
.chain_err(|| "Couldn't read the file")?;
|
||||
.with_context(|| "Couldn't read the file")?;
|
||||
|
||||
Config::from_str(&buffer)
|
||||
}
|
||||
@@ -122,7 +122,7 @@ impl Config {
|
||||
/// > when building the book with something like
|
||||
/// >
|
||||
/// > ```text
|
||||
/// > $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
|
||||
/// > $ export MDBOOK_BOOK='{"title": "My Awesome Book", "authors": ["Michael-F-Bryan"]}'
|
||||
/// > $ mdbook build
|
||||
/// > ```
|
||||
///
|
||||
@@ -140,6 +140,17 @@ impl Config {
|
||||
let parsed_value = serde_json::from_str(&value)
|
||||
.unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
|
||||
|
||||
if key == "book" || key == "build" {
|
||||
if let serde_json::Value::Object(ref map) = parsed_value {
|
||||
// To `set` each `key`, we wrap them as `prefix.key`
|
||||
for (k, v) in map {
|
||||
let full_key = format!("{}.{}", key, k);
|
||||
self.set(&full_key, v).expect("unreachable");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.set(key, parsed_value).expect("unreachable");
|
||||
}
|
||||
}
|
||||
@@ -147,18 +158,15 @@ impl Config {
|
||||
/// 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
|
||||
/// `output.html.playground` will fetch the "playground" out of the html output
|
||||
/// table).
|
||||
pub fn get(&self, key: &str) -> Option<&Value> {
|
||||
self.rest.read(key).unwrap_or(None)
|
||||
self.rest.read(key)
|
||||
}
|
||||
|
||||
/// Fetch a value from the `Config` so you can mutate it.
|
||||
pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
|
||||
match self.rest.read_mut(key) {
|
||||
Ok(inner) => inner,
|
||||
Err(_) => None,
|
||||
}
|
||||
self.rest.read_mut(key)
|
||||
}
|
||||
|
||||
/// Convenience method for getting the html renderer's configuration.
|
||||
@@ -169,11 +177,14 @@ impl Config {
|
||||
/// HTML renderer is refactored to be less coupled to `mdbook` internals.
|
||||
#[doc(hidden)]
|
||||
pub fn html_config(&self) -> Option<HtmlConfig> {
|
||||
match self.get_deserialized_opt("output.html") {
|
||||
match self
|
||||
.get_deserialized_opt("output.html")
|
||||
.with_context(|| "Parsing configuration [output.html]")
|
||||
{
|
||||
Ok(Some(config)) => Some(config),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
utils::log_backtrace(&e.chain_err(|| "Parsing configuration [output.html]"));
|
||||
utils::log_backtrace(&e);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -201,7 +212,7 @@ impl Config {
|
||||
value
|
||||
.clone()
|
||||
.try_into()
|
||||
.chain_err(|| "Couldn't deserialize the value")
|
||||
.with_context(|| "Couldn't deserialize the value")
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
@@ -213,17 +224,15 @@ impl Config {
|
||||
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")?;
|
||||
let value = Value::try_from(value)
|
||||
.with_context(|| "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);
|
||||
if let Some(key) = index.strip_prefix("book.") {
|
||||
self.book.update_value(key, value);
|
||||
} else if let Some(key) = index.strip_prefix("build.") {
|
||||
self.build.update_value(key, value);
|
||||
} else {
|
||||
self.rest
|
||||
.insert(index, value)
|
||||
.map_err(ErrorKind::TomlQueryError)?;
|
||||
self.rest.insert(index, value);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -264,7 +273,7 @@ impl Config {
|
||||
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 Some(dest) = table.delete("output.html.destination") {
|
||||
if let Ok(destination) = dest.try_into() {
|
||||
cfg.build.build_dir = destination;
|
||||
}
|
||||
@@ -280,10 +289,12 @@ impl Default for Config {
|
||||
Config {
|
||||
book: BookConfig::default(),
|
||||
build: BuildConfig::default(),
|
||||
rust: RustConfig::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)?;
|
||||
@@ -300,10 +311,10 @@ impl<'de> Deserialize<'de> for Config {
|
||||
return Ok(Config::from_legacy(raw));
|
||||
}
|
||||
|
||||
use serde::de::Error;
|
||||
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",
|
||||
));
|
||||
@@ -312,17 +323,26 @@ impl<'de> Deserialize<'de> for Config {
|
||||
|
||||
let book: BookConfig = table
|
||||
.remove("book")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.map(|book| book.try_into().map_err(D::Error::custom))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let build: BuildConfig = table
|
||||
.remove("build")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.map(|build| build.try_into().map_err(D::Error::custom))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let rust: RustConfig = table
|
||||
.remove("rust")
|
||||
.map(|rust| rust.try_into().map_err(D::Error::custom))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Config {
|
||||
book,
|
||||
build,
|
||||
rust,
|
||||
rest: Value::Table(table),
|
||||
})
|
||||
}
|
||||
@@ -330,32 +350,29 @@ impl<'de> Deserialize<'de> for Config {
|
||||
|
||||
impl Serialize for Config {
|
||||
fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
|
||||
use serde::ser::Error;
|
||||
|
||||
// TODO: This should probably be removed and use a derive instead.
|
||||
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"));
|
||||
}
|
||||
};
|
||||
let book_config = Value::try_from(&self.book).expect("should always be serializable");
|
||||
table.insert("book", book_config);
|
||||
|
||||
if self.build != BuildConfig::default() {
|
||||
let build_config = Value::try_from(&self.build).expect("should always be serializable");
|
||||
table.insert("build", build_config);
|
||||
}
|
||||
|
||||
if self.rust != RustConfig::default() {
|
||||
let rust_config = Value::try_from(&self.rust).expect("should always be serializable");
|
||||
table.insert("rust", rust_config);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
key.strip_prefix("MDBOOK_")
|
||||
.map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
|
||||
}
|
||||
|
||||
fn is_legacy_format(table: &Value) -> bool {
|
||||
@@ -368,7 +385,7 @@ fn is_legacy_format(table: &Value) -> bool {
|
||||
];
|
||||
|
||||
for item in &legacy_items {
|
||||
if let Ok(Some(_)) = table.read(item) {
|
||||
if table.read(item).is_some() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -432,8 +449,30 @@ impl Default for BuildConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the Rust compiler(e.g., for playground)
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct RustConfig {
|
||||
/// Rust edition used in playground
|
||||
pub edition: Option<RustEdition>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// Rust edition to use for the code.
|
||||
pub enum RustEdition {
|
||||
/// The 2021 edition of Rust
|
||||
#[serde(rename = "2021")]
|
||||
E2021,
|
||||
/// The 2018 edition of Rust
|
||||
#[serde(rename = "2018")]
|
||||
E2018,
|
||||
/// The 2015 edition of Rust
|
||||
#[serde(rename = "2015")]
|
||||
E2015,
|
||||
}
|
||||
|
||||
/// Configuration for the HTML renderer.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct HtmlConfig {
|
||||
/// The theme directory, if specified.
|
||||
@@ -441,12 +480,14 @@ pub struct HtmlConfig {
|
||||
/// The default theme to use, defaults to 'light'
|
||||
pub default_theme: Option<String>,
|
||||
/// The theme to use if the browser requests the dark version of the site.
|
||||
/// Defaults to the same as 'default_theme'
|
||||
/// Defaults to 'navy'.
|
||||
pub preferred_dark_theme: Option<String>,
|
||||
/// Use "smart quotes" instead of the usual `"` character.
|
||||
pub curly_quotes: bool,
|
||||
/// Should mathjax be enabled?
|
||||
pub mathjax_support: bool,
|
||||
/// Whether to fonts.css and respective font files to the output directory.
|
||||
pub copy_fonts: bool,
|
||||
/// An optional google analytics code.
|
||||
pub google_analytics: Option<String>,
|
||||
/// Additional CSS stylesheets to include in the rendered page's `<head>`.
|
||||
@@ -456,8 +497,11 @@ pub struct HtmlConfig {
|
||||
pub additional_js: Vec<PathBuf>,
|
||||
/// Fold settings.
|
||||
pub fold: Fold,
|
||||
/// Playpen settings.
|
||||
pub playpen: Playpen,
|
||||
/// Playground settings.
|
||||
#[serde(alias = "playpen")]
|
||||
pub playground: Playground,
|
||||
/// Print settings.
|
||||
pub print: Print,
|
||||
/// Don't render section labels.
|
||||
pub no_section_label: bool,
|
||||
/// Search settings. If `None`, the default will be used.
|
||||
@@ -467,20 +511,67 @@ pub struct HtmlConfig {
|
||||
/// FontAwesome icon class to use for the Git repository link.
|
||||
/// Defaults to `fa-github` if `None`.
|
||||
pub git_repository_icon: Option<String>,
|
||||
/// 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.
|
||||
/// Input path for the 404 file, defaults to 404.md, set to "" to disable 404 file output
|
||||
pub input_404: Option<String>,
|
||||
/// Absolute url to site, used to emit correct paths for the 404 page, which might be accessed in a deeply nested directory
|
||||
pub site_url: Option<String>,
|
||||
/// The DNS subdomain or apex domain at which your book will be hosted. This
|
||||
/// string will be written to a file named CNAME in the root of your site,
|
||||
/// as required by GitHub Pages (see [*Managing a custom domain for your
|
||||
/// GitHub Pages site*][custom domain]).
|
||||
///
|
||||
/// [custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
||||
pub cname: Option<String>,
|
||||
/// Edit url template, when set shows a "Suggest an edit" button for
|
||||
/// directly jumping to editing the currently viewed page.
|
||||
/// Contains {path} that is replaced with chapter source file path
|
||||
pub edit_url_template: Option<String>,
|
||||
/// Endpoint of websocket, for livereload usage. Value loaded from .toml file
|
||||
/// is ignored, because our code overrides this field with the value [`LIVE_RELOAD_ENDPOINT`]
|
||||
///
|
||||
/// [`LIVE_RELOAD_ENDPOINT`]: cmd::serve::LIVE_RELOAD_ENDPOINT
|
||||
///
|
||||
/// This config item *should not be edited* by the end user.
|
||||
#[doc(hidden)]
|
||||
pub livereload_url: Option<String>,
|
||||
pub live_reload_endpoint: Option<String>,
|
||||
/// The mapping from old pages to new pages/URLs to use when generating
|
||||
/// redirects.
|
||||
pub redirect: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for HtmlConfig {
|
||||
fn default() -> HtmlConfig {
|
||||
HtmlConfig {
|
||||
theme: None,
|
||||
default_theme: None,
|
||||
preferred_dark_theme: None,
|
||||
curly_quotes: false,
|
||||
mathjax_support: false,
|
||||
copy_fonts: true,
|
||||
google_analytics: None,
|
||||
additional_css: Vec::new(),
|
||||
additional_js: Vec::new(),
|
||||
fold: Fold::default(),
|
||||
playground: Playground::default(),
|
||||
print: Print::default(),
|
||||
no_section_label: false,
|
||||
search: None,
|
||||
git_repository_url: None,
|
||||
git_repository_icon: None,
|
||||
edit_url_template: None,
|
||||
input_404: None,
|
||||
site_url: None,
|
||||
cname: None,
|
||||
live_reload_endpoint: None,
|
||||
redirect: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
pub fn theme_dir(&self, root: &Path) -> PathBuf {
|
||||
match self.theme {
|
||||
Some(ref d) => root.join(d),
|
||||
None => root.join("theme"),
|
||||
@@ -488,6 +579,25 @@ impl HtmlConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for how to render the print icon, print.html, and print.css.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Print {
|
||||
/// Whether print support is enabled.
|
||||
pub enable: bool,
|
||||
/// Insert page breaks between chapters. Default: `true`.
|
||||
pub page_break: bool,
|
||||
}
|
||||
|
||||
impl Default for Print {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enable: true,
|
||||
page_break: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for how to fold chapters of sidebar.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
@@ -500,28 +610,31 @@ pub struct Fold {
|
||||
pub level: u8,
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the the HTML renderer handles the playpen.
|
||||
/// Configuration for tweaking how the the HTML renderer handles the playground.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Playpen {
|
||||
/// Should playpen snippets be editable? Default: `false`.
|
||||
pub struct Playground {
|
||||
/// Should playground snippets be editable? Default: `false`.
|
||||
pub editable: bool,
|
||||
/// Display the copy button. Default: `true`.
|
||||
pub copyable: bool,
|
||||
/// Copy JavaScript files for the editor to the output directory?
|
||||
/// Default: `true`.
|
||||
pub copy_js: bool,
|
||||
/// Display line numbers on playpen snippets. Default: `false`.
|
||||
/// Display line numbers on playground snippets. Default: `false`.
|
||||
pub line_numbers: bool,
|
||||
/// Display the run button. Default: `true`
|
||||
pub runnable: bool,
|
||||
}
|
||||
|
||||
impl Default for Playpen {
|
||||
fn default() -> Playpen {
|
||||
Playpen {
|
||||
impl Default for Playground {
|
||||
fn default() -> Playground {
|
||||
Playground {
|
||||
editable: false,
|
||||
copyable: true,
|
||||
copy_js: true,
|
||||
line_numbers: false,
|
||||
runnable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -551,7 +664,7 @@ pub struct Search {
|
||||
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
|
||||
/// Documents are split into smaller parts, separated 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?
|
||||
@@ -603,6 +716,7 @@ impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::fs::get_404_output_file;
|
||||
|
||||
const COMPLEX_CONFIG: &str = r#"
|
||||
[book]
|
||||
@@ -627,10 +741,14 @@ mod tests {
|
||||
git-repository-url = "https://foo.com/"
|
||||
git-repository-icon = "fa-code-fork"
|
||||
|
||||
[output.html.playpen]
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
editor = "ace"
|
||||
|
||||
[output.html.redirect]
|
||||
"index.html" = "overview.html"
|
||||
"nexted/page.md" = "https://rust-lang.org/"
|
||||
|
||||
[preprocessor.first]
|
||||
|
||||
[preprocessor.second]
|
||||
@@ -653,11 +771,13 @@ mod tests {
|
||||
create_missing: false,
|
||||
use_default_preprocessors: true,
|
||||
};
|
||||
let playpen_should_be = Playpen {
|
||||
let rust_should_be = RustConfig { edition: None };
|
||||
let playground_should_be = Playground {
|
||||
editable: true,
|
||||
copyable: true,
|
||||
copy_js: true,
|
||||
line_numbers: false,
|
||||
runnable: true,
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
@@ -665,9 +785,18 @@ mod tests {
|
||||
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
|
||||
theme: Some(PathBuf::from("./themedir")),
|
||||
default_theme: Some(String::from("rust")),
|
||||
playpen: playpen_should_be,
|
||||
playground: playground_should_be,
|
||||
git_repository_url: Some(String::from("https://foo.com/")),
|
||||
git_repository_icon: Some(String::from("fa-code-fork")),
|
||||
redirect: vec![
|
||||
(String::from("index.html"), String::from("overview.html")),
|
||||
(
|
||||
String::from("nexted/page.md"),
|
||||
String::from("https://rust-lang.org/"),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -675,9 +804,98 @@ mod tests {
|
||||
|
||||
assert_eq!(got.book, book_should_be);
|
||||
assert_eq!(got.build, build_should_be);
|
||||
assert_eq!(got.rust, rust_should_be);
|
||||
assert_eq!(got.html_config().unwrap(), html_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disable_runnable() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
description = "book book book"
|
||||
authors = ["Shogo Takata"]
|
||||
|
||||
[output.html.playground]
|
||||
runnable = false
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert!(!got.html_config().unwrap().playground.runnable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edition_2015() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
src = "./source"
|
||||
[rust]
|
||||
edition = "2015"
|
||||
"#;
|
||||
|
||||
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 got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.book, book_should_be);
|
||||
|
||||
let rust_should_be = RustConfig {
|
||||
edition: Some(RustEdition::E2015),
|
||||
};
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.rust, rust_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edition_2018() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
src = "./source"
|
||||
[rust]
|
||||
edition = "2018"
|
||||
"#;
|
||||
|
||||
let rust_should_be = RustConfig {
|
||||
edition: Some(RustEdition::E2018),
|
||||
};
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.rust, rust_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edition_2021() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
src = "./source"
|
||||
[rust]
|
||||
edition = "2021"
|
||||
"#;
|
||||
|
||||
let rust_should_be = RustConfig {
|
||||
edition: Some(RustEdition::E2021),
|
||||
};
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.rust, rust_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_arbitrary_output_type() {
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
@@ -720,7 +938,7 @@ mod tests {
|
||||
// is happy...
|
||||
let src = COMPLEX_CONFIG;
|
||||
let mut config = Config::from_str(src).unwrap();
|
||||
let key = "output.html.playpen.editable";
|
||||
let key = "output.html.playground.editable";
|
||||
|
||||
assert_eq!(config.get(key).unwrap(), &Value::Boolean(true));
|
||||
*config.get_mut(key).unwrap() = Value::Boolean(false);
|
||||
@@ -812,7 +1030,7 @@ mod tests {
|
||||
fn encode_env_var(key: &str) -> String {
|
||||
format!(
|
||||
"MDBOOK_{}",
|
||||
key.to_uppercase().replace('.', "__").replace("-", "_")
|
||||
key.to_uppercase().replace('.', "__").replace('-', "_")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -836,11 +1054,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::approx_constant)]
|
||||
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 = json!({"array": [1, 2, 3], "number": 13.37});
|
||||
let value_str = serde_json::to_string(&value).unwrap();
|
||||
|
||||
assert!(cfg.get(key).is_none());
|
||||
@@ -870,4 +1087,104 @@ mod tests {
|
||||
|
||||
assert_eq!(cfg.book.title, Some(should_be));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_404_default() {
|
||||
let src = r#"
|
||||
[output.html]
|
||||
destination = "my-book"
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
let html_config = got.html_config().unwrap();
|
||||
assert_eq!(html_config.input_404, None);
|
||||
assert_eq!(&get_404_output_file(&html_config.input_404), "404.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_404_custom() {
|
||||
let src = r#"
|
||||
[output.html]
|
||||
input-404= "missing.md"
|
||||
output-404= "missing.html"
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
let html_config = got.html_config().unwrap();
|
||||
assert_eq!(html_config.input_404, Some("missing.md".to_string()));
|
||||
assert_eq!(&get_404_output_file(&html_config.input_404), "missing.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Invalid configuration file")]
|
||||
fn invalid_language_type_error() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
language = ["en", "pt-br"]
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
src = "./source"
|
||||
"#;
|
||||
|
||||
Config::from_str(src).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Invalid configuration file")]
|
||||
fn invalid_title_type() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = 20
|
||||
language = "en"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
src = "./source"
|
||||
"#;
|
||||
|
||||
Config::from_str(src).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Invalid configuration file")]
|
||||
fn invalid_build_dir_type() {
|
||||
let src = r#"
|
||||
[build]
|
||||
build-dir = 99
|
||||
create-missing = false
|
||||
"#;
|
||||
|
||||
Config::from_str(src).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Invalid configuration file")]
|
||||
fn invalid_rust_edition() {
|
||||
let src = r#"
|
||||
[rust]
|
||||
edition = "1999"
|
||||
"#;
|
||||
|
||||
Config::from_str(src).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_config() {
|
||||
let src = r#"
|
||||
[output.html.print]
|
||||
enable = false
|
||||
"#;
|
||||
let got = Config::from_str(src).unwrap();
|
||||
let html_config = got.html_config().unwrap();
|
||||
assert!(!html_config.print.enable);
|
||||
assert!(html_config.print.page_break);
|
||||
let src = r#"
|
||||
[output.html.print]
|
||||
page-break = false
|
||||
"#;
|
||||
let got = Config::from_str(src).unwrap();
|
||||
let html_config = got.html_config().unwrap();
|
||||
assert!(html_config.print.enable);
|
||||
assert!(!html_config.print.page_break);
|
||||
}
|
||||
}
|
||||
|
||||
54
src/lib.rs
54
src/lib.rs
@@ -76,22 +76,18 @@
|
||||
//! access to the various methods for working with the [`Config`].
|
||||
//!
|
||||
//! [user guide]: https://rust-lang.github.io/mdBook/
|
||||
//! [`RenderContext`]: renderer/struct.RenderContext.html
|
||||
//! [`RenderContext`]: renderer::RenderContext
|
||||
//! [relevant chapter]: https://rust-lang.github.io/mdBook/for_developers/backends.html
|
||||
//! [`Config`]: config/struct.Config.html
|
||||
//! [`Config`]: config::Config
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(rust_2018_idioms)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -118,48 +114,6 @@ pub use crate::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"];
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
/// Error with a TOML file.
|
||||
TomlQueryError(inner: toml_query::error::Error) {
|
||||
description("toml_query error")
|
||||
display("{}", inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Box to halve the size of Error
|
||||
impl From<handlebars::TemplateError> for Error {
|
||||
fn from(e: handlebars::TemplateError) -> Error {
|
||||
From::from(Box::new(e))
|
||||
}
|
||||
}
|
||||
pub(crate) use anyhow::{bail, ensure, Context};
|
||||
pub use anyhow::{Error, Result};
|
||||
}
|
||||
|
||||
101
src/main.rs
101
src/main.rs
@@ -3,8 +3,10 @@ extern crate clap;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::Local;
|
||||
use clap::{App, AppSettings, ArgMatches};
|
||||
use clap::{App, AppSettings, Arg, ArgMatches};
|
||||
use clap_complete::Shell;
|
||||
use env_logger::Builder;
|
||||
use log::LevelFilter;
|
||||
use mdbook::utils;
|
||||
@@ -20,39 +22,35 @@ const VERSION: &str = concat!("v", crate_version!());
|
||||
fn main() {
|
||||
init_logger();
|
||||
|
||||
// Create a list of valid arguments and sub-commands
|
||||
let app = App::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
.version(VERSION)
|
||||
.setting(AppSettings::GlobalVersion)
|
||||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.setting(AppSettings::ColoredHelp)
|
||||
.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/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());
|
||||
let app = create_clap_app();
|
||||
|
||||
// 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),
|
||||
Some(("init", sub_matches)) => cmd::init::execute(sub_matches),
|
||||
Some(("build", sub_matches)) => cmd::build::execute(sub_matches),
|
||||
Some(("clean", sub_matches)) => cmd::clean::execute(sub_matches),
|
||||
#[cfg(feature = "watch")]
|
||||
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches),
|
||||
Some(("watch", 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!(),
|
||||
Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches),
|
||||
Some(("test", sub_matches)) => cmd::test::execute(sub_matches),
|
||||
Some(("completions", sub_matches)) => (|| {
|
||||
let shell: Shell = sub_matches
|
||||
.value_of("shell")
|
||||
.ok_or_else(|| anyhow!("Shell name missing."))?
|
||||
.parse()
|
||||
.map_err(|s| anyhow!("Invalid shell: {}", s))?;
|
||||
|
||||
let mut complete_app = create_clap_app();
|
||||
clap_complete::generate(
|
||||
shell,
|
||||
&mut complete_app,
|
||||
"mdbook",
|
||||
&mut std::io::stdout().lock(),
|
||||
);
|
||||
Ok(())
|
||||
})(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
@@ -62,6 +60,43 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a list of valid arguments and sub-commands
|
||||
fn create_clap_app() -> App<'static> {
|
||||
let app = App::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
.version(VERSION)
|
||||
.setting(AppSettings::PropagateVersion)
|
||||
.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/mdBook",
|
||||
)
|
||||
.subcommand(cmd::init::make_subcommand())
|
||||
.subcommand(cmd::build::make_subcommand())
|
||||
.subcommand(cmd::test::make_subcommand())
|
||||
.subcommand(cmd::clean::make_subcommand())
|
||||
.subcommand(
|
||||
App::new("completions")
|
||||
.about("Generate shell completions for your shell to stdout")
|
||||
.arg(
|
||||
Arg::new("shell")
|
||||
.takes_value(true)
|
||||
.possible_values(Shell::possible_values())
|
||||
.help("the shell to generate completions for")
|
||||
.value_name("SHELL")
|
||||
.required(true),
|
||||
),
|
||||
);
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
let app = app.subcommand(cmd::watch::make_subcommand());
|
||||
#[cfg(feature = "serve")]
|
||||
let app = app.subcommand(cmd::serve::make_subcommand());
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
fn init_logger() {
|
||||
let mut builder = Builder::new();
|
||||
|
||||
@@ -103,7 +138,13 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
||||
}
|
||||
|
||||
fn open<P: AsRef<OsStr>>(path: P) {
|
||||
if let Err(e) = open::that(path) {
|
||||
info!("Opening web browser");
|
||||
if let Err(e) = opener::open(path) {
|
||||
error!("Error opening web browser: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_app() {
|
||||
create_clap_app().debug_assert();
|
||||
}
|
||||
|
||||
@@ -43,13 +43,13 @@ impl CmdPreprocessor {
|
||||
/// A convenience function custom preprocessors can use to parse the input
|
||||
/// written to `stdin` by a `CmdRenderer`.
|
||||
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
|
||||
serde_json::from_reader(reader).chain_err(|| "Unable to parse the input")
|
||||
serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
|
||||
}
|
||||
|
||||
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
|
||||
let stdin = child.stdin.take().expect("Child has stdin");
|
||||
|
||||
if let Err(e) = self.write_input(stdin, &book, &ctx) {
|
||||
if let Err(e) = self.write_input(stdin, book, 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);
|
||||
@@ -100,7 +100,7 @@ impl Preprocessor for CmdPreprocessor {
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.chain_err(|| {
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Unable to start the \"{}\" preprocessor. Is it installed?",
|
||||
self.name()
|
||||
@@ -109,17 +109,28 @@ impl Preprocessor for CmdPreprocessor {
|
||||
|
||||
self.write_input_to_child(&mut child, &book, ctx);
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.chain_err(|| "Error waiting for the preprocessor to complete")?;
|
||||
let output = child.wait_with_output().with_context(|| {
|
||||
format!(
|
||||
"Error waiting for the \"{}\" preprocessor to complete",
|
||||
self.name
|
||||
)
|
||||
})?;
|
||||
|
||||
trace!("{} exited with output: {:?}", self.cmd, output);
|
||||
ensure!(
|
||||
output.status.success(),
|
||||
"The preprocessor exited unsuccessfully"
|
||||
format!(
|
||||
"The \"{}\" preprocessor exited unsuccessfully with {} status",
|
||||
self.name, output.status
|
||||
)
|
||||
);
|
||||
|
||||
serde_json::from_slice(&output.stdout).chain_err(|| "Unable to parse the preprocessed book")
|
||||
serde_json::from_slice(&output.stdout).with_context(|| {
|
||||
format!(
|
||||
"Unable to parse the preprocessed book from \"{}\" processor",
|
||||
self.name
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
@@ -170,15 +181,15 @@ mod tests {
|
||||
use crate::MDBook;
|
||||
use std::path::Path;
|
||||
|
||||
fn book_example() -> MDBook {
|
||||
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
|
||||
fn guide() -> MDBook {
|
||||
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
|
||||
MDBook::load(example).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_write_and_parse_input() {
|
||||
let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string());
|
||||
let md = book_example();
|
||||
let md = guide();
|
||||
let ctx = PreprocessorContext::new(
|
||||
md.root.clone(),
|
||||
md.config.clone(),
|
||||
|
||||
@@ -29,13 +29,15 @@ impl Preprocessor for IndexPreprocessor {
|
||||
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);
|
||||
}
|
||||
if let Some(ref mut path) = ch.path {
|
||||
if is_readme_file(&path) {
|
||||
let mut index_md = source_dir.join(path.with_file_name("index.md"));
|
||||
if index_md.exists() {
|
||||
warn_readme_name_conflict(&path, &&mut index_md);
|
||||
}
|
||||
|
||||
ch.path.set_file_name("index.md");
|
||||
path.set_file_name("index.md");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,7 +22,8 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
///. specified or the lines between specified anchors, and include the rest of the file behind `#`.
|
||||
/// This hides the lines from initial display but shows them when the reader expands the code
|
||||
/// block and provides them to Rustdoc for testing.
|
||||
/// - `{{# playpen}}` - Insert runnable Rust files
|
||||
/// - `{{# playground}}` - Insert runnable Rust files
|
||||
/// - `{{# title}}` - Override \<title\> of a webpage.
|
||||
#[derive(Default)]
|
||||
pub struct LinkPreprocessor;
|
||||
|
||||
@@ -45,14 +46,22 @@ impl Preprocessor for LinkPreprocessor {
|
||||
|
||||
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");
|
||||
if let Some(ref chapter_path) = ch.path {
|
||||
let base = chapter_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;
|
||||
let mut chapter_title = ch.name.clone();
|
||||
let content =
|
||||
replace_all(&ch.content, base, chapter_path, 0, &mut chapter_title);
|
||||
ch.content = content;
|
||||
if chapter_title != ch.name {
|
||||
ctx.chapter_titles
|
||||
.borrow_mut()
|
||||
.insert(chapter_path.clone(), chapter_title);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -60,7 +69,13 @@ impl Preprocessor for LinkPreprocessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_all<P1, P2>(s: &str, path: P1, source: P2, depth: usize) -> String
|
||||
fn replace_all<P1, P2>(
|
||||
s: &str,
|
||||
path: P1,
|
||||
source: P2,
|
||||
depth: usize,
|
||||
chapter_title: &mut String,
|
||||
) -> String
|
||||
where
|
||||
P1: AsRef<Path>,
|
||||
P2: AsRef<Path>,
|
||||
@@ -76,11 +91,17 @@ where
|
||||
for link in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..link.start_index]);
|
||||
|
||||
match link.render_with_path(&path) {
|
||||
match link.render_with_path(&path, chapter_title) {
|
||||
Ok(new_content) => {
|
||||
if depth < MAX_LINK_NESTED_DEPTH {
|
||||
if let Some(rel_path) = link.link_type.relative_path(path) {
|
||||
replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1));
|
||||
replaced.push_str(&replace_all(
|
||||
&new_content,
|
||||
rel_path,
|
||||
source,
|
||||
depth + 1,
|
||||
chapter_title,
|
||||
));
|
||||
} else {
|
||||
replaced.push_str(&new_content);
|
||||
}
|
||||
@@ -94,7 +115,7 @@ where
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error updating \"{}\", {}", link.link_text, e);
|
||||
for cause in e.iter().skip(1) {
|
||||
for cause in e.chain().skip(1) {
|
||||
warn!("Caused By: {}", cause);
|
||||
}
|
||||
|
||||
@@ -113,8 +134,9 @@ where
|
||||
enum LinkType<'a> {
|
||||
Escaped,
|
||||
Include(PathBuf, RangeOrAnchor),
|
||||
Playpen(PathBuf, Vec<&'a str>),
|
||||
Playground(PathBuf, Vec<&'a str>),
|
||||
RustdocInclude(PathBuf, RangeOrAnchor),
|
||||
Title(&'a str),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
@@ -124,6 +146,7 @@ enum RangeOrAnchor {
|
||||
}
|
||||
|
||||
// A range of lines specified with some include directive.
|
||||
#[allow(clippy::enum_variant_names)] // The prefix can't be removed, and is meant to mirror the contained type
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum LineRange {
|
||||
Range(Range<usize>),
|
||||
@@ -182,8 +205,9 @@ impl<'a> LinkType<'a> {
|
||||
match self {
|
||||
LinkType::Escaped => None,
|
||||
LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Playground(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Title(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,6 +278,9 @@ struct Link<'a> {
|
||||
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(title)) if typ.as_str() == "title" => {
|
||||
Some(LinkType::Title(title.as_str()))
|
||||
}
|
||||
(_, Some(typ), Some(rest)) => {
|
||||
let mut path_props = rest.as_str().split_whitespace();
|
||||
let file_arg = path_props.next();
|
||||
@@ -261,7 +288,15 @@ impl<'a> Link<'a> {
|
||||
|
||||
match (typ.as_str(), file_arg) {
|
||||
("include", Some(pth)) => Some(parse_include_path(pth)),
|
||||
("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
|
||||
("playground", Some(pth)) => Some(LinkType::Playground(pth.into(), props)),
|
||||
("playpen", Some(pth)) => {
|
||||
warn!(
|
||||
"the {{{{#playpen}}}} expression has been \
|
||||
renamed to {{{{#playground}}}}, \
|
||||
please update your book to use the new name"
|
||||
);
|
||||
Some(LinkType::Playground(pth.into(), props))
|
||||
}
|
||||
("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)),
|
||||
_ => None,
|
||||
}
|
||||
@@ -282,7 +317,11 @@ impl<'a> Link<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
|
||||
fn render_with_path<P: AsRef<Path>>(
|
||||
&self,
|
||||
base: P,
|
||||
chapter_title: &mut String,
|
||||
) -> Result<String> {
|
||||
let base = base.as_ref();
|
||||
match self.link_type {
|
||||
// omit the escape char
|
||||
@@ -295,7 +334,7 @@ impl<'a> Link<'a> {
|
||||
RangeOrAnchor::Range(range) => take_lines(&s, range.clone()),
|
||||
RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor),
|
||||
})
|
||||
.chain_err(|| {
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
@@ -315,7 +354,7 @@ impl<'a> Link<'a> {
|
||||
take_rustdoc_include_anchored_lines(&s, anchor)
|
||||
}
|
||||
})
|
||||
.chain_err(|| {
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
@@ -323,10 +362,10 @@ impl<'a> Link<'a> {
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::Playpen(ref pat, ref attrs) => {
|
||||
LinkType::Playground(ref pat, ref attrs) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
let contents = fs::read_to_string(&target).chain_err(|| {
|
||||
let mut contents = fs::read_to_string(&target).with_context(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
@@ -334,13 +373,20 @@ impl<'a> Link<'a> {
|
||||
)
|
||||
})?;
|
||||
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
||||
if !contents.ends_with('\n') {
|
||||
contents.push('\n');
|
||||
}
|
||||
Ok(format!(
|
||||
"```{}{}\n{}\n```\n",
|
||||
"```{}{}\n{}```\n",
|
||||
ftype,
|
||||
attrs.join(","),
|
||||
contents
|
||||
))
|
||||
}
|
||||
LinkType::Title(title) => {
|
||||
*chapter_title = title.to_owned();
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,17 +407,17 @@ impl<'a> Iterator for LinkIter<'a> {
|
||||
|
||||
fn find_links(contents: &str) -> LinkIter<'_> {
|
||||
// lazily compute following regex
|
||||
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
|
||||
// r"\\\{\{#.*\}\}|\{\{#([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"
|
||||
r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9_]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([^}]+) # link target path and space separated properties
|
||||
\}\} # link closing parens"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -394,7 +440,21 @@ mod tests {
|
||||
```hbs
|
||||
{{#include file.rs}} << an escaped link!
|
||||
```";
|
||||
assert_eq!(replace_all(start, "", "", 0), end);
|
||||
let mut chapter_title = "test_replace_all_escaped".to_owned();
|
||||
assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_chapter_title() {
|
||||
let start = r"{{#title My Title}}
|
||||
# My Chapter
|
||||
";
|
||||
let end = r"
|
||||
# My Chapter
|
||||
";
|
||||
let mut chapter_title = "test_set_chapter_title".to_owned();
|
||||
assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
|
||||
assert_eq!(chapter_title, "My Title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -405,7 +465,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_links_partial_link() {
|
||||
let s = "Some random text with {{#playpen...";
|
||||
let s = "Some random text with {{#playground...";
|
||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||
let s = "Some random text with {{#include...";
|
||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||
@@ -415,19 +475,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_links_empty_link() {
|
||||
let s = "Some random text with {{#playpen}} and {{#playpen }} {{}} {{#}}...";
|
||||
let s = "Some random text with {{#playground}} and {{#playground }} {{}} {{#}}...";
|
||||
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}}...";
|
||||
let s = "Some random text with {{#playgroundz 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 s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
@@ -437,20 +497,38 @@ mod tests {
|
||||
vec![
|
||||
Link {
|
||||
start_index: 22,
|
||||
end_index: 42,
|
||||
link_type: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
|
||||
link_text: "{{#playpen file.rs}}",
|
||||
end_index: 45,
|
||||
link_type: LinkType::Playground(PathBuf::from("file.rs"), vec![]),
|
||||
link_text: "{{#playground file.rs}}",
|
||||
},
|
||||
Link {
|
||||
start_index: 47,
|
||||
end_index: 68,
|
||||
link_type: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
|
||||
link_text: "{{#playpen test.rs }}",
|
||||
start_index: 50,
|
||||
end_index: 74,
|
||||
link_type: LinkType::Playground(PathBuf::from("test.rs"), vec![]),
|
||||
link_text: "{{#playground test.rs }}",
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_with_special_characters() {
|
||||
let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 57,
|
||||
link_type: LinkType::Playground(PathBuf::from("foo-bar\\baz/_c++.rs"), vec![]),
|
||||
link_text: "{{#playground foo-bar\\baz/_c++.rs}}",
|
||||
},]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_with_range() {
|
||||
let s = "Some random text with {{#include file.rs:10:20}}...";
|
||||
@@ -586,7 +664,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_links_escaped_link() {
|
||||
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
|
||||
let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
@@ -594,18 +672,19 @@ mod tests {
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
start_index: 41,
|
||||
end_index: 74,
|
||||
link_type: LinkType::Escaped,
|
||||
link_text: "\\{{#playpen file.rs editable}}",
|
||||
link_text: "\\{{#playground 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}} ...";
|
||||
fn test_find_playgrounds_with_properties() {
|
||||
let s =
|
||||
"Some random text with escaped playground {{#playground file.rs editable }} and some \
|
||||
more\n text {{#playground my.rs editable no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
@@ -613,19 +692,19 @@ mod tests {
|
||||
res,
|
||||
vec![
|
||||
Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link_type: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
|
||||
link_text: "{{#playpen file.rs editable }}",
|
||||
start_index: 41,
|
||||
end_index: 74,
|
||||
link_type: LinkType::Playground(PathBuf::from("file.rs"), vec!["editable"]),
|
||||
link_text: "{{#playground file.rs editable }}",
|
||||
},
|
||||
Link {
|
||||
start_index: 89,
|
||||
end_index: 136,
|
||||
link_type: LinkType::Playpen(
|
||||
start_index: 95,
|
||||
end_index: 145,
|
||||
link_type: LinkType::Playground(
|
||||
PathBuf::from("my.rs"),
|
||||
vec!["editable", "no_run", "should_panic"],
|
||||
),
|
||||
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
||||
link_text: "{{#playground my.rs editable no_run should_panic}}",
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -633,8 +712,9 @@ mod tests {
|
||||
|
||||
#[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 \
|
||||
let s =
|
||||
"Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
|
||||
insignifficant in escaped link}} some more\n text {{#playground my.rs editable \
|
||||
no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
@@ -643,8 +723,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
res[0],
|
||||
Link {
|
||||
start_index: 38,
|
||||
end_index: 58,
|
||||
start_index: 41,
|
||||
end_index: 61,
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..))
|
||||
@@ -655,8 +735,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
res[1],
|
||||
Link {
|
||||
start_index: 63,
|
||||
end_index: 112,
|
||||
start_index: 66,
|
||||
end_index: 115,
|
||||
link_type: LinkType::Escaped,
|
||||
link_text: "\\{{#contents are insignifficant in escaped link}}",
|
||||
}
|
||||
@@ -664,13 +744,13 @@ mod tests {
|
||||
assert_eq!(
|
||||
res[2],
|
||||
Link {
|
||||
start_index: 130,
|
||||
end_index: 177,
|
||||
link_type: LinkType::Playpen(
|
||||
start_index: 133,
|
||||
end_index: 183,
|
||||
link_type: LinkType::Playground(
|
||||
PathBuf::from("my.rs"),
|
||||
vec!["editable", "no_run", "should_panic"]
|
||||
),
|
||||
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
||||
link_text: "{{#playground my.rs editable no_run should_panic}}",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,12 @@ use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Extra information for a `Preprocessor` to give them more context when
|
||||
/// processing a book.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
@@ -27,6 +31,8 @@ pub struct PreprocessorContext {
|
||||
/// The calling `mdbook` version.
|
||||
pub mdbook_version: String,
|
||||
#[serde(skip)]
|
||||
pub(crate) chapter_titles: RefCell<HashMap<PathBuf, String>>,
|
||||
#[serde(skip)]
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
@@ -38,6 +44,7 @@ impl PreprocessorContext {
|
||||
config,
|
||||
renderer,
|
||||
mdbook_version: crate::MDBOOK_VERSION.to_string(),
|
||||
chapter_titles: RefCell::new(HashMap::new()),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::config::{Config, HtmlConfig, Playpen};
|
||||
use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::theme::{self, playpen_editor, Theme};
|
||||
use crate::theme::{self, playground_editor, Theme};
|
||||
use crate::utils;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::fs::{self, File};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::utils::fs::get_404_output_file;
|
||||
use handlebars::Handlebars;
|
||||
use regex::{Captures, Regex};
|
||||
|
||||
@@ -30,86 +31,172 @@ impl HtmlHandlebars {
|
||||
print_content: &mut String,
|
||||
) -> Result<()> {
|
||||
// FIXME: This should be made DRY-er and rely less on mutable state
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
let content = ch.content.clone();
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
|
||||
let fixed_content = utils::render_markdown_with_path(
|
||||
&ch.content,
|
||||
ctx.html_config.curly_quotes,
|
||||
Some(&ch.path),
|
||||
);
|
||||
print_content.push_str(&fixed_content);
|
||||
let (ch, path) = match item {
|
||||
BookItem::Chapter(ch) if !ch.is_draft_chapter() => (ch, ch.path.as_ref().unwrap()),
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
// 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");
|
||||
if let Some(ref edit_url_template) = ctx.html_config.edit_url_template {
|
||||
let full_path = ctx.book_config.src.to_str().unwrap_or_default().to_owned()
|
||||
+ "/"
|
||||
+ ch.source_path
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
// "print.html" is used for the print page.
|
||||
if ch.path == Path::new("print.md") {
|
||||
bail!(ErrorKind::ReservedFilenameError(ch.path.clone()));
|
||||
};
|
||||
let edit_url = edit_url_template.replace("{path}", &full_path);
|
||||
ctx.data
|
||||
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
|
||||
}
|
||||
|
||||
// Non-lexical lifetimes needed :'(
|
||||
let title: String;
|
||||
{
|
||||
let book_title = ctx
|
||||
.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
let content = ch.content.clone();
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
|
||||
title = match book_title {
|
||||
"" => ch.name.clone(),
|
||||
_ => ch.name.clone() + " - " + book_title,
|
||||
}
|
||||
}
|
||||
let fixed_content =
|
||||
utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path));
|
||||
if !ctx.is_index && ctx.html_config.print.page_break {
|
||||
// Add page break between chapters
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
|
||||
// Add both two CSS properties because of the compatibility issue
|
||||
print_content
|
||||
.push_str(r#"<div style="break-before: page; page-break-before: always;"></div>"#);
|
||||
}
|
||||
print_content.push_str(&fixed_content);
|
||||
|
||||
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)),
|
||||
);
|
||||
if let Some(ref section) = ch.number {
|
||||
ctx.data
|
||||
.insert("section".to_owned(), json!(section.to_string()));
|
||||
}
|
||||
// Update the context with data for this file
|
||||
let ctx_path = path
|
||||
.to_str()
|
||||
.with_context(|| "Could not convert path to str")?;
|
||||
let filepath = Path::new(&ctx_path).with_extension("html");
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
// "print.html" is used for the print page.
|
||||
if path == Path::new("print.md") {
|
||||
bail!("{} is reserved for internal use", path.display());
|
||||
};
|
||||
|
||||
let rendered = self.post_process(rendered, &ctx.html_config.playpen);
|
||||
let book_title = ctx
|
||||
.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
|
||||
// Write to file
|
||||
debug!("Creating {}", filepath.display());
|
||||
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
|
||||
let title = if let Some(title) = ctx.chapter_titles.get(path) {
|
||||
title.clone()
|
||||
} else if book_title.is_empty() {
|
||||
ch.name.clone()
|
||||
} else {
|
||||
ch.name.clone() + " - " + book_title
|
||||
};
|
||||
|
||||
if ctx.is_index {
|
||||
ctx.data.insert("path".to_owned(), json!("index.md"));
|
||||
ctx.data.insert("path_to_root".to_owned(), json!(""));
|
||||
ctx.data.insert("is_index".to_owned(), json!("true"));
|
||||
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())?;
|
||||
}
|
||||
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(&path)),
|
||||
);
|
||||
if let Some(ref section) = ch.number {
|
||||
ctx.data
|
||||
.insert("section".to_owned(), json!(section.to_string()));
|
||||
}
|
||||
|
||||
// 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.playground, ctx.edition);
|
||||
|
||||
// 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.md"));
|
||||
ctx.data.insert("path_to_root".to_owned(), json!(""));
|
||||
ctx.data.insert("is_index".to_owned(), json!(true));
|
||||
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
|
||||
let rendered_index =
|
||||
self.post_process(rendered_index, &ctx.html_config.playground, ctx.edition);
|
||||
debug!("Creating index.html from {}", ctx_path);
|
||||
utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_404(
|
||||
&self,
|
||||
ctx: &RenderContext,
|
||||
html_config: &HtmlConfig,
|
||||
src_dir: &Path,
|
||||
handlebars: &mut Handlebars<'_>,
|
||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||
) -> Result<()> {
|
||||
let destination = &ctx.destination;
|
||||
let content_404 = if let Some(ref filename) = html_config.input_404 {
|
||||
let path = src_dir.join(filename);
|
||||
std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("unable to open 404 input file {:?}", path))?
|
||||
} else {
|
||||
// 404 input not explicitly configured try the default file 404.md
|
||||
let default_404_location = src_dir.join("404.md");
|
||||
if default_404_location.exists() {
|
||||
std::fs::read_to_string(&default_404_location).with_context(|| {
|
||||
format!("unable to open 404 input file {:?}", default_404_location)
|
||||
})?
|
||||
} else {
|
||||
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
|
||||
navigation bar or search to continue."
|
||||
.to_string()
|
||||
}
|
||||
};
|
||||
let html_content_404 = utils::render_markdown(&content_404, html_config.curly_quotes);
|
||||
|
||||
let mut data_404 = data.clone();
|
||||
let base_url = if let Some(site_url) = &html_config.site_url {
|
||||
site_url
|
||||
} else {
|
||||
debug!(
|
||||
"HTML 'site-url' parameter not set, defaulting to '/'. Please configure \
|
||||
this to ensure the 404 page work correctly, especially if your site is hosted in a \
|
||||
subdirectory on the HTTP server."
|
||||
);
|
||||
"/"
|
||||
};
|
||||
data_404.insert("base_url".to_owned(), json!(base_url));
|
||||
// Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
|
||||
data_404.insert("path".to_owned(), json!("404.md"));
|
||||
data_404.insert("content".to_owned(), json!(html_content_404));
|
||||
|
||||
let mut title = String::from("Page not found");
|
||||
if let Some(book_title) = &ctx.config.book.title {
|
||||
title.push_str(" - ");
|
||||
title.push_str(book_title);
|
||||
}
|
||||
data_404.insert("title".to_owned(), json!(title));
|
||||
let rendered = handlebars.render("index", &data_404)?;
|
||||
|
||||
let rendered =
|
||||
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
|
||||
let output_file = get_404_output_file(&html_config.input_404);
|
||||
utils::fs::write_file(destination, output_file, rendered.as_bytes())?;
|
||||
debug!("Creating 404.html ✓");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
|
||||
fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String {
|
||||
fn post_process(
|
||||
&self,
|
||||
rendered: String,
|
||||
playground_config: &Playground,
|
||||
edition: Option<RustEdition>,
|
||||
) -> String {
|
||||
let rendered = build_header_links(&rendered);
|
||||
let rendered = fix_code_blocks(&rendered);
|
||||
let rendered = add_playpen_pre(&rendered, playpen_config);
|
||||
let rendered = add_playground_pre(&rendered, playground_config, edition);
|
||||
|
||||
rendered
|
||||
}
|
||||
@@ -125,15 +212,26 @@ impl HtmlHandlebars {
|
||||
write_file(
|
||||
destination,
|
||||
".nojekyll",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
write_file(destination, "CNAME", format!("{}\n", cname).as_bytes())?;
|
||||
}
|
||||
|
||||
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)?;
|
||||
if html_config.print.enable {
|
||||
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)?;
|
||||
if let Some(contents) = &theme.favicon_png {
|
||||
write_file(destination, "favicon.png", contents)?;
|
||||
}
|
||||
if let Some(contents) = &theme.favicon_svg {
|
||||
write_file(destination, "favicon.svg", contents)?;
|
||||
}
|
||||
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)?;
|
||||
@@ -174,20 +272,38 @@ impl HtmlHandlebars {
|
||||
"FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
if html_config.copy_fonts {
|
||||
write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
|
||||
for (file_name, contents) in theme::fonts::LICENSES.iter() {
|
||||
write_file(destination, file_name, contents)?;
|
||||
}
|
||||
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
|
||||
write_file(destination, file_name, contents)?;
|
||||
}
|
||||
write_file(
|
||||
destination,
|
||||
theme::fonts::SOURCE_CODE_PRO.0,
|
||||
theme::fonts::SOURCE_CODE_PRO.1,
|
||||
)?;
|
||||
}
|
||||
|
||||
let playpen_config = &html_config.playpen;
|
||||
let playground_config = &html_config.playground;
|
||||
|
||||
// Ace is a very large dependency, so only load it when requested
|
||||
if playpen_config.editable && playpen_config.copy_js {
|
||||
if playground_config.editable && playground_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, "editor.js", playground_editor::JS)?;
|
||||
write_file(destination, "ace.js", playground_editor::ACE_JS)?;
|
||||
write_file(destination, "mode-rust.js", playground_editor::MODE_RUST_JS)?;
|
||||
write_file(
|
||||
destination,
|
||||
"theme-dawn.js",
|
||||
playground_editor::THEME_DAWN_JS,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"theme-tomorrow_night.js",
|
||||
playpen_editor::THEME_TOMORROW_NIGHT_JS,
|
||||
playground_editor::THEME_TOMORROW_NIGHT_JS,
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -241,7 +357,7 @@ impl HtmlHandlebars {
|
||||
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()))?;
|
||||
.with_context(|| format!("Unable to create {}", parent.display()))?;
|
||||
}
|
||||
debug!(
|
||||
"Copying {} -> {}",
|
||||
@@ -249,7 +365,7 @@ impl HtmlHandlebars {
|
||||
output_location.display()
|
||||
);
|
||||
|
||||
fs::copy(&input_location, &output_location).chain_err(|| {
|
||||
fs::copy(&input_location, &output_location).with_context(|| {
|
||||
format!(
|
||||
"Unable to copy {} to {}",
|
||||
input_location.display(),
|
||||
@@ -260,6 +376,68 @@ impl HtmlHandlebars {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_redirects(
|
||||
&self,
|
||||
root: &Path,
|
||||
handlebars: &Handlebars<'_>,
|
||||
redirects: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
if redirects.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!("Emitting redirects");
|
||||
|
||||
for (original, new) in redirects {
|
||||
log::debug!("Redirecting \"{}\" → \"{}\"", original, new);
|
||||
// Note: all paths are relative to the build directory, so the
|
||||
// leading slash in an absolute path means nothing (and would mess
|
||||
// up `root.join(original)`).
|
||||
let original = original.trim_start_matches('/');
|
||||
let filename = root.join(original);
|
||||
self.emit_redirect(handlebars, &filename, new)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_redirect(
|
||||
&self,
|
||||
handlebars: &Handlebars<'_>,
|
||||
original: &Path,
|
||||
destination: &str,
|
||||
) -> Result<()> {
|
||||
if original.exists() {
|
||||
// sanity check to avoid accidentally overwriting a real file.
|
||||
let msg = format!(
|
||||
"Not redirecting \"{}\" to \"{}\" because it already exists. Are you sure it needs to be redirected?",
|
||||
original.display(),
|
||||
destination,
|
||||
);
|
||||
return Err(Error::msg(msg));
|
||||
}
|
||||
|
||||
if let Some(parent) = original.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?;
|
||||
}
|
||||
|
||||
let ctx = json!({
|
||||
"url": destination,
|
||||
});
|
||||
let f = File::create(original)?;
|
||||
handlebars
|
||||
.render_to_write("redirect", &ctx, f)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Unable to create a redirect file at \"{}\"",
|
||||
original.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mattico): Remove some time after the 0.1.8 release
|
||||
@@ -287,6 +465,7 @@ impl Renderer for HtmlHandlebars {
|
||||
}
|
||||
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||
let book_config = &ctx.config.book;
|
||||
let html_config = ctx.config.html_config().unwrap_or_default();
|
||||
let src_dir = ctx.root.join(&ctx.config.book.src);
|
||||
let destination = &ctx.destination;
|
||||
@@ -295,14 +474,20 @@ impl Renderer for HtmlHandlebars {
|
||||
|
||||
if destination.exists() {
|
||||
utils::fs::remove_dir_content(destination)
|
||||
.chain_err(|| "Unable to remove stale HTML output")?;
|
||||
.with_context(|| "Unable to remove stale HTML output")?;
|
||||
}
|
||||
|
||||
trace!("render");
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
let theme_dir = match html_config.theme {
|
||||
Some(ref theme) => theme.to_path_buf(),
|
||||
Some(ref theme) => {
|
||||
let dir = ctx.root.join(theme);
|
||||
if !dir.is_dir() {
|
||||
bail!("theme dir {} does not exist", dir.display());
|
||||
}
|
||||
dir
|
||||
}
|
||||
None => ctx.root.join("theme"),
|
||||
};
|
||||
|
||||
@@ -321,19 +506,26 @@ impl Renderer for HtmlHandlebars {
|
||||
debug!("Register the index handlebars template");
|
||||
handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
|
||||
|
||||
debug!("Register the head handlebars template");
|
||||
handlebars.register_partial("head", String::from_utf8(theme.head.clone())?)?;
|
||||
|
||||
debug!("Register the redirect handlebars template");
|
||||
handlebars
|
||||
.register_template_string("redirect", String::from_utf8(theme.redirect.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)?;
|
||||
let mut data = make_data(&ctx.root, book, &ctx.config, &html_config, &theme)?;
|
||||
|
||||
// Print version
|
||||
let mut print_content = String::new();
|
||||
|
||||
fs::create_dir_all(&destination)
|
||||
.chain_err(|| "Unexpected error when constructing destination path")?;
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
let mut is_index = true;
|
||||
for item in book.iter() {
|
||||
@@ -342,10 +534,19 @@ impl Renderer for HtmlHandlebars {
|
||||
destination: destination.to_path_buf(),
|
||||
data: data.clone(),
|
||||
is_index,
|
||||
book_config: book_config.clone(),
|
||||
html_config: html_config.clone(),
|
||||
edition: ctx.config.rust.edition,
|
||||
chapter_titles: &ctx.chapter_titles,
|
||||
};
|
||||
self.render_item(item, ctx, &mut print_content)?;
|
||||
is_index = false;
|
||||
// Only the first non-draft chapter item should be treated as the "index"
|
||||
is_index &= !matches!(item, BookItem::Chapter(ch) if !ch.is_draft_chapter());
|
||||
}
|
||||
|
||||
// Render 404 page
|
||||
if html_config.input_404 != Some("".to_string()) {
|
||||
self.render_404(ctx, &html_config, &src_dir, &mut handlebars, &mut data)?;
|
||||
}
|
||||
|
||||
// Print version
|
||||
@@ -355,31 +556,37 @@ impl Renderer for HtmlHandlebars {
|
||||
}
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
if html_config.print.enable {
|
||||
debug!("Render template");
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
|
||||
let rendered = self.post_process(rendered, &html_config.playpen);
|
||||
let rendered =
|
||||
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
|
||||
|
||||
utils::fs::write_file(&destination, "print.html", rendered.as_bytes())?;
|
||||
debug!("Creating print.html ✓");
|
||||
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")?;
|
||||
self.copy_static_files(destination, &theme, &html_config)
|
||||
.with_context(|| "Unable to copy across static files")?;
|
||||
self.copy_additional_css_and_js(&html_config, &ctx.root, destination)
|
||||
.with_context(|| "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)?;
|
||||
super::search::create_files(&search, destination, book)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
|
||||
.context("Unable to emit redirects")?;
|
||||
|
||||
// Copy all remaining files, avoid a recursive copy from/to the book build dir
|
||||
utils::fs::copy_files_except_ext(&src_dir, &destination, true, Some(&build_dir), &["md"])?;
|
||||
utils::fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -390,6 +597,7 @@ fn make_data(
|
||||
book: &Book,
|
||||
config: &Config,
|
||||
html_config: &HtmlConfig,
|
||||
theme: &Theme,
|
||||
) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
trace!("make_data");
|
||||
|
||||
@@ -406,9 +614,17 @@ fn make_data(
|
||||
"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));
|
||||
if theme.favicon_png.is_some() {
|
||||
data.insert("favicon_png".to_owned(), json!("favicon.png"));
|
||||
}
|
||||
if theme.favicon_svg.is_some() {
|
||||
data.insert("favicon_svg".to_owned(), json!("favicon.svg"));
|
||||
}
|
||||
if let Some(ref live_reload_endpoint) = html_config.live_reload_endpoint {
|
||||
data.insert(
|
||||
"live_reload_endpoint".to_owned(),
|
||||
json!(live_reload_endpoint),
|
||||
);
|
||||
}
|
||||
|
||||
let default_theme = match html_config.default_theme {
|
||||
@@ -419,7 +635,7 @@ fn make_data(
|
||||
|
||||
let preferred_dark_theme = match html_config.preferred_dark_theme {
|
||||
Some(ref theme) => theme.to_lowercase(),
|
||||
None => default_theme,
|
||||
None => "navy".to_string(),
|
||||
};
|
||||
data.insert(
|
||||
"preferred_dark_theme".to_owned(),
|
||||
@@ -435,6 +651,10 @@ fn make_data(
|
||||
data.insert("mathjax_support".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
if html_config.copy_fonts {
|
||||
data.insert("copy_fonts".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional style
|
||||
if !html_config.additional_css.is_empty() {
|
||||
let mut css = Vec::new();
|
||||
@@ -459,18 +679,19 @@ fn make_data(
|
||||
data.insert("additional_js".to_owned(), json!(js));
|
||||
}
|
||||
|
||||
if html_config.playpen.editable && html_config.playpen.copy_js {
|
||||
data.insert("playpen_js".to_owned(), json!(true));
|
||||
if html_config.playpen.line_numbers {
|
||||
data.insert("playpen_line_numbers".to_owned(), json!(true));
|
||||
if html_config.playground.editable && html_config.playground.copy_js {
|
||||
data.insert("playground_js".to_owned(), json!(true));
|
||||
if html_config.playground.line_numbers {
|
||||
data.insert("playground_line_numbers".to_owned(), json!(true));
|
||||
}
|
||||
}
|
||||
if html_config.playpen.copyable {
|
||||
data.insert("playpen_copyable".to_owned(), json!(true));
|
||||
if html_config.playground.copyable {
|
||||
data.insert("playground_copyable".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
data.insert("fold_enable".to_owned(), json!((html_config.fold.enable)));
|
||||
data.insert("fold_level".to_owned(), json!((html_config.fold.level)));
|
||||
data.insert("print_enable".to_owned(), json!(html_config.print.enable));
|
||||
data.insert("fold_enable".to_owned(), json!(html_config.fold.enable));
|
||||
data.insert("fold_level".to_owned(), json!(html_config.fold.level));
|
||||
|
||||
let search = html_config.search.clone();
|
||||
if cfg!(feature = "search") {
|
||||
@@ -505,6 +726,9 @@ fn make_data(
|
||||
let mut chapter = BTreeMap::new();
|
||||
|
||||
match *item {
|
||||
BookItem::PartTitle(ref title) => {
|
||||
chapter.insert("part".to_owned(), json!(title));
|
||||
}
|
||||
BookItem::Chapter(ref ch) => {
|
||||
if let Some(ref section) = ch.number {
|
||||
chapter.insert("section".to_owned(), json!(section.to_string()));
|
||||
@@ -516,11 +740,12 @@ fn make_data(
|
||||
);
|
||||
|
||||
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));
|
||||
if let Some(ref path) = ch.path {
|
||||
let p = path
|
||||
.to_str()
|
||||
.with_context(|| "Could not convert path to str")?;
|
||||
chapter.insert("path".to_owned(), json!(p));
|
||||
}
|
||||
}
|
||||
BookItem::Separator => {
|
||||
chapter.insert("spacer".to_owned(), json!("_spacer_"));
|
||||
@@ -539,10 +764,13 @@ fn make_data(
|
||||
/// Goes through the rendered HTML, making sure all header tags have
|
||||
/// an anchor respectively so people can link to sections directly.
|
||||
fn build_header_links(html: &str) -> String {
|
||||
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
||||
lazy_static! {
|
||||
static ref BUILD_HEADER_LINKS: Regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
||||
}
|
||||
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
regex
|
||||
BUILD_HEADER_LINKS
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let level = caps[1]
|
||||
.parse()
|
||||
@@ -560,19 +788,10 @@ fn insert_link_into_header(
|
||||
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;
|
||||
let id = utils::unique_id_from_content(content, id_counter);
|
||||
|
||||
format!(
|
||||
r##"<h{level}><a class="header" href="#{id}" id="{id}">{text}</a></h{level}>"##,
|
||||
r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content
|
||||
@@ -588,11 +807,15 @@ fn insert_link_into_header(
|
||||
// ```
|
||||
// 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
|
||||
lazy_static! {
|
||||
static ref FIX_CODE_BLOCKS: Regex =
|
||||
Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
||||
}
|
||||
|
||||
FIX_CODE_BLOCKS
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let before = &caps[1];
|
||||
let classes = &caps[2].replace(",", " ");
|
||||
let classes = &caps[2].replace(',', " ");
|
||||
let after = &caps[3];
|
||||
|
||||
format!(
|
||||
@@ -605,24 +828,50 @@ fn fix_code_blocks(html: &str) -> String {
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
regex
|
||||
fn add_playground_pre(
|
||||
html: &str,
|
||||
playground_config: &Playground,
|
||||
edition: Option<RustEdition>,
|
||||
) -> String {
|
||||
lazy_static! {
|
||||
static ref ADD_PLAYGROUND_PRE: Regex =
|
||||
Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
}
|
||||
ADD_PLAYGROUND_PRE
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
|
||||
if classes.contains("language-rust") {
|
||||
if (!classes.contains("ignore") && !classes.contains("noplaypen"))
|
||||
if (!classes.contains("ignore")
|
||||
&& !classes.contains("noplayground")
|
||||
&& !classes.contains("noplaypen")
|
||||
&& playground_config.runnable)
|
||||
|| classes.contains("mdbook-runnable")
|
||||
{
|
||||
let contains_e2015 = classes.contains("edition2015");
|
||||
let contains_e2018 = classes.contains("edition2018");
|
||||
let contains_e2021 = classes.contains("edition2021");
|
||||
let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 {
|
||||
// the user forced edition, we should not overwrite it
|
||||
""
|
||||
} else {
|
||||
match edition {
|
||||
Some(RustEdition::E2015) => " edition2015",
|
||||
Some(RustEdition::E2018) => " edition2018",
|
||||
Some(RustEdition::E2021) => " edition2021",
|
||||
None => "",
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the contents in an external pre block
|
||||
format!(
|
||||
"<pre class=\"playpen\"><code class=\"{}\">{}</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
|
||||
classes,
|
||||
edition_class,
|
||||
{
|
||||
let content: Cow<'_, str> = if playpen_config.editable
|
||||
let content: Cow<'_, str> = if playground_config.editable
|
||||
&& classes.contains("editable")
|
||||
|| text.contains("fn main")
|
||||
|| text.contains("quick_main!")
|
||||
@@ -632,11 +881,8 @@ fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
|
||||
format!(
|
||||
"\n# #![allow(unused_variables)]\n{}#fn main() {{\n{}#}}",
|
||||
attrs, code
|
||||
)
|
||||
.into()
|
||||
format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
|
||||
.into()
|
||||
};
|
||||
hide_lines(&content)
|
||||
}
|
||||
@@ -652,11 +898,11 @@ fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap();
|
||||
}
|
||||
|
||||
fn hide_lines(content: &str) -> String {
|
||||
lazy_static! {
|
||||
static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap();
|
||||
}
|
||||
|
||||
let mut result = String::with_capacity(content.len());
|
||||
for line in content.lines() {
|
||||
if let Some(caps) = BORING_LINES_REGEX.captures(line) {
|
||||
@@ -695,10 +941,10 @@ fn partition_source(s: &str) -> (String, String) {
|
||||
if !header || after_header {
|
||||
after_header = true;
|
||||
after.push_str(line);
|
||||
after.push_str("\n");
|
||||
after.push('\n');
|
||||
} else {
|
||||
before.push_str(line);
|
||||
before.push_str("\n");
|
||||
before.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,7 +956,10 @@ struct RenderItemContext<'a> {
|
||||
destination: PathBuf,
|
||||
data: serde_json::Map<String, serde_json::Value>,
|
||||
is_index: bool,
|
||||
book_config: BookConfig,
|
||||
html_config: HtmlConfig,
|
||||
edition: Option<RustEdition>,
|
||||
chapter_titles: &'a HashMap<PathBuf, String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -722,61 +971,134 @@ mod tests {
|
||||
let inputs = vec![
|
||||
(
|
||||
"blah blah <h1>Foo</h1>",
|
||||
r##"blah blah <h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
|
||||
r##"blah blah <h1 id="foo"><a class="header" href="#foo">Foo</a></h1>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1>",
|
||||
r##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
|
||||
r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1>"##,
|
||||
),
|
||||
(
|
||||
"<h3>Foo^bar</h3>",
|
||||
r##"<h3><a class="header" href="#foobar" id="foobar">Foo^bar</a></h3>"##,
|
||||
r##"<h3 id="foobar"><a class="header" href="#foobar">Foo^bar</a></h3>"##,
|
||||
),
|
||||
(
|
||||
"<h4></h4>",
|
||||
r##"<h4><a class="header" href="#" id=""></a></h4>"##,
|
||||
r##"<h4 id=""><a class="header" href="#"></a></h4>"##,
|
||||
),
|
||||
(
|
||||
"<h4><em>Hï</em></h4>",
|
||||
r##"<h4><a class="header" href="#hï" id="hï"><em>Hï</em></a></h4>"##,
|
||||
r##"<h4 id="hï"><a class="header" href="#hï"><em>Hï</em></a></h4>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1><h3>Foo</h3>",
|
||||
r##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1><h3><a class="header" href="#foo-1" id="foo-1">Foo</a></h3>"##,
|
||||
r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1><h3 id="foo-1"><a class="header" href="#foo-1">Foo</a></h3>"##,
|
||||
),
|
||||
];
|
||||
|
||||
for (src, should_be) in inputs {
|
||||
let got = build_header_links(&src);
|
||||
let got = build_header_links(src);
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_playpen() {
|
||||
fn add_playground() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playpen\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused_variables)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playpen\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
|
||||
"<pre class=\"playpen\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
|
||||
"<pre class=\"playpen\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
|
||||
"<pre class=\"playpen\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";\n</code></pre>"),
|
||||
("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
|
||||
"<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code>"),
|
||||
("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
|
||||
"<pre class=\"playpen\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]\n</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playpen_pre(
|
||||
let got = add_playground_pre(
|
||||
src,
|
||||
&Playpen {
|
||||
&Playground {
|
||||
editable: true,
|
||||
..Playpen::default()
|
||||
..Playground::default()
|
||||
},
|
||||
None,
|
||||
);
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn add_playground_edition2015() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
src,
|
||||
&Playground {
|
||||
editable: true,
|
||||
..Playground::default()
|
||||
},
|
||||
Some(RustEdition::E2015),
|
||||
);
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn add_playground_edition2018() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
src,
|
||||
&Playground {
|
||||
editable: true,
|
||||
..Playground::default()
|
||||
},
|
||||
Some(RustEdition::E2018),
|
||||
);
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn add_playground_edition2021() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
src,
|
||||
&Playground {
|
||||
editable: true,
|
||||
..Playground::default()
|
||||
},
|
||||
Some(RustEdition::E2021),
|
||||
);
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ fn find_chapter(
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
.replace('\"', "");
|
||||
|
||||
if !rc.evaluate(ctx, "@root/is_index")?.is_missing() {
|
||||
// Special case for index.md which may be a synthetic page.
|
||||
@@ -75,8 +75,7 @@ fn find_chapter(
|
||||
// Skip things like "spacer"
|
||||
chapter.contains_key("path")
|
||||
})
|
||||
.skip(1)
|
||||
.next()
|
||||
.nth(1)
|
||||
{
|
||||
Some(chapter) => return Ok(Some(chapter.clone())),
|
||||
None => return Ok(None),
|
||||
@@ -92,7 +91,7 @@ fn find_chapter(
|
||||
match item.get("path") {
|
||||
Some(path) if !path.is_empty() => {
|
||||
if let Some(previous) = previous {
|
||||
if let Some(item) = target.find(&base_path, &path, &item, &previous)? {
|
||||
if let Some(item) = target.find(&base_path, path, &item, &previous)? {
|
||||
return Ok(Some(item));
|
||||
}
|
||||
}
|
||||
@@ -122,7 +121,7 @@ fn render(
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
.replace('\"', "");
|
||||
|
||||
context.insert(
|
||||
"path_to_root".to_owned(),
|
||||
@@ -142,7 +141,7 @@ fn render(
|
||||
.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("\\", "/"))))
|
||||
.map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/"))))
|
||||
})?;
|
||||
|
||||
trace!("Render template");
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::{cmp::Ordering, collections::BTreeMap};
|
||||
|
||||
use crate::utils;
|
||||
use crate::utils::bracket_escape;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
|
||||
use pulldown_cmark::{html, Event, Parser};
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -32,8 +32,8 @@ impl HelperDef for RenderToc {
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or(RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace('\"', "");
|
||||
|
||||
let current_section = rc
|
||||
.evaluate(ctx, "@root/section")?
|
||||
@@ -46,21 +46,22 @@ impl HelperDef for RenderToc {
|
||||
.evaluate(ctx, "@root/fold_enable")?
|
||||
.as_json()
|
||||
.as_bool()
|
||||
.ok_or(RenderError::new(
|
||||
"Type error for `fold_enable`, bool expected",
|
||||
))?;
|
||||
.ok_or_else(|| RenderError::new("Type error for `fold_enable`, bool expected"))?;
|
||||
|
||||
let fold_level = rc
|
||||
.evaluate(ctx, "@root/fold_level")?
|
||||
.as_json()
|
||||
.as_u64()
|
||||
.ok_or(RenderError::new(
|
||||
"Type error for `fold_level`, u64 expected",
|
||||
))?;
|
||||
.ok_or_else(|| RenderError::new("Type error for `fold_level`, u64 expected"))?;
|
||||
|
||||
out.write("<ol class=\"chapter\">")?;
|
||||
|
||||
let mut current_level = 1;
|
||||
// The "index" page, which has this attribute set, is supposed to alias the first chapter in
|
||||
// the book, i.e. the first link. There seems to be no easy way to determine which chapter
|
||||
// the "index" is aliasing from within the renderer, so this is used instead to force the
|
||||
// first link to be active. See further below.
|
||||
let mut is_first_chapter = ctx.data().get("is_index").is_some();
|
||||
|
||||
for item in chapters {
|
||||
// Spacer
|
||||
@@ -75,64 +76,74 @@ impl HelperDef for RenderToc {
|
||||
("", 1)
|
||||
};
|
||||
|
||||
let is_expanded = {
|
||||
if !fold_enable {
|
||||
// Disable fold. Expand all chapters.
|
||||
true
|
||||
} else if !section.is_empty() && current_section.starts_with(section) {
|
||||
// The section is ancestor or the current section itself.
|
||||
let is_expanded =
|
||||
if !fold_enable || (!section.is_empty() && current_section.starts_with(section)) {
|
||||
// Expand if folding is disabled, or if the section is an
|
||||
// ancestor or the current section itself.
|
||||
true
|
||||
} else {
|
||||
// Levels that are larger than this would be folded.
|
||||
level - 1 < fold_level as usize
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if level > current_level {
|
||||
while level > current_level {
|
||||
out.write("<li>")?;
|
||||
out.write("<ol class=\"section\">")?;
|
||||
current_level += 1;
|
||||
match level.cmp(¤t_level) {
|
||||
Ordering::Greater => {
|
||||
while level > current_level {
|
||||
out.write("<li>")?;
|
||||
out.write("<ol class=\"section\">")?;
|
||||
current_level += 1;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
} else if level < current_level {
|
||||
while level < current_level {
|
||||
out.write("</ol>")?;
|
||||
out.write("</li>")?;
|
||||
current_level -= 1;
|
||||
Ordering::Less => {
|
||||
while level < current_level {
|
||||
out.write("</ol>")?;
|
||||
out.write("</li>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
} else {
|
||||
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
|
||||
Ordering::Equal => {
|
||||
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
|
||||
}
|
||||
}
|
||||
|
||||
// Part title
|
||||
if let Some(title) = item.get("part") {
|
||||
out.write("<li class=\"part-title\">")?;
|
||||
out.write(&bracket_escape(title))?;
|
||||
out.write("</li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Link
|
||||
let path_exists = if let Some(path) = item.get("path") {
|
||||
if !path.is_empty() {
|
||||
out.write("<a href=\"")?;
|
||||
let path_exists = if let Some(path) =
|
||||
item.get("path")
|
||||
.and_then(|p| if p.is_empty() { None } else { Some(p) })
|
||||
{
|
||||
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("\\", "/");
|
||||
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_path))?;
|
||||
out.write(&tmp)?;
|
||||
out.write("\"")?;
|
||||
// Add link
|
||||
out.write(&utils::fs::path_to_root(¤t_path))?;
|
||||
out.write(&tmp)?;
|
||||
out.write("\"")?;
|
||||
|
||||
if path == ¤t_path {
|
||||
out.write(" class=\"active\"")?;
|
||||
}
|
||||
|
||||
out.write(">")?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
if path == ¤t_path || is_first_chapter {
|
||||
is_first_chapter = false;
|
||||
out.write(" class=\"active\"")?;
|
||||
}
|
||||
|
||||
out.write(">")?;
|
||||
true
|
||||
} else {
|
||||
out.write("<div>")?;
|
||||
false
|
||||
};
|
||||
|
||||
@@ -140,30 +151,19 @@ impl HelperDef for RenderToc {
|
||||
// Section does not necessarily exist
|
||||
if let Some(section) = item.get("section") {
|
||||
out.write("<strong aria-hidden=\"true\">")?;
|
||||
out.write(§ion)?;
|
||||
out.write(section)?;
|
||||
out.write("</strong> ")?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = item.get("name") {
|
||||
// Render only inline code blocks
|
||||
|
||||
// filter all events that are not inline code blocks
|
||||
let parser = Parser::new(name).filter(|event| match *event {
|
||||
Event::Code(_) | Event::Html(_) | 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)?;
|
||||
out.write(&bracket_escape(name))?
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
out.write("</a>")?;
|
||||
} else {
|
||||
out.write("</div>")?;
|
||||
}
|
||||
|
||||
// Render expand/collapse toggle
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use elasticlunr::Index;
|
||||
use elasticlunr::{Index, IndexBuilder};
|
||||
use pulldown_cmark::*;
|
||||
|
||||
use crate::book::{Book, BookItem};
|
||||
@@ -11,16 +11,34 @@ use crate::errors::*;
|
||||
use crate::theme::searcher;
|
||||
use crate::utils;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
const MAX_WORD_LENGTH_TO_INDEX: usize = 80;
|
||||
|
||||
/// Tokenizes in the same way as elasticlunr-rs (for English), but also drops long tokens.
|
||||
fn tokenize(text: &str) -> Vec<String> {
|
||||
text.split(|c: char| c.is_whitespace() || c == '-')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.trim().to_lowercase())
|
||||
.filter(|s| s.len() <= MAX_WORD_LENGTH_TO_INDEX)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 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 index = IndexBuilder::new()
|
||||
.add_field_with_tokenizer("title", Box::new(&tokenize))
|
||||
.add_field_with_tokenizer("body", Box::new(&tokenize))
|
||||
.add_field_with_tokenizer("breadcrumbs", Box::new(&tokenize))
|
||||
.build();
|
||||
|
||||
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)?;
|
||||
render_item(&mut index, search_config, &mut doc_urls, item)?;
|
||||
}
|
||||
|
||||
let index = write_to_json(index, &search_config, doc_urls)?;
|
||||
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());
|
||||
@@ -71,17 +89,21 @@ fn render_item(
|
||||
item: &BookItem,
|
||||
) -> Result<()> {
|
||||
let chapter = match *item {
|
||||
BookItem::Chapter(ref ch) => ch,
|
||||
BookItem::Chapter(ref ch) if !ch.is_draft_chapter() => ch,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
let filepath = Path::new(&chapter.path).with_extension("html");
|
||||
let chapter_path = chapter
|
||||
.path
|
||||
.as_ref()
|
||||
.expect("Checked that path exists above");
|
||||
let filepath = Path::new(&chapter_path).with_extension("html");
|
||||
let filepath = filepath
|
||||
.to_str()
|
||||
.chain_err(|| "Could not convert HTML path to str")?;
|
||||
.with_context(|| "Could not convert HTML path to str")?;
|
||||
let anchor_base = utils::fs::normalize_path(filepath);
|
||||
|
||||
let mut p = utils::new_cmark_parser(&chapter.content).peekable();
|
||||
let mut p = utils::new_cmark_parser(&chapter.content, false).peekable();
|
||||
|
||||
let mut in_heading = false;
|
||||
let max_section_depth = u32::from(search_config.heading_split_level);
|
||||
@@ -91,9 +113,12 @@ fn render_item(
|
||||
let mut breadcrumbs = chapter.parent_names.clone();
|
||||
let mut footnote_numbers = HashMap::new();
|
||||
|
||||
breadcrumbs.push(chapter.name.clone());
|
||||
|
||||
let mut id_counter = HashMap::new();
|
||||
while let Some(event) = p.next() {
|
||||
match event {
|
||||
Event::Start(Tag::Heading(i)) if i <= max_section_depth => {
|
||||
Event::Start(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
|
||||
if !heading.is_empty() {
|
||||
// Section finished, the next heading is following now
|
||||
// Write the data to the index, and clear it for the next section
|
||||
@@ -112,9 +137,9 @@ fn render_item(
|
||||
|
||||
in_heading = true;
|
||||
}
|
||||
Event::End(Tag::Heading(i)) if i <= max_section_depth => {
|
||||
Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
|
||||
in_heading = false;
|
||||
section_id = Some(utils::id_from_content(&heading));
|
||||
section_id = Some(utils::unique_id_from_content(&heading, &mut id_counter));
|
||||
breadcrumbs.push(heading.clone());
|
||||
}
|
||||
Event::Start(Tag::FootnoteDefinition(name)) => {
|
||||
@@ -128,14 +153,14 @@ fn render_item(
|
||||
// in an HtmlBlock tag. We must collect consecutive Html events
|
||||
// into a block ourselves.
|
||||
while let Some(Event::Html(html)) = p.peek() {
|
||||
html_block.push_str(&html);
|
||||
html_block.push_str(html);
|
||||
p.next();
|
||||
}
|
||||
|
||||
body.push_str(&clean_html(&html_block));
|
||||
}
|
||||
Event::Start(_) | Event::End(_) | Event::Rule | Event::SoftBreak | Event::HardBreak => {
|
||||
// Insert spaces where HTML output would usually seperate text
|
||||
// Insert spaces where HTML output would usually separate text
|
||||
// to ensure words don't get merged together
|
||||
if in_heading {
|
||||
heading.push(' ');
|
||||
@@ -159,7 +184,12 @@ fn render_item(
|
||||
}
|
||||
}
|
||||
|
||||
if !heading.is_empty() {
|
||||
if !body.is_empty() || !heading.is_empty() {
|
||||
if heading.is_empty() {
|
||||
if let Some(chapter) = breadcrumbs.first() {
|
||||
heading = chapter.clone();
|
||||
}
|
||||
}
|
||||
// Make sure the last section is added to the index
|
||||
add_doc(
|
||||
index,
|
||||
@@ -197,12 +227,13 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) ->
|
||||
|
||||
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 mut insert_boost = |key: &str, boost| {
|
||||
opt.boost = Some(boost);
|
||||
fields.insert(key.into(), opt);
|
||||
};
|
||||
insert_boost("title", search_config.boost_title);
|
||||
insert_boost("body", search_config.boost_paragraph);
|
||||
insert_boost("breadcrumbs", search_config.boost_hierarchy);
|
||||
|
||||
let search_options = SearchOptions {
|
||||
bool: if search_config.use_boolean_and {
|
||||
|
||||
@@ -28,18 +28,24 @@ impl Renderer for MarkdownRenderer {
|
||||
|
||||
if destination.exists() {
|
||||
utils::fs::remove_dir_content(destination)
|
||||
.chain_err(|| "Unable to remove stale Markdown output")?;
|
||||
.with_context(|| "Unable to remove stale Markdown output")?;
|
||||
}
|
||||
|
||||
trace!("markdown render");
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
utils::fs::write_file(&ctx.destination, &ch.path, ch.content.as_bytes())?;
|
||||
if !ch.is_draft_chapter() {
|
||||
utils::fs::write_file(
|
||||
&ctx.destination,
|
||||
&ch.path.as_ref().expect("Checked path exists before"),
|
||||
ch.content.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_dir_all(&destination)
|
||||
.chain_err(|| "Unexpected error when constructing destination path")?;
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -18,14 +18,18 @@ mod html_handlebars;
|
||||
mod markdown_renderer;
|
||||
|
||||
use shlex::Shlex;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::path::PathBuf;
|
||||
use std::io::{self, ErrorKind, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use toml::Value;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An arbitrary `mdbook` backend.
|
||||
///
|
||||
@@ -33,12 +37,9 @@ use crate::errors::*;
|
||||
/// 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
|
||||
/// - [`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;
|
||||
@@ -66,6 +67,8 @@ pub struct RenderContext {
|
||||
/// guaranteed to be empty or even exist.
|
||||
pub destination: PathBuf,
|
||||
#[serde(skip)]
|
||||
pub(crate) chapter_titles: HashMap<PathBuf, String>,
|
||||
#[serde(skip)]
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
@@ -82,6 +85,7 @@ impl RenderContext {
|
||||
version: crate::MDBOOK_VERSION.to_string(),
|
||||
root: root.into(),
|
||||
destination: destination.into(),
|
||||
chapter_titles: HashMap::new(),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
}
|
||||
@@ -93,7 +97,7 @@ impl RenderContext {
|
||||
|
||||
/// 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`")
|
||||
serde_json::from_reader(reader).with_context(|| "Unable to deserialize the `RenderContext`")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,14 +136,44 @@ impl CmdRenderer {
|
||||
CmdRenderer { name, cmd }
|
||||
}
|
||||
|
||||
fn compose_command(&self) -> Result<Command> {
|
||||
fn compose_command(&self, root: &Path, destination: &Path) -> Result<Command> {
|
||||
let mut words = Shlex::new(&self.cmd);
|
||||
let executable = match words.next() {
|
||||
Some(e) => e,
|
||||
let exe = match words.next() {
|
||||
Some(e) => PathBuf::from(e),
|
||||
None => bail!("Command string was empty"),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(executable);
|
||||
let exe = if exe.components().count() == 1 {
|
||||
// Search PATH for the executable.
|
||||
exe
|
||||
} else {
|
||||
// Relative paths are preferred to be relative to the book root.
|
||||
let abs_exe = root.join(&exe);
|
||||
if abs_exe.exists() {
|
||||
abs_exe
|
||||
} else {
|
||||
// Historically paths were relative to the destination, but
|
||||
// this is not the preferred way.
|
||||
let legacy_path = destination.join(&exe);
|
||||
if legacy_path.exists() {
|
||||
warn!(
|
||||
"Renderer command `{}` uses a path relative to the \
|
||||
renderer output directory `{}`. This was previously \
|
||||
accepted, but has been deprecated. Relative executable \
|
||||
paths should be relative to the book root.",
|
||||
exe.display(),
|
||||
destination.display()
|
||||
);
|
||||
legacy_path
|
||||
} else {
|
||||
// Let this bubble through to later be handled by
|
||||
// handle_render_command_error.
|
||||
abs_exe
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(exe);
|
||||
|
||||
for arg in words {
|
||||
cmd.arg(arg);
|
||||
@@ -149,6 +183,40 @@ impl CmdRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
impl CmdRenderer {
|
||||
fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> {
|
||||
if let ErrorKind::NotFound = error.kind() {
|
||||
// Look for "output.{self.name}.optional".
|
||||
// If it exists and is true, treat this as a warning.
|
||||
// Otherwise, fail the build.
|
||||
|
||||
let optional_key = format!("output.{}.optional", self.name);
|
||||
|
||||
let is_optional = match ctx.config.get(&optional_key) {
|
||||
Some(Value::Boolean(value)) => *value,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_optional {
|
||||
warn!(
|
||||
"The command `{}` for backend `{}` was not found, \
|
||||
but was marked as optional.",
|
||||
self.cmd, self.name
|
||||
);
|
||||
return Ok(());
|
||||
} else {
|
||||
error!(
|
||||
"The command `{0}` wasn't found, is the \"{1}\" backend installed? \
|
||||
If you want to ignore this error when the \"{1}\" backend is not installed, \
|
||||
set `optional = true` in the `[output.{1}]` section of the book.toml configuration file.",
|
||||
self.cmd, self.name
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error).with_context(|| "Unable to start the backend")?
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for CmdRenderer {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
@@ -160,7 +228,7 @@ impl Renderer for CmdRenderer {
|
||||
let _ = fs::create_dir_all(&ctx.destination);
|
||||
|
||||
let mut child = match self
|
||||
.compose_command()?
|
||||
.compose_command(&ctx.root, &ctx.destination)?
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
@@ -168,34 +236,22 @@ impl Renderer for CmdRenderer {
|
||||
.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")?;
|
||||
}
|
||||
Err(e) => return self.handle_render_command_error(ctx, e),
|
||||
};
|
||||
|
||||
{
|
||||
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 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")?;
|
||||
.with_context(|| "Error waiting for the backend to complete")?;
|
||||
|
||||
trace!("{} exited with output: {:?}", self.cmd, status);
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ Original by Dempfi (https://github.com/dempfi/ayu)
|
||||
overflow-x: auto;
|
||||
background: #191f26;
|
||||
color: #e6e1cf;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
window.onunload = function () { };
|
||||
|
||||
// Global variable, shared between modules
|
||||
function playpen_text(playpen) {
|
||||
let code_block = playpen.querySelector("code");
|
||||
function playground_text(playground) {
|
||||
let code_block = playground.querySelector("code");
|
||||
|
||||
if (window.ace && code_block.classList.contains("editable")) {
|
||||
let editor = window.ace.edit(code_block);
|
||||
@@ -23,8 +23,8 @@ function playpen_text(playpen) {
|
||||
]);
|
||||
}
|
||||
|
||||
var playpens = Array.from(document.querySelectorAll(".playpen"));
|
||||
if (playpens.length > 0) {
|
||||
var playgrounds = Array.from(document.querySelectorAll(".playground"));
|
||||
if (playgrounds.length > 0) {
|
||||
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
@@ -36,21 +36,21 @@ function playpen_text(playpen) {
|
||||
.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));
|
||||
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
|
||||
});
|
||||
}
|
||||
|
||||
function handle_crate_list_update(playpen_block, playground_crates) {
|
||||
function handle_crate_list_update(playground_block, playground_crates) {
|
||||
// update the play buttons after receiving the response
|
||||
update_play_button(playpen_block, playground_crates);
|
||||
update_play_button(playground_block, playground_crates);
|
||||
|
||||
// and install on change listener to dynamically update ACE editors
|
||||
if (window.ace) {
|
||||
let code_block = playpen_block.querySelector("code");
|
||||
let code_block = playground_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);
|
||||
update_play_button(playground_block, playground_crates);
|
||||
});
|
||||
// add Ctrl-Enter command to execute rust code
|
||||
editor.commands.addCommand({
|
||||
@@ -59,7 +59,7 @@ function playpen_text(playpen) {
|
||||
win: "Ctrl-Enter",
|
||||
mac: "Ctrl-Enter"
|
||||
},
|
||||
exec: _editor => run_rust_code(playpen_block)
|
||||
exec: _editor => run_rust_code(playground_block)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ function playpen_text(playpen) {
|
||||
}
|
||||
|
||||
// get list of `extern crate`'s from snippet
|
||||
var txt = playpen_text(pre_block);
|
||||
var txt = playground_text(pre_block);
|
||||
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
|
||||
var snippet_crates = [];
|
||||
var item;
|
||||
@@ -106,11 +106,14 @@ function playpen_text(playpen) {
|
||||
code_block.append(result_block);
|
||||
}
|
||||
|
||||
let text = playpen_text(code_block);
|
||||
let text = playground_text(code_block);
|
||||
let classes = code_block.querySelector('code').classList;
|
||||
let has_2018 = classes.contains("edition2018");
|
||||
let edition = has_2018 ? "2018" : "2015";
|
||||
|
||||
let edition = "2015";
|
||||
if(classes.contains("edition2018")) {
|
||||
edition = "2018";
|
||||
} else if(classes.contains("edition2021")) {
|
||||
edition = "2021";
|
||||
}
|
||||
var params = {
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
@@ -133,7 +136,15 @@ function playpen_text(playpen) {
|
||||
body: JSON.stringify(params)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => result_block.innerText = response.result)
|
||||
.then(response => {
|
||||
if (response.result.trim() === '') {
|
||||
result_block.innerText = "No output";
|
||||
result_block.classList.add("result-no-output");
|
||||
} else {
|
||||
result_block.innerText = response.result;
|
||||
result_block.classList.remove("result-no-output");
|
||||
}
|
||||
})
|
||||
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
|
||||
}
|
||||
|
||||
@@ -151,12 +162,13 @@ function playpen_text(playpen) {
|
||||
if (window.ace) {
|
||||
// language-rust class needs to be removed for editable
|
||||
// blocks or highlightjs will capture events
|
||||
Array
|
||||
.from(document.querySelectorAll('code.editable'))
|
||||
code_nodes
|
||||
.filter(function (node) {return node.classList.contains("editable"); })
|
||||
.forEach(function (block) { block.classList.remove('language-rust'); });
|
||||
|
||||
Array
|
||||
.from(document.querySelectorAll('code:not(.editable)'))
|
||||
code_nodes
|
||||
.filter(function (node) {return !node.classList.contains("editable"); })
|
||||
.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
} else {
|
||||
code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
@@ -175,23 +187,23 @@ function playpen_text(playpen) {
|
||||
|
||||
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>";
|
||||
buttons.innerHTML = "<button class=\"fa fa-eye\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
|
||||
|
||||
// add expand button
|
||||
var pre_block = block.parentNode;
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
|
||||
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('fa-expand')) {
|
||||
e.target.classList.remove('fa-expand');
|
||||
e.target.classList.add('fa-compress');
|
||||
if (e.target.classList.contains('fa-eye')) {
|
||||
e.target.classList.remove('fa-eye');
|
||||
e.target.classList.add('fa-eye-slash');
|
||||
e.target.title = 'Hide lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
|
||||
block.classList.remove('hide-boring');
|
||||
} else if (e.target.classList.contains('fa-compress')) {
|
||||
e.target.classList.remove('fa-compress');
|
||||
e.target.classList.add('fa-expand');
|
||||
} else if (e.target.classList.contains('fa-eye-slash')) {
|
||||
e.target.classList.remove('fa-eye-slash');
|
||||
e.target.classList.add('fa-eye');
|
||||
e.target.title = 'Show hidden lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
|
||||
@@ -200,10 +212,10 @@ function playpen_text(playpen) {
|
||||
});
|
||||
});
|
||||
|
||||
if (window.playpen_copyable) {
|
||||
if (window.playground_copyable) {
|
||||
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
|
||||
var pre_block = block.parentNode;
|
||||
if (!pre_block.classList.contains('playpen')) {
|
||||
if (!pre_block.classList.contains('playground')) {
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
@@ -222,8 +234,8 @@ function playpen_text(playpen) {
|
||||
});
|
||||
}
|
||||
|
||||
// Process playpen code blocks
|
||||
Array.from(document.querySelectorAll(".playpen")).forEach(function (pre_block) {
|
||||
// Process playground code blocks
|
||||
Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) {
|
||||
// Add play button
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
@@ -243,7 +255,7 @@ function playpen_text(playpen) {
|
||||
run_rust_code(pre_block);
|
||||
});
|
||||
|
||||
if (window.playpen_copyable) {
|
||||
if (window.playground_copyable) {
|
||||
var copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
@@ -359,7 +371,14 @@ function playpen_text(playpen) {
|
||||
});
|
||||
|
||||
themePopup.addEventListener('click', function (e) {
|
||||
var theme = e.target.id || e.target.parentElement.id;
|
||||
var theme;
|
||||
if (e.target.className === "theme") {
|
||||
theme = e.target.id;
|
||||
} else if (e.target.parentElement.className === "theme") {
|
||||
theme = e.target.parentElement.id;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
set_theme(theme);
|
||||
});
|
||||
|
||||
@@ -456,6 +475,11 @@ function playpen_text(playpen) {
|
||||
// Toggle sidebar
|
||||
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
|
||||
if (html.classList.contains("sidebar-hidden")) {
|
||||
var current_width = parseInt(
|
||||
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
|
||||
if (current_width < 150) {
|
||||
document.documentElement.style.setProperty('--sidebar-width', '150px');
|
||||
}
|
||||
showSidebar();
|
||||
} else if (html.classList.contains("sidebar-visible")) {
|
||||
hideSidebar();
|
||||
@@ -476,7 +500,16 @@ function playpen_text(playpen) {
|
||||
html.classList.add('sidebar-resizing');
|
||||
}
|
||||
function resize(e) {
|
||||
document.documentElement.style.setProperty('--sidebar-width', (e.clientX - sidebar.offsetLeft) + 'px');
|
||||
var pos = (e.clientX - sidebar.offsetLeft);
|
||||
if (pos < 20) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (html.classList.contains("sidebar-hidden")) {
|
||||
showSidebar();
|
||||
}
|
||||
pos = Math.min(pos, window.innerWidth - 100);
|
||||
document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
|
||||
}
|
||||
}
|
||||
//on mouseup remove windows functions mousemove & mouseup
|
||||
function stopResize(e) {
|
||||
@@ -558,8 +591,8 @@ function playpen_text(playpen) {
|
||||
var clipboardSnippets = new ClipboardJS('.clip-button', {
|
||||
text: function (trigger) {
|
||||
hideTooltip(trigger);
|
||||
let playpen = trigger.closest("pre");
|
||||
return playpen_text(playpen);
|
||||
let playground = trigger.closest("pre");
|
||||
return playground_text(playground);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ a > .hljs {
|
||||
.menu-title {
|
||||
display: inline-block;
|
||||
font-weight: 200;
|
||||
font-size: 2rem;
|
||||
font-size: 2.4rem;
|
||||
line-height: var(--menu-bar-height);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
@@ -208,24 +208,63 @@ pre {
|
||||
pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
right: 0px;
|
||||
top: 2px;
|
||||
margin: 0px;
|
||||
padding: 2px 0px;
|
||||
|
||||
color: var(--sidebar-fg);
|
||||
cursor: pointer;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: visibility 0.1s linear, opacity 0.1s linear;
|
||||
}
|
||||
pre:hover > .buttons {
|
||||
visibility: visible;
|
||||
opacity: 1
|
||||
}
|
||||
pre > .buttons :hover {
|
||||
color: var(--sidebar-active);
|
||||
border-color: var(--icons-hover);
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
pre > .buttons i {
|
||||
margin-left: 8px;
|
||||
}
|
||||
pre > .buttons button {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: inherit;
|
||||
margin: 0px 5px;
|
||||
padding: 3px 5px;
|
||||
font-size: 14px;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
border-color: var(--icons);
|
||||
background-color: var(--theme-popup-bg);
|
||||
transition: 100ms;
|
||||
transition-property: color,border-color,background-color;
|
||||
color: var(--icons);
|
||||
}
|
||||
@media (pointer: coarse) {
|
||||
pre > .buttons button {
|
||||
/* On mobile, make it easier to tap buttons. */
|
||||
padding: 0.3rem 1rem;
|
||||
}
|
||||
}
|
||||
pre > code {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* FIXME: ACE editors overlap their buttons because ACE does absolute
|
||||
positioning within the code block which breaks padding. The only solution I
|
||||
can think of is to move the padding to the outer pre tag (or insert a div
|
||||
wrapper), but that would require fixing a whole bunch of CSS rules.
|
||||
*/
|
||||
.hljs.ace_editor {
|
||||
padding: 0rem 0rem;
|
||||
}
|
||||
|
||||
pre > .result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ html {
|
||||
color: var(--fg);
|
||||
background-color: var(--bg);
|
||||
text-size-adjust: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -25,6 +26,16 @@ code {
|
||||
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
|
||||
}
|
||||
|
||||
/* make long words/inline code not x overflow */
|
||||
main {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* make wide tables scroll if they overflow */
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Don't change font size in headers. */
|
||||
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
||||
font-size: unset;
|
||||
@@ -34,7 +45,7 @@ h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
||||
.right { float: right; }
|
||||
.boring { opacity: 0.6; }
|
||||
.hide-boring .boring { display: none; }
|
||||
.hidden { display: none; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
h2, h3 { margin-top: 2.5em; }
|
||||
h4, h5 { margin-top: 2em; }
|
||||
@@ -45,20 +56,23 @@ h4, h5 { margin-top: 2em; }
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
h1 a.header:target::before,
|
||||
h2 a.header:target::before,
|
||||
h3 a.header:target::before,
|
||||
h4 a.header:target::before {
|
||||
h1:target::before,
|
||||
h2:target::before,
|
||||
h3:target::before,
|
||||
h4:target::before,
|
||||
h5:target::before,
|
||||
h6:target::before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-left: -30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
h1 a.header:target,
|
||||
h2 a.header:target,
|
||||
h3 a.header:target,
|
||||
h4 a.header:target {
|
||||
/* This is broken on Safari as of version 14, but is fixed
|
||||
in Safari Technology Preview 117 which I think will be Safari 14.2.
|
||||
https://bugs.webkit.org/show_bug.cgi?id=218076
|
||||
*/
|
||||
:target {
|
||||
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
|
||||
}
|
||||
|
||||
@@ -76,8 +90,7 @@ h4 a.header:target {
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
padding: 0 15px;
|
||||
padding-bottom: 50px;
|
||||
padding: 0 5px 50px 5px;
|
||||
}
|
||||
.content main {
|
||||
margin-left: auto;
|
||||
@@ -89,7 +102,7 @@ h4 a.header:target {
|
||||
.content ul { line-height: 1.45em; }
|
||||
.content a { text-decoration: none; }
|
||||
.content a:hover { text-decoration: underline; }
|
||||
.content img { max-width: 100%; }
|
||||
.content img, .content video { max-width: 100%; }
|
||||
.content .header:link,
|
||||
.content .header:visited {
|
||||
color: var(--fg);
|
||||
@@ -166,3 +179,13 @@ blockquote {
|
||||
.tooltipped .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.chapter li.part-title {
|
||||
color: var(--sidebar-fg);
|
||||
margin: 5px 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.result-no-output {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;;
|
||||
--inline-code-color: #c5c8c6;
|
||||
|
||||
--theme-popup-bg: #141617;
|
||||
--theme-popup-border: #43484d;
|
||||
@@ -92,22 +92,22 @@
|
||||
|
||||
.light {
|
||||
--bg: hsl(0, 0%, 100%);
|
||||
--fg: #333333;
|
||||
--fg: hsl(0, 0%, 0%);
|
||||
|
||||
--sidebar-bg: #fafafa;
|
||||
--sidebar-fg: #364149;
|
||||
--sidebar-fg: hsl(0, 0%, 0%);
|
||||
--sidebar-non-existant: #aaaaaa;
|
||||
--sidebar-active: #008cff;
|
||||
--sidebar-active: #1f1fff;
|
||||
--sidebar-spacer: #f4f4f4;
|
||||
|
||||
--scrollbar: #cccccc;
|
||||
--scrollbar: #8F8F8F;
|
||||
|
||||
--icons: #cccccc;
|
||||
--icons-hover: #333333;
|
||||
--icons: #747474;
|
||||
--icons-hover: #000000;
|
||||
|
||||
--links: #4183c4;
|
||||
--links: #20609f;
|
||||
|
||||
--inline-code-color: #6e6b5e;
|
||||
--inline-code-color: #301900;
|
||||
|
||||
--theme-popup-bg: #fafafa;
|
||||
--theme-popup-border: #cccccc;
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;;
|
||||
--inline-code-color: #c5c8c6;
|
||||
|
||||
--theme-popup-bg: #161923;
|
||||
--theme-popup-border: #737480;
|
||||
@@ -228,7 +228,7 @@
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;;
|
||||
--inline-code-color: #c5c8c6;
|
||||
|
||||
--theme-popup-bg: #141617;
|
||||
--theme-popup-border: #43484d;
|
||||
|
||||
22
src/theme/favicon.svg
Executable file
22
src/theme/favicon.svg
Executable file
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 199.7 184.2">
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
svg { fill: white; }
|
||||
}
|
||||
</style>
|
||||
<path d="M189.5,36.8c0.2,2.8,0,5.1-0.6,6.8L153,162c-0.6,2.1-2,3.7-4.2,5c-2.2,1.2-4.4,1.9-6.7,1.9H31.4c-9.6,0-15.3-2.8-17.3-8.4
|
||||
c-0.8-2.2-0.8-3.9,0.1-5.2c0.9-1.2,2.4-1.8,4.6-1.8H123c7.4,0,12.6-1.4,15.4-4.1s5.7-8.9,8.6-18.4l32.9-108.6
|
||||
c1.8-5.9,1-11.1-2.2-15.6S169.9,0,164,0H72.7c-1,0-3.1,0.4-6.1,1.1l0.1-0.4C64.5,0.2,62.6,0,61,0.1s-3,0.5-4.3,1.4
|
||||
c-1.3,0.9-2.4,1.8-3.2,2.8S52,6.5,51.2,8.1c-0.8,1.6-1.4,3-1.9,4.3s-1.1,2.7-1.8,4.2c-0.7,1.5-1.3,2.7-2,3.7c-0.5,0.6-1.2,1.5-2,2.5
|
||||
s-1.6,2-2.2,2.8s-0.9,1.5-1.1,2.2c-0.2,0.7-0.1,1.8,0.2,3.2c0.3,1.4,0.4,2.4,0.4,3.1c-0.3,3-1.4,6.9-3.3,11.6
|
||||
c-1.9,4.7-3.6,8.1-5.1,10.1c-0.3,0.4-1.2,1.3-2.6,2.7c-1.4,1.4-2.3,2.6-2.6,3.7c-0.3,0.4-0.3,1.5-0.1,3.4c0.3,1.8,0.4,3.1,0.3,3.8
|
||||
c-0.3,2.7-1.3,6.3-3,10.8c-1.7,4.5-3.4,8.2-5,11c-0.2,0.5-0.9,1.4-2,2.8c-1.1,1.4-1.8,2.5-2,3.4c-0.2,0.6-0.1,1.8,0.1,3.4
|
||||
c0.2,1.6,0.2,2.8-0.1,3.6c-0.6,3-1.8,6.7-3.6,11c-1.8,4.3-3.6,7.9-5.4,11c-0.5,0.8-1.1,1.7-2,2.8c-0.8,1.1-1.5,2-2,2.8
|
||||
s-0.8,1.6-1,2.5c-0.1,0.5,0,1.3,0.4,2.3c0.3,1.1,0.4,1.9,0.4,2.6c-0.1,1.1-0.2,2.6-0.5,4.4c-0.2,1.8-0.4,2.9-0.4,3.2
|
||||
c-1.8,4.8-1.7,9.9,0.2,15.2c2.2,6.2,6.2,11.5,11.9,15.8c5.7,4.3,11.7,6.4,17.8,6.4h110.7c5.2,0,10.1-1.7,14.7-5.2s7.7-7.8,9.2-12.9
|
||||
l33-108.6c1.8-5.8,1-10.9-2.2-15.5C194.9,39.7,192.6,38,189.5,36.8z M59.6,122.8L73.8,80c0,0,7,0,10.8,0s28.8-1.7,25.4,17.5
|
||||
c-3.4,19.2-18.8,25.2-36.8,25.4S59.6,122.8,59.6,122.8z M78.6,116.8c4.7-0.1,18.9-2.9,22.1-17.1S89.2,86.3,89.2,86.3l-8.9,0
|
||||
l-10.2,30.5C70.2,116.9,74,116.9,78.6,116.8z M75.3,68.7L89,26.2h9.8l0.8,34l23.6-34h9.9l-13.6,42.5h-7.1l12.5-35.4l-24.5,35.4h-6.8
|
||||
l-0.8-35L82,68.7H75.3z"/>
|
||||
</svg>
|
||||
<!-- Original image Copyright Dave Gandy — CC BY 4.0 License -->
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
202
src/theme/fonts/OPEN-SANS-LICENSE.txt
Executable file
202
src/theme/fonts/OPEN-SANS-LICENSE.txt
Executable file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
93
src/theme/fonts/SOURCE-CODE-PRO-LICENSE.txt
Normal file
93
src/theme/fonts/SOURCE-CODE-PRO-LICENSE.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
100
src/theme/fonts/fonts.css
Normal file
100
src/theme/fonts/fonts.css
Normal file
@@ -0,0 +1,100 @@
|
||||
/* Open Sans is licensed under the Apache License, Version 2.0. See http://www.apache.org/licenses/LICENSE-2.0 */
|
||||
/* Source Code Pro is under the Open Font License. See https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL */
|
||||
|
||||
/* open-sans-300 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'),
|
||||
url('open-sans-v17-all-charsets-300.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'),
|
||||
url('open-sans-v17-all-charsets-300italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'),
|
||||
url('open-sans-v17-all-charsets-regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'),
|
||||
url('open-sans-v17-all-charsets-italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
|
||||
url('open-sans-v17-all-charsets-600.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-600italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'),
|
||||
url('open-sans-v17-all-charsets-700.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),
|
||||
url('open-sans-v17-all-charsets-700italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'),
|
||||
url('open-sans-v17-all-charsets-800.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-800italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Code Pro';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
|
||||
}
|
||||
61
src/theme/fonts/mod.rs
Normal file
61
src/theme/fonts/mod.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
pub static CSS: &[u8] = include_bytes!("fonts.css");
|
||||
// An array of (file_name, file_contents) pairs
|
||||
pub static LICENSES: [(&str, &[u8]); 2] = [
|
||||
(
|
||||
"fonts/OPEN-SANS-LICENSE.txt",
|
||||
include_bytes!("OPEN-SANS-LICENSE.txt"),
|
||||
),
|
||||
(
|
||||
"fonts/SOURCE-CODE-PRO-LICENSE.txt",
|
||||
include_bytes!("SOURCE-CODE-PRO-LICENSE.txt"),
|
||||
),
|
||||
];
|
||||
// An array of (file_name, file_contents) pairs
|
||||
pub static OPEN_SANS: [(&str, &[u8]); 10] = [
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-300.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-300.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-300italic.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-300italic.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-regular.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-regular.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-italic.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-italic.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-600.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-600.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-600italic.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-600italic.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-700.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-700.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-700italic.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-700italic.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-800.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-800.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-800italic.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-800italic.woff2"),
|
||||
),
|
||||
];
|
||||
|
||||
// A (file_name, file_contents) pair
|
||||
pub static SOURCE_CODE_PRO: (&str, &[u8]) = (
|
||||
"fonts/source-code-pro-v11-all-charsets-500.woff2",
|
||||
include_bytes!("source-code-pro-v11-all-charsets-500.woff2"),
|
||||
);
|
||||
BIN
src/theme/fonts/open-sans-v17-all-charsets-300.woff2
Normal file
BIN
src/theme/fonts/open-sans-v17-all-charsets-300.woff2
Normal file
Binary file not shown.
BIN
src/theme/fonts/open-sans-v17-all-charsets-300italic.woff2
Normal file
BIN
src/theme/fonts/open-sans-v17-all-charsets-300italic.woff2
Normal file
Binary file not shown.
BIN
src/theme/fonts/open-sans-v17-all-charsets-600.woff2
Normal file
BIN
src/theme/fonts/open-sans-v17-all-charsets-600.woff2
Normal file
Binary file not shown.
BIN
src/theme/fonts/open-sans-v17-all-charsets-600italic.woff2
Normal file
BIN
src/theme/fonts/open-sans-v17-all-charsets-600italic.woff2
Normal file
Binary file not shown.
BIN
src/theme/fonts/open-sans-v17-all-charsets-700.woff2
Normal file
BIN
src/theme/fonts/open-sans-v17-all-charsets-700.woff2
Normal file
Binary file not shown.
BIN
src/theme/fonts/open-sans-v17-all-charsets-700italic.woff2
Normal file
BIN
src/theme/fonts/open-sans-v17-all-charsets-700italic.woff2
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user