mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 12:41:16 -05:00
Compare commits
419 Commits
update-rea
...
v0.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
8e673c96c2 | ||
|
|
99ecd4f87c | ||
|
|
e839ef0866 | ||
|
|
769cc0a7c1 | ||
|
|
c2686a817a | ||
|
|
bd14d0910a | ||
|
|
6eb597a556 | ||
|
|
5c91041dad | ||
|
|
59568208ff | ||
|
|
21a16c9b75 | ||
|
|
4e8e1e1408 | ||
|
|
2baed040c2 | ||
|
|
101063093b | ||
|
|
f7ffffbd1e | ||
|
|
760c9b0767 | ||
|
|
6016e12b90 | ||
|
|
88684d843d | ||
|
|
b82562fe8a | ||
|
|
44c3213f5d | ||
|
|
fd56a53e76 | ||
|
|
ca4b85b815 | ||
|
|
d7a2b29f06 | ||
|
|
4039c72fd3 | ||
|
|
2bd8bdf798 | ||
|
|
0da7ba4abe | ||
|
|
d6cfa21fff | ||
|
|
95fba3f357 | ||
|
|
d5999849d9 | ||
|
|
8b2659e0f4 | ||
|
|
c4a64ab599 | ||
|
|
6b4e3584b4 | ||
|
|
b8fc7a1b2d | ||
|
|
2ee083dfbe | ||
|
|
1947f8ca65 | ||
|
|
2f59943c04 | ||
|
|
980f943179 | ||
|
|
5e998788e9 | ||
|
|
6a94492238 | ||
|
|
e3717ad47b | ||
|
|
49b7f08164 | ||
|
|
7def6d70e8 | ||
|
|
554f29703f | ||
|
|
730d7f8410 | ||
|
|
b6603468d6 | ||
|
|
441a10bdd7 | ||
|
|
efdb83266a | ||
|
|
ac9c12334a | ||
|
|
2a3088422a | ||
|
|
1f505c2b2e | ||
|
|
a7b3aa0444 | ||
|
|
a9160acd64 | ||
|
|
4c1bca1684 | ||
|
|
8fffb2a704 | ||
|
|
ba37cc8462 | ||
|
|
3ea0f9b745 | ||
|
|
29d8747e01 | ||
|
|
f5549f2267 | ||
|
|
e2a8600712 | ||
|
|
f2cb601c11 | ||
|
|
6e0d0facff | ||
|
|
f79d5d4582 | ||
|
|
820714a560 | ||
|
|
d5535d1226 | ||
|
|
e5f77aaaf2 | ||
|
|
86a368b726 | ||
|
|
1dc482b00d | ||
|
|
21d8f394ae | ||
|
|
c9dae170f3 | ||
|
|
fcf2d7a03b | ||
|
|
2498887dfc | ||
|
|
f04d7b802d | ||
|
|
bfcddf2680 | ||
|
|
2b649fe94f | ||
|
|
fc4236eaa7 | ||
|
|
a592da33bb | ||
|
|
6af6219e5b | ||
|
|
e5f74b6c86 | ||
|
|
84a2ab0dba | ||
|
|
d63ef8330d | ||
|
|
01e50303a2 | ||
|
|
2b3304cb8b | ||
|
|
4448f3fc4b | ||
|
|
859659f197 | ||
|
|
4a93eddae2 | ||
|
|
0173451b67 | ||
|
|
ac1749ff2f | ||
|
|
8cdeb121c5 | ||
|
|
74313bb701 | ||
|
|
3c25dba9b4 | ||
|
|
2387942588 | ||
|
|
93c9ae5700 | ||
|
|
9efa9fd1c4 | ||
|
|
8a33407cc5 | ||
|
|
699844a5c3 | ||
|
|
9bdec5e7cc | ||
|
|
930f730361 | ||
|
|
09c738468f | ||
|
|
a3d1afdd1f | ||
|
|
8e8e53ae15 | ||
|
|
5fe801a7d1 | ||
|
|
a6f317e352 | ||
|
|
ed95252f05 | ||
|
|
a058da8b74 | ||
|
|
73be1292ab | ||
|
|
98ecd1178b | ||
|
|
996ac382c1 | ||
|
|
b88839cc25 | ||
|
|
1ef94c2a7e | ||
|
|
f0ac13e3e2 | ||
|
|
b0ae14a2c7 | ||
|
|
81ab2eb7db | ||
|
|
213171591a | ||
|
|
db13d8e561 | ||
|
|
b4bb44292d | ||
|
|
bb7a863d3e | ||
|
|
e62a9dba87 | ||
|
|
4a94b656cd | ||
|
|
a873d46871 | ||
|
|
ce0c5f1d07 | ||
|
|
33d7e86fb6 | ||
|
|
f9f9785839 | ||
|
|
0c37b912ba | ||
|
|
e880fb6339 | ||
|
|
a8d6337ac6 | ||
|
|
f37a89cd4c | ||
|
|
aaeb3e2852 | ||
|
|
8c4b292d58 | ||
|
|
40159362c0 | ||
|
|
aa67245743 | ||
|
|
d968443074 | ||
|
|
3716123e10 | ||
|
|
50a2ec3cf1 | ||
|
|
07459aef60 | ||
|
|
0f56c09d3a | ||
|
|
63ad3d9340 | ||
|
|
1c5dc1e310 | ||
|
|
77af889a2e | ||
|
|
e48fed74bf | ||
|
|
e512850c13 | ||
|
|
bb412edf53 | ||
|
|
5b0a23ebab | ||
|
|
e56c41a1c2 | ||
|
|
d1b5a8f982 | ||
|
|
f396623b63 | ||
|
|
9ec43b6c6d | ||
|
|
7c4d2070f7 | ||
|
|
50d5917530 | ||
|
|
9cd47eb80f | ||
|
|
4932df2570 | ||
|
|
11d31c989c | ||
|
|
e5ace6d6a4 | ||
|
|
e7c3d02c61 | ||
|
|
d8a68ba3f6 | ||
|
|
d29a79349c | ||
|
|
d6088c8a57 | ||
|
|
b91e5c8807 | ||
|
|
6199e4df79 | ||
|
|
2d11eb05fe | ||
|
|
3d45e40693 | ||
|
|
228e99ba11 | ||
|
|
4b569edadd | ||
|
|
3e652b5bfc | ||
|
|
ba41d73dc3 | ||
|
|
1ce1401263 | ||
|
|
00b3d9cf86 | ||
|
|
bb3398bdbb | ||
|
|
19c26217c0 | ||
|
|
a2029f0a78 | ||
|
|
7c33ac800c | ||
|
|
d371001ab8 | ||
|
|
d73504eb23 | ||
|
|
abddd7c6f7 | ||
|
|
31e36f85e7 | ||
|
|
92a7b0cdcd | ||
|
|
592140db5b | ||
|
|
3a0eeb4bbb | ||
|
|
a9dae326fa | ||
|
|
abba959add | ||
|
|
ea15e55829 | ||
|
|
d07bd9fed4 | ||
|
|
b83c55f7ef | ||
|
|
69a08ef390 | ||
|
|
1cd1151790 | ||
|
|
84d4063e4a | ||
|
|
07830f7f11 | ||
|
|
828b7d05c5 | ||
|
|
379004efcb | ||
|
|
2497e77bf1 | ||
|
|
0c2292b9aa | ||
|
|
4386a10e87 | ||
|
|
3cfed10098 | ||
|
|
a655d5d241 | ||
|
|
f8c3a2deea | ||
|
|
b226d2fc55 | ||
|
|
53ba0d6655 | ||
|
|
43ead86ecc | ||
|
|
1d3ec7e0c7 | ||
|
|
f4017376a9 | ||
|
|
672cf456eb | ||
|
|
8dce00d54d | ||
|
|
4f7c299de7 | ||
|
|
04e74bfa1b | ||
|
|
4026a586a1 | ||
|
|
71281bff10 | ||
|
|
8542f7f29d | ||
|
|
fe492d1cb9 | ||
|
|
481c2f2194 | ||
|
|
882014860c | ||
|
|
e3ec751a3f | ||
|
|
fc565df86b | ||
|
|
ec8e63145c | ||
|
|
2752c88c46 | ||
|
|
e7befd19bc | ||
|
|
644b8e132c | ||
|
|
8e82ae534a | ||
|
|
6a8a5b7642 | ||
|
|
c3284a2ae9 | ||
|
|
df12cc55c8 | ||
|
|
cb4a3e0711 | ||
|
|
9194a40acd | ||
|
|
506996808b | ||
|
|
5163c5ab75 | ||
|
|
ecfaed1e02 | ||
|
|
8bb5426441 | ||
|
|
a674c9eff1 | ||
|
|
7ab939f8f2 | ||
|
|
581187098c | ||
|
|
ab7802a9a9 | ||
|
|
345acb8597 | ||
|
|
6380526e93 | ||
|
|
4560bdeb47 | ||
|
|
0aa3a9045a | ||
|
|
b30b58b565 | ||
|
|
c6220fba83 | ||
|
|
652eab6e7e | ||
|
|
5726a8afd6 | ||
|
|
7e26a8430d | ||
|
|
07a64b110a | ||
|
|
dd69e03ff5 | ||
|
|
7f3a0ff6a0 | ||
|
|
aea317e173 | ||
|
|
f9454615b1 | ||
|
|
39211291d9 | ||
|
|
f01fe854fa | ||
|
|
6eeaaaa44d | ||
|
|
357ebcf7ce | ||
|
|
1a4f38eace | ||
|
|
1d3f83eede | ||
|
|
9712347b9c | ||
|
|
f73d42d994 | ||
|
|
a647017e4b | ||
|
|
a66d44190e | ||
|
|
01fd7a76f0 | ||
|
|
99dc62f9c3 | ||
|
|
b891fd5a12 | ||
|
|
02fa7b0a11 | ||
|
|
8b2e1c2daa | ||
|
|
88d2f69138 | ||
|
|
cb94053779 | ||
|
|
0a8707b1e6 | ||
|
|
0dc2728fa9 | ||
|
|
9b02cd7496 | ||
|
|
11f86f4511 | ||
|
|
4abac12c04 | ||
|
|
d7c7d91005 | ||
|
|
9243cf9d95 | ||
|
|
d2470730fc | ||
|
|
6a2e2461fb | ||
|
|
3faa3e42f0 | ||
|
|
9c8fae4704 | ||
|
|
9b6f5a9840 | ||
|
|
62af2367bb | ||
|
|
37808b7e08 | ||
|
|
b37f21a09b | ||
|
|
966632a724 | ||
|
|
c7281459f9 | ||
|
|
ae3f87ad0c | ||
|
|
c068703028 | ||
|
|
6cbc41d413 | ||
|
|
25c1ca1275 | ||
|
|
acbb951240 | ||
|
|
9e96165d8f | ||
|
|
5c5ef2f86b | ||
|
|
23ac06e2eb | ||
|
|
2ddbb37f49 | ||
|
|
a481735fa2 | ||
|
|
954cfa86e5 | ||
|
|
7e52da3c1b | ||
|
|
4e8d051bd1 | ||
|
|
78ee8e43bb | ||
|
|
b675b91980 | ||
|
|
3d8db7f25c | ||
|
|
3d37e24c14 | ||
|
|
eb19d2d654 | ||
|
|
1052ee92e1 | ||
|
|
3598e905aa | ||
|
|
3f002979c4 | ||
|
|
742dbbc917 | ||
|
|
991a725c26 | ||
|
|
317c7731da | ||
|
|
4c17b11ed0 | ||
|
|
005dfc55bf | ||
|
|
8c86031384 | ||
|
|
5151aae07e | ||
|
|
42b87e0fbc | ||
|
|
33add4b532 | ||
|
|
b0513ee771 | ||
|
|
b4538da9c3 | ||
|
|
7ac3e50b37 | ||
|
|
13a9aab2b2 | ||
|
|
eccec9bb52 | ||
|
|
e63f53fe47 | ||
|
|
2c20c99d4a | ||
|
|
c6125b184f | ||
|
|
dfb6e3cb10 | ||
|
|
41071a5dd9 | ||
|
|
f6a7432569 | ||
|
|
89ea60e7a5 | ||
|
|
10b69e60c8 | ||
|
|
336e08fe50 | ||
|
|
5bfdf9fcc8 | ||
|
|
d2565af000 | ||
|
|
599e47f1f1 | ||
|
|
0c31ab2953 | ||
|
|
f654c42426 | ||
|
|
d729a762fe | ||
|
|
43b3d157d9 | ||
|
|
a9f3be6f44 | ||
|
|
34356b87a0 |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
* text=auto eol=lf
|
||||
*.rs rust
|
||||
*.woff -text
|
||||
*.ttf -text
|
||||
*.otf -text
|
||||
*.png -text
|
||||
*.woff binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.png binary
|
||||
|
||||
42
.github/workflows/deploy.yml
vendored
Normal file
42
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Deploy
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Deploy Release
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install hub
|
||||
run: ci/install-hub.sh ${{ matrix.os }}
|
||||
shell: bash
|
||||
- name: Install Rust
|
||||
run: ci/install-rust.sh stable
|
||||
shell: bash
|
||||
- name: Build and deploy artifacts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ci/make-release.sh ${{ matrix.os }}
|
||||
shell: bash
|
||||
pages:
|
||||
name: GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Build book
|
||||
run: cargo run -- build book-example
|
||||
- name: Deploy to GitHub
|
||||
env:
|
||||
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
|
||||
run: |
|
||||
touch book-example/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
|
||||
/tmp/deploy
|
||||
51
.github/workflows/main.yml
vendored
Normal file
51
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: CI
|
||||
on:
|
||||
# Only run when merging to master, or open/synchronize/reopen a PR.
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
build: [stable, beta, nightly, macos, windows, msrv]
|
||||
include:
|
||||
- build: stable
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
- build: beta
|
||||
os: ubuntu-latest
|
||||
rust: beta
|
||||
- build: nightly
|
||||
os: ubuntu-latest
|
||||
rust: nightly
|
||||
- build: macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
- build: windows
|
||||
os: windows-latest
|
||||
rust: stable
|
||||
- build: msrv
|
||||
os: ubuntu-latest
|
||||
rust: 1.39.0
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh ${{ matrix.rust }}
|
||||
- name: Build and run tests
|
||||
run: cargo test
|
||||
- name: Test no default
|
||||
run: cargo test --no-default-features
|
||||
|
||||
rustfmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust
|
||||
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||
- run: cargo fmt -- --check
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,3 +9,5 @@ book-example/book
|
||||
.vscode
|
||||
tests/dummy_book/book/
|
||||
|
||||
# Ignore Jetbrains specific files.
|
||||
.idea/
|
||||
|
||||
48
.travis.yml
48
.travis.yml
@@ -1,48 +0,0 @@
|
||||
language: rust
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
cache:
|
||||
timeout: 360
|
||||
cargo: true
|
||||
|
||||
before_cache:
|
||||
- chmod -R a+r $HOME/.cargo
|
||||
|
||||
env:
|
||||
global:
|
||||
- CRATE_NAME=mdbook
|
||||
|
||||
script:
|
||||
- cargo test --all
|
||||
- cargo test --all --no-default-features
|
||||
|
||||
before_deploy:
|
||||
- sh ci/before_deploy.sh
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
- secure: cURRWBr034iqBz/ifD7uOunBfNR30YxIXfgLX0osWz+iafkVbhDGYYz9sBmRraqO2P7L2koEXMADVb/md1kI2+ykiq/ml+l9zuEAZPVmvSGUN7ZD+7s+lu3l5OBPG5z175T+b2q2q2m8XVR7TW20ra4QbE0bq06KAoOyjSgQVBTSCYsL9uTsGwiVRMEqqJT/BmKhKJNkpGsTKyBSKkOXvfeAAbE260vXUDEN9TYdJ3fvteRrpwLX56ee64gIZUq0RjDc4SKIEqilM6iUtNMvurqaewYNGkiXKRruV6BPCHxEHo6NNT46kOJLBJTf7gZw//dWhSoWpg9P0gdAnPWm407kSa3F7aJ1eRShAFQ4BLyfz9efTqm+jP3fOp7Mm7igSh9w6caSRuOnSsUf5+raRQ8E5Y9HsWGzzpZQk24Fx9EGZ04EeDSdpZAFz+jcbMpHf8t2p4CEx0CCNwYvKx6EydMKbMF5QteQ8SQkXNLhv7Rz2OgtXWYZPRVCMfQfOplsi2InsLCrQxTgwh+6u654SqVSgaHG+IncEAxBrdWy4rHcg7qereUcKfcY3k96vaDxdn/T2c00Ig0aNFR91YnixGMd6J6tQgDcRK9jh6fUm1CCBE9hT+pNUmtgYKuWBoLZexUZFFnfuBed0WciBot1bGDDamndqKq0jJiAzg+GMHk=
|
||||
file_glob: true
|
||||
file: "$CRATE_NAME-$TRAVIS_TAG-$TARGET.*"
|
||||
on:
|
||||
condition: "$TRAVIS_RUST_VERSION = stable"
|
||||
tags: true
|
||||
skip_cleanup: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
479
CHANGELOG.md
Normal file
479
CHANGELOG.md
Normal file
@@ -0,0 +1,479 @@
|
||||
# Changelog
|
||||
|
||||
## 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)
|
||||
- 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)
|
||||
|
||||
### Changed
|
||||
- Code spans in headers are no longer highlighted as code.
|
||||
[#1162](https://github.com/rust-lang/mdBook/pull/1162)
|
||||
- The sidebar will now scroll the activate page to the middle instead of the top.
|
||||
[#1161](https://github.com/rust-lang/mdBook/pull/1161)
|
||||
- Reverted change to reject build output within the `src` directory, and
|
||||
instead add a check that prevents infinite copies.
|
||||
[#1181](https://github.com/rust-lang/mdBook/pull/1181)
|
||||
[#1026](https://github.com/rust-lang/mdBook/pull/1026)
|
||||
|
||||
### Fixed
|
||||
- Fixed sidebar line-height jumping for collapsed chapters.
|
||||
[#1182](https://github.com/rust-lang/mdBook/pull/1182)
|
||||
- Fixed theme selector focus.
|
||||
[#1170](https://github.com/rust-lang/mdBook/pull/1170)
|
||||
|
||||
## mdBook 0.3.6
|
||||
[efdb832...88684d8](https://github.com/rust-lang/mdBook/compare/efdb832...88684d8)
|
||||
|
||||
### Added
|
||||
- `MDBook::execute_build_process` is now publicly accessible in the API so
|
||||
that plugins can more easily initiate the build process.
|
||||
[#1099](https://github.com/rust-lang/mdBook/pull/1099)
|
||||
|
||||
### Changed
|
||||
- Use a different color for Ayu theme's highlighting for Rust attributes (uses
|
||||
a bright color instead of the comment color).
|
||||
[#1133](https://github.com/rust-lang/mdBook/pull/1133)
|
||||
- Adjusted spacing of sidebar entries.
|
||||
[#1137](https://github.com/rust-lang/mdBook/pull/1137)
|
||||
- Slightly adjusted line-height of `<p>`, `<ul>`, and `<ol>`.
|
||||
[#1136](https://github.com/rust-lang/mdBook/pull/1136)
|
||||
- Handlebars updated to 3.0.
|
||||
[#1130](https://github.com/rust-lang/mdBook/pull/1130)
|
||||
|
||||
### Fixed
|
||||
- Fix an issue with sidebar scroll position on reload.
|
||||
[#1108](https://github.com/rust-lang/mdBook/pull/1108)
|
||||
- `mdbook serve` will retain the current scroll position when the page is reloaded.
|
||||
[#1097](https://github.com/rust-lang/mdBook/pull/1097)
|
||||
- Fixed the page name if the book didn't have a title to not be prefixed with ` - `.
|
||||
[#1145](https://github.com/rust-lang/mdBook/pull/1145)
|
||||
- HTML attributes `rel=next` and `rel=previous` are now supported in "wide"
|
||||
mode (previously they were only set in narrow mode).
|
||||
[#1150](https://github.com/rust-lang/mdBook/pull/1150)
|
||||
- Prevent recursive copies when the destination directory is contained in the
|
||||
source directory.
|
||||
[#1135](https://github.com/rust-lang/mdBook/pull/1135)
|
||||
- Adjusted the menu bar animation to not immediately obscure the top content.
|
||||
[#989](https://github.com/rust-lang/mdBook/pull/989)
|
||||
- Fix for comments in SUMMARY.md that appear between items.
|
||||
[#1167](https://github.com/rust-lang/mdBook/pull/1167)
|
||||
|
||||
## mdBook 0.3.5
|
||||
[6e0d0fa...efdb832](https://github.com/rust-lang/mdBook/compare/6e0d0fa...efdb832)
|
||||
|
||||
### Changed
|
||||
- The `default-theme` config setting is now case-insensitive.
|
||||
[#1079](https://github.com/rust-lang/mdBook/pull/1079)
|
||||
|
||||
### Fixed
|
||||
- Fixed `#` hidden Rust code lines not rendering properly.
|
||||
[#1088](https://github.com/rust-lang/mdBook/pull/1088)
|
||||
- Updated pulldown-cmark to 0.6.1, fixing several issues.
|
||||
[#1021](https://github.com/rust-lang/mdBook/pull/1021)
|
||||
|
||||
## mdBook 0.3.4
|
||||
[e5f77aa...6e0d0fa](https://github.com/rust-lang/mdBook/compare/e5f77aa...6e0d0fa)
|
||||
|
||||
### Changed
|
||||
- Switch to relative `rem` font sizes from `px`.
|
||||
[#894](https://github.com/rust-lang/mdBook/pull/894)
|
||||
- Migrated repository to https://github.com/rust-lang/mdBook/
|
||||
[#1083](https://github.com/rust-lang/mdBook/pull/1083)
|
||||
|
||||
## mdBook 0.3.3
|
||||
[2b649fe...e5f77aa](https://github.com/rust-lang/mdBook/compare/2b649fe...e5f77aa)
|
||||
|
||||
### Changed
|
||||
- Improvements to the automatic dark theme selection.
|
||||
[#1069](https://github.com/rust-lang/mdBook/pull/1069)
|
||||
- Fragment links now prevent scrolling the header behind the menu bar.
|
||||
[#1077](https://github.com/rust-lang/mdBook/pull/1077)
|
||||
|
||||
### Fixed
|
||||
- Fixed error when building a book that has a spacer immediately after the
|
||||
first chapter.
|
||||
[#1075](https://github.com/rust-lang/mdBook/pull/1075)
|
||||
|
||||
## mdBook 0.3.2
|
||||
[9cd47eb...2b649fe](https://github.com/rust-lang/mdBook/compare/9cd47eb...2b649fe)
|
||||
|
||||
### Added
|
||||
- Added a markdown renderer, which is off by default. This may be useful for
|
||||
debugging preprocessors.
|
||||
[#1018](https://github.com/rust-lang/mdBook/pull/1018)
|
||||
- Code samples may now include line numbers with the
|
||||
`output.html.playpen.line-numbers` configuration value.
|
||||
[#1035](https://github.com/rust-lang/mdBook/pull/1035)
|
||||
- The `watch` and `serve` commands will now ignore files listed in
|
||||
`.gitignore`.
|
||||
[#1044](https://github.com/rust-lang/mdBook/pull/1044)
|
||||
- Added automatic dark-theme detection based on the CSS `prefers-color-scheme`
|
||||
feature. This may be enabled by setting `output.html.preferred-dark-theme`
|
||||
to your preferred dark theme.
|
||||
[#1037](https://github.com/rust-lang/mdBook/pull/1037)
|
||||
- Added `rustdoc_include` preprocessor. This makes it easier to include
|
||||
portions of an external Rust source file. The rest of the file is hidden,
|
||||
but the user may expand it to see the entire file, and will continue to work
|
||||
with `mdbook test`.
|
||||
[#1003](https://github.com/rust-lang/mdBook/pull/1003)
|
||||
- Added Ctrl-Enter shortcut to the playpen editor to automatically run the
|
||||
sample.
|
||||
[#1066](https://github.com/rust-lang/mdBook/pull/1066)
|
||||
- Added `output.html.playpen.copyable` configuration option to disable
|
||||
the copy button.
|
||||
[#1050](https://github.com/rust-lang/mdBook/pull/1050)
|
||||
- Added ability to dynamically expand and fold sections within the sidebar.
|
||||
See the `output.html.fold` configuration to enable this feature.
|
||||
[#1027](https://github.com/rust-lang/mdBook/pull/1027)
|
||||
|
||||
### Changed
|
||||
- Use standard `scrollbar-color` CSS along with webkit extension
|
||||
[#816](https://github.com/rust-lang/mdBook/pull/816)
|
||||
- The renderer build directory is no longer deleted before the renderer is
|
||||
run. This allows a backend to cache results between runs.
|
||||
[#985](https://github.com/rust-lang/mdBook/pull/985)
|
||||
- Next/prev links now highlight on hover to indicate it is clickable.
|
||||
[#994](https://github.com/rust-lang/mdBook/pull/994)
|
||||
- Increase padding of table headers.
|
||||
[#824](https://github.com/rust-lang/mdBook/pull/824)
|
||||
- Errors in `[output.html]` config are no longer ignored.
|
||||
[#1033](https://github.com/rust-lang/mdBook/pull/1033)
|
||||
- Updated highlight.js for syntax highlighting updates (primarily to add
|
||||
async/await to Rust highlighting).
|
||||
[#1041](https://github.com/rust-lang/mdBook/pull/1041)
|
||||
- Raised minimum supported rust version to 1.35.
|
||||
[#1003](https://github.com/rust-lang/mdBook/pull/1003)
|
||||
- Hidden code lines are no longer dynamically removed via JavaScript, but
|
||||
instead managed with CSS.
|
||||
[#846](https://github.com/rust-lang/mdBook/pull/846)
|
||||
[#1065](https://github.com/rust-lang/mdBook/pull/1065)
|
||||
- Changed the default font set for the ACE editor, giving preference to
|
||||
"Source Code Pro".
|
||||
[#1062](https://github.com/rust-lang/mdBook/pull/1062)
|
||||
- Windows 32-bit releases are no longer published.
|
||||
[#1071](https://github.com/rust-lang/mdBook/pull/1071)
|
||||
|
||||
### Fixed
|
||||
- Fixed sidebar auto-scrolling.
|
||||
[#1052](https://github.com/rust-lang/mdBook/pull/1052)
|
||||
- Fixed error message when running `clean` multiple times.
|
||||
[#1055](https://github.com/rust-lang/mdBook/pull/1055)
|
||||
- Actually fix the "next" link on index.html. The previous fix didn't work.
|
||||
[#1005](https://github.com/rust-lang/mdBook/pull/1005)
|
||||
- Stop using `inline-block` for `inline code`, fixing selection highlighting
|
||||
and some rendering issues.
|
||||
[#1058](https://github.com/rust-lang/mdBook/pull/1058)
|
||||
- Fix header auto-hide on browsers with momentum scrolling that allows
|
||||
negative `scrollTop`.
|
||||
[#1070](https://github.com/rust-lang/mdBook/pull/1070)
|
||||
|
||||
## mdBook 0.3.1
|
||||
[69a08ef...9cd47eb](https://github.com/rust-lang/mdBook/compare/69a08ef...9cd47eb)
|
||||
|
||||
### Added
|
||||
- 🔥 Added ability to include files using anchor points instead of line numbers.
|
||||
[#851](https://github.com/rust-lang/mdBook/pull/851)
|
||||
- Added `language` configuration value to set the language of the book, which
|
||||
will affect things like the `<html lang="en">` tag.
|
||||
[#941](https://github.com/rust-lang/mdBook/pull/941)
|
||||
|
||||
### Changed
|
||||
- Updated to handlebars 2.0.
|
||||
[#977](https://github.com/rust-lang/mdBook/pull/977)
|
||||
|
||||
### Fixed
|
||||
- Fixed memory leak warning.
|
||||
[#967](https://github.com/rust-lang/mdBook/pull/967)
|
||||
- Fix more print.html links.
|
||||
[#963](https://github.com/rust-lang/mdBook/pull/963)
|
||||
- Fixed crash on some unicode input.
|
||||
[#978](https://github.com/rust-lang/mdBook/pull/978)
|
||||
|
||||
## mdBook 0.3.0
|
||||
[6cbc41d...69a08ef](https://github.com/rust-lang/mdBook/compare/6cbc41d...69a08ef)
|
||||
|
||||
### Added
|
||||
- Added ability to resize the sidebar.
|
||||
[#849](https://github.com/rust-lang/mdBook/pull/849)
|
||||
- Added `load_with_config_and_summary` function to `MDBook` to be able to
|
||||
build a book with a custom `Summary`.
|
||||
[#883](https://github.com/rust-lang/mdBook/pull/883)
|
||||
- Set `noindex` on `print.html` page to prevent robots from indexing it.
|
||||
[#844](https://github.com/rust-lang/mdBook/pull/844)
|
||||
- Added support for ~~strikethrough~~ and GitHub-style tasklists.
|
||||
[#952](https://github.com/rust-lang/mdBook/pull/952)
|
||||
|
||||
### Changed
|
||||
- Command-line help output is now colored.
|
||||
[#861](https://github.com/rust-lang/mdBook/pull/861)
|
||||
- The build directory is now deleted before rendering starts, instead of after
|
||||
if finishes.
|
||||
[#878](https://github.com/rust-lang/mdBook/pull/878)
|
||||
- Removed dependency on `same-file` crate.
|
||||
[#903](https://github.com/rust-lang/mdBook/pull/903)
|
||||
- 💥 Renamed `with_preprecessor` to `with_preprocessor`.
|
||||
[#906](https://github.com/rust-lang/mdBook/pull/906)
|
||||
- Updated ACE editor to 1.4.4, should remove a JavaScript console warning.
|
||||
[#935](https://github.com/rust-lang/mdBook/pull/935)
|
||||
- Dependencies have been updated.
|
||||
[#934](https://github.com/rust-lang/mdBook/pull/934)
|
||||
[#945](https://github.com/rust-lang/mdBook/pull/945)
|
||||
- Highlight.js has been updated. This fixes some TOML highlighting, and adds
|
||||
Julia support.
|
||||
[#942](https://github.com/rust-lang/mdBook/pull/942)
|
||||
- 🔥 Updated to pulldown-cmark 0.5. This may have significant changes to the
|
||||
formatting of existing books, as the newer version has more accurate
|
||||
interpretation of the CommonMark spec and a large number of bug fixes and
|
||||
changes.
|
||||
[#898](https://github.com/rust-lang/mdBook/pull/898)
|
||||
- The `diff` language should now highlight correctly.
|
||||
[#943](https://github.com/rust-lang/mdBook/pull/943)
|
||||
- Make the blank region of a header not clickable.
|
||||
[#948](https://github.com/rust-lang/mdBook/pull/948)
|
||||
- Rustdoc tests now use the preprocessed content instead of the raw,
|
||||
unpreprocessed content.
|
||||
[#891](https://github.com/rust-lang/mdBook/pull/891)
|
||||
|
||||
### Fixed
|
||||
- Fixed file change detection so that `mdbook serve` only reloads once when
|
||||
multiple files are changed at once.
|
||||
[#870](https://github.com/rust-lang/mdBook/pull/870)
|
||||
- Fixed on-hover color highlighting for links in sidebar.
|
||||
[#834](https://github.com/rust-lang/mdBook/pull/834)
|
||||
- Fixed loss of focus when clicking the "Copy" button in code blocks.
|
||||
[#867](https://github.com/rust-lang/mdBook/pull/867)
|
||||
- Fixed incorrectly stripping the path for `additional-js` files.
|
||||
[#796](https://github.com/rust-lang/mdBook/pull/796)
|
||||
- Fixed color of `code spans` that are links.
|
||||
[#905](https://github.com/rust-lang/mdBook/pull/905)
|
||||
- Fixed "next" navigation on index.html.
|
||||
[#916](https://github.com/rust-lang/mdBook/pull/916)
|
||||
- Fixed keyboard chapter navigation for `file` urls.
|
||||
[#915](https://github.com/rust-lang/mdBook/pull/915)
|
||||
- Fixed bad wrapping for inline code on some browsers.
|
||||
[#818](https://github.com/rust-lang/mdBook/pull/818)
|
||||
- Properly load an existing `SUMMARY.md` in `mdbook init`.
|
||||
[#841](https://github.com/rust-lang/mdBook/pull/841)
|
||||
- Fixed some broken links in `print.html`.
|
||||
[#871](https://github.com/rust-lang/mdBook/pull/871)
|
||||
- The Rust Playground link now supports the 2018 edition.
|
||||
[#946](https://github.com/rust-lang/mdBook/pull/946)
|
||||
|
||||
## mdBook 0.2.3 (2018-01-18)
|
||||
[2c20c99...6cbc41d](https://github.com/rust-lang/mdBook/compare/2c20c99...6cbc41d)
|
||||
|
||||
### Added
|
||||
- Added an optional button to the top of the page which will link to a git
|
||||
repository. Use the `git-repository-url` and `git-repository-icon` options
|
||||
in the `[output.html]` section to enable it and set its appearance.
|
||||
[#802](https://github.com/rust-lang/mdBook/pull/802)
|
||||
- Added a `default-theme` option to the `[output.html]` section.
|
||||
[#804](https://github.com/rust-lang/mdBook/pull/804)
|
||||
|
||||
### Changed
|
||||
- 💥 Header ID anchors no longer add an arbitrary `a` character for headers
|
||||
that start with a non-ascii-alphabetic character.
|
||||
[#788](https://github.com/rust-lang/mdBook/pull/788)
|
||||
|
||||
### Fixed
|
||||
- Fix websocket hostname usage
|
||||
[#865](https://github.com/rust-lang/mdBook/pull/865)
|
||||
- Fixing links in print.html
|
||||
[#866](https://github.com/rust-lang/mdBook/pull/866)
|
||||
|
||||
## mdBook 0.2.2 (2018-10-19)
|
||||
[7e2e095...2c20c99](https://github.com/rust-lang/mdBook/compare/7e2e095...2c20c99)
|
||||
|
||||
### Added
|
||||
- 🎉 Process-based custom preprocessors. See [the
|
||||
docs](https://rust-lang.github.io/mdBook/for_developers/preprocessors.html)
|
||||
for more.
|
||||
[#792](https://github.com/rust-lang/mdBook/pull/792)
|
||||
|
||||
- 🎉 Configurable preprocessors.
|
||||
|
||||
Added `build.use-default-preprocessors` boolean TOML key to allow disabling
|
||||
the built-in `links` and `index` preprocessors.
|
||||
|
||||
Added `[preprocessor]` TOML tables to configure each preprocessor.
|
||||
|
||||
Specifying `[preprocessor.links]` or `[preprocessor.index]` will enable the
|
||||
respective built-in preprocessor if `build.use-default-preprocessors` is
|
||||
`false`.
|
||||
|
||||
Added `fn supports_renderer(&self, renderer: &str) -> bool` to the
|
||||
`Preprocessor` trait to specify if the preprocessor supports the given
|
||||
renderer. The default implementation always returns `true`.
|
||||
|
||||
`Preprocessor::run` now takes a book by value instead of a mutable
|
||||
reference. It should return a `Book` value with the intended modifications.
|
||||
|
||||
Added `PreprocessorContext::renderer` to indicate the renderer being used.
|
||||
|
||||
[#658](https://github.com/rust-lang/mdBook/pull/658)
|
||||
[#787](https://github.com/rust-lang/mdBook/pull/787)
|
||||
|
||||
### Fixed
|
||||
- Fix paths to additional CSS and JavaScript files
|
||||
[#777](https://github.com/rust-lang/mdBook/pull/777)
|
||||
- Ensure section numbers are correctly incremented after a horizontal
|
||||
separator
|
||||
[#790](https://github.com/rust-lang/mdBook/pull/790)
|
||||
|
||||
## mdBook 0.2.1 (2018-08-22)
|
||||
[91ffca1...7e2e095](https://github.com/rust-lang/mdBook/compare/91ffca1...7e2e095)
|
||||
|
||||
### Changed
|
||||
- Update to handlebars-rs 1.0
|
||||
[#761](https://github.com/rust-lang/mdBook/pull/761)
|
||||
|
||||
### Fixed
|
||||
- Fix table colors, broken by Stylus -> CSS transition
|
||||
[#765](https://github.com/rust-lang/mdBook/pull/765)
|
||||
|
||||
## mdBook 0.2.0 (2018-08-02)
|
||||
|
||||
### Changed
|
||||
- 💥 This release changes how links are handled in mdBook. Previously, relative
|
||||
links were interpreted relative to the book's root. In `0.2.0`+ links are
|
||||
relative to the page they are in, and use the `.md` extension. This has [several
|
||||
advantages](https://github.com/rust-lang/mdBook/pull/603#issue-166701447),
|
||||
such as making links work in other markdown viewers like GitHub. You will
|
||||
likely have to change links in your book to accommodate this change. For
|
||||
example, a book with this layout:
|
||||
|
||||
```
|
||||
chapter_1/
|
||||
section_1.md
|
||||
section_2.md
|
||||
SUMMARY.md
|
||||
```
|
||||
|
||||
Previously a link in `section_1.md` to `section_2.md` would look like this:
|
||||
```markdown
|
||||
[section_2](chapter_1/section_2.html)
|
||||
```
|
||||
|
||||
Now it must be changed to this:
|
||||
```markdown
|
||||
[section_2](section_2.md)
|
||||
```
|
||||
|
||||
- 💥 `mdbook test --library-path` now accepts a comma-delimited list of
|
||||
arguments rather than taking all following arguments. This makes it easier
|
||||
to handle the trailing book directory argument without always needing to put
|
||||
` -- ` before it. Multiple instances of the option continue to be accepted:
|
||||
`mdbook test -L foo -L bar`.
|
||||
|
||||
- 💥 `mdbook serve` has some of its options renamed for clarity. See `mdbook
|
||||
help serve` for details.
|
||||
|
||||
- Embedded rust playpens now use the "stable" playground API.
|
||||
[#754](https://github.com/rust-lang/mdBook/pull/754)
|
||||
|
||||
### Fixed
|
||||
- Escaped includes (`\{{#include file.rs}}`) will now render correctly.
|
||||
[f30ce01](https://github.com/rust-lang/mdBook/commit/f30ce0184d71e342141145472bf816419d30a2c5)
|
||||
- `index.html` will now render correctly when the book's first section is
|
||||
inside a subdirectory.
|
||||
[#756](https://github.com/rust-lang/mdBook/pull/756)
|
||||
@@ -5,22 +5,22 @@ Welcome stranger!
|
||||
If you have come here to learn how to contribute to mdBook, we have some tips for you!
|
||||
|
||||
First of all, don't hesitate to ask questions!
|
||||
Use the [issue tracker](https://github.com/rust-lang-nursery/mdBook/issues), no question is too simple.
|
||||
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
|
||||
|
||||
Any issue is up for the grabbing, but if you are starting out, you might be interested in the
|
||||
[E-Easy issues](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
||||
[E-Easy issues](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
||||
Those are issues that are considered more straightforward for beginners to Rust or the codebase itself.
|
||||
These issues can be a good launching pad for more involved issues. Easy tasks for a first time contribution
|
||||
include documentation improvements, new tests, examples, updating dependencies, etc.
|
||||
|
||||
If you come from a web development background, you might be interested in issues related to web technologies tagged
|
||||
[A-JavaScript](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||
[A-Style](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
|
||||
[A-HTML](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
||||
[A-Mobile](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
|
||||
[A-JavaScript](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||
[A-Style](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
|
||||
[A-HTML](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
||||
[A-Mobile](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
|
||||
|
||||
When you decide you want to work on a specific issue, ping us on that issue so that we can assign it to you.
|
||||
Again, do not hesitate to ask questions. We will gladly mentor anyone that want to tackle an issue.
|
||||
@@ -41,7 +41,7 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
|
||||
0. Clone this repository with git.
|
||||
|
||||
```
|
||||
git clone https://github.com/rust-lang-nursery/mdBook.git
|
||||
git clone https://github.com/rust-lang/mdBook.git
|
||||
```
|
||||
0. Navigate into the newly created `mdBook` directory
|
||||
0. Run `cargo build`
|
||||
@@ -57,12 +57,12 @@ We love code quality and Rust has some excellent tools to assist you with contri
|
||||
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
|
||||
This will ensure we have good quality source code that is better for us all to maintain.
|
||||
|
||||
[rustfmt](https://github.com/rust-lang-nursery/rustfmt) has a lot more information on the project.
|
||||
[rustfmt](https://github.com/rust-lang/rustfmt) has a lot more information on the project.
|
||||
The quick guide is
|
||||
|
||||
1. Install it
|
||||
```
|
||||
rustup component add rustfmt-preview
|
||||
rustup component add rustfmt
|
||||
```
|
||||
1. You can now run `rustfmt` on a single file simply by...
|
||||
```
|
||||
@@ -74,7 +74,7 @@ The quick guide is
|
||||
```
|
||||
When run through `cargo` it will format all bin and lib files in the current crate.
|
||||
|
||||
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang-nursery/rustfmt)
|
||||
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang/rustfmt)
|
||||
|
||||
|
||||
#### Finding Issues with Clippy
|
||||
@@ -83,21 +83,18 @@ Clippy is a code analyser/linter detecting mistakes, and therfore helps to impro
|
||||
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will
|
||||
help us maintain awesome code.
|
||||
|
||||
The best documentation can be found over at [rust-clippy](https://github.com/rust-lang-nursery/rust-clippy)
|
||||
The best documentation can be found over at [rust-clippy](https://github.com/rust-lang/rust-clippy)
|
||||
|
||||
1. To install
|
||||
```
|
||||
rustup update
|
||||
rustup install nightly
|
||||
rustup component add clippy-preview --toolchain=nightly
|
||||
rustup component add clippy
|
||||
```
|
||||
2. Running clippy
|
||||
As you may notice from the previous step, Clippy is on the nightly branch, so running it is like
|
||||
```
|
||||
cargo +nightly clippy
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
Clippy has an ever growing list of checks, that are managed in [lint files](https://rust-lang-nursery.github.io/rust-clippy/master/index.html).
|
||||
Clippy has an ever growing list of checks, that are managed in [lint files](https://rust-lang.github.io/rust-clippy/master/index.html).
|
||||
|
||||
### Making a pull-request
|
||||
|
||||
@@ -105,7 +102,7 @@ When you feel comfortable that your changes could be integrated into mdBook, you
|
||||
One of the core maintainers will then approve the changes or request some changes before it gets merged.
|
||||
|
||||
If you want to make your pull-request even better, you might want to run [Clippy](https://github.com/Manishearth/rust-clippy)
|
||||
and [rustfmt](https://github.com/rust-lang-nursery/rustfmt) on the code first.
|
||||
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:
|
||||
|
||||
2008
Cargo.lock
generated
2008
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
57
Cargo.toml
57
Cargo.toml
@@ -1,64 +1,61 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.2.2-alpha.0"
|
||||
version = "0.4.1"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
"Matt Ickstadt <mattico8@gmail.com>"
|
||||
]
|
||||
description = "Create books from markdown files"
|
||||
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
|
||||
repository = "https://github.com/rust-lang-nursery/mdBook"
|
||||
documentation = "http://rust-lang.github.io/mdBook/index.html"
|
||||
edition = "2018"
|
||||
exclude = ["/book-example/*"]
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
exclude = ["book-example/*"]
|
||||
repository = "https://github.com/rust-lang/mdBook"
|
||||
description = "Creates a book from markdown files"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.24"
|
||||
anyhow = "1.0.28"
|
||||
chrono = "0.4"
|
||||
handlebars = "1.0"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
error-chain = "0.12"
|
||||
serde_json = "1.0"
|
||||
pulldown-cmark = "0.1.2"
|
||||
clap = "2.24"
|
||||
env_logger = "0.7.1"
|
||||
handlebars = "3.0"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.5"
|
||||
toml = "0.4"
|
||||
memchr = "2.0"
|
||||
open = "1.1"
|
||||
pulldown-cmark = "0.7.0"
|
||||
regex = "1.0.0"
|
||||
tempfile = "3.0"
|
||||
itertools = "0.7"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
shlex = "0.1"
|
||||
toml-query = "0.7"
|
||||
tempfile = "3.0"
|
||||
toml = "0.5.1"
|
||||
|
||||
# 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.7", optional = true}
|
||||
futures-util = { version = "0.3.4", optional = true }
|
||||
tokio = { version = "0.2.18", features = ["macros"], optional = true }
|
||||
warp = { version = "0.2.2", default-features = false, features = ["websocket"], optional = true }
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
||||
ammonia = { version = "1.1", optional = true }
|
||||
ammonia = { version = "3", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
select = "0.4"
|
||||
pretty_assertions = "0.5"
|
||||
select = "0.5"
|
||||
pretty_assertions = "0.6"
|
||||
walkdir = "2.0"
|
||||
pulldown-cmark-to-cmark = "1.1.0"
|
||||
|
||||
[features]
|
||||
default = ["output", "watch", "serve", "search"]
|
||||
debug = []
|
||||
output = []
|
||||
watch = ["notify"]
|
||||
serve = ["iron", "staticfile", "ws"]
|
||||
default = ["watch", "serve", "search"]
|
||||
watch = ["notify", "gitignore"]
|
||||
serve = ["futures-util", "tokio", "warp"]
|
||||
search = ["elasticlunr-rs", "ammonia"]
|
||||
|
||||
[[bin]]
|
||||
|
||||
72
README.md
72
README.md
@@ -1,25 +1,8 @@
|
||||
# mdBook
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><strong>Linux / OS X</strong></td>
|
||||
<td>
|
||||
<a href="https://travis-ci.org/rust-lang-nursery/mdBook"><img src="https://travis-ci.org/rust-lang-nursery/mdBook.svg?branch=master"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Windows</strong></td>
|
||||
<td>
|
||||
<a href="https://ci.appveyor.com/project/rust-lang-libs/mdbook"><img src="https://ci.appveyor.com/api/projects/status/ysyke2rvo85sni55?svg=true"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a href="https://crates.io/crates/mdbook"><img src="https://img.shields.io/crates/v/mdbook.svg"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/rust-lang-nursery/mdBook.svg"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
[](https://github.com/rust-lang/mdBook/actions?workflow=CI)
|
||||
[](https://crates.io/crates/mdbook)
|
||||
[](LICENSE)
|
||||
|
||||
mdBook is a utility to create modern online books from Markdown files.
|
||||
|
||||
@@ -41,7 +24,7 @@ There are multiple ways to install mdBook.
|
||||
|
||||
2. **From Crates.io**
|
||||
|
||||
This requires at least [Rust] 1.20 and Cargo to be installed. Once you have installed
|
||||
This requires at least [Rust] 1.39 and Cargo to be installed. Once you have installed
|
||||
Rust, type the following in the terminal:
|
||||
|
||||
```
|
||||
@@ -57,33 +40,37 @@ There are multiple ways to install mdBook.
|
||||
another CI server, we recommend that you specify a semver version range for
|
||||
mdBook when you install it through your script!
|
||||
|
||||
This will constrain the server to install the latests **non-breaking**
|
||||
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. For example:
|
||||
we released a new version.
|
||||
|
||||
You can also disable default features to speed up compile time.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
cargo install mdbook --vers "^0.1.0"
|
||||
cargo install mdbook --no-default-features --features output --vers "^0.1.0"
|
||||
```
|
||||
|
||||
3. **From Git**
|
||||
3. **From Git**
|
||||
|
||||
The version published to crates.io will ever so slightly be behind the
|
||||
version hosted here on GitHub. If you need the latest version you can build
|
||||
the git version of mdBook yourself. Cargo makes this ***super easy***!
|
||||
|
||||
```
|
||||
cargo install --git https://github.com/rust-lang-nursery/mdBook.git mdbook
|
||||
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**
|
||||
4. **For Contributions**
|
||||
|
||||
If you want to contribute to mdBook you will have to clone the repository on
|
||||
your local machine:
|
||||
|
||||
```
|
||||
git clone https://github.com/rust-lang-nursery/mdBook.git
|
||||
git clone https://github.com/rust-lang/mdBook.git
|
||||
```
|
||||
|
||||
`cd` into `mdBook/` and run
|
||||
@@ -152,16 +139,16 @@ 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 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.
|
||||
`{{# playground}}` 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
|
||||
@@ -169,6 +156,9 @@ 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
|
||||
|
||||
@@ -214,7 +204,7 @@ 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 issus that are
|
||||
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.
|
||||
@@ -231,14 +221,14 @@ available, for those hacking on `master`.
|
||||
All the code in this repository is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE] file.
|
||||
|
||||
|
||||
[User Guide]: https://rust-lang-nursery.github.io/mdBook/
|
||||
[User Guide]: https://rust-lang.github.io/mdBook/
|
||||
[API docs]: https://docs.rs/mdbook/*/mdbook/
|
||||
[E-Easy]: https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy
|
||||
[contribution guide]: https://github.com/rust-lang-nursery/mdBook/blob/master/CONTRIBUTING.md
|
||||
[LICENSE]: https://github.com/rust-lang-nursery/mdBook/blob/master/LICENSE
|
||||
[releases]: https://github.com/rust-lang-nursery/mdBook/releases
|
||||
[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-nursery.github.io/mdBook/cli/init.html
|
||||
[master-docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
|
||||
[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
|
||||
[`epub`]: https://crates.io/crates/mdbook-epub
|
||||
|
||||
64
appveyor.yml
64
appveyor.yml
@@ -1,64 +0,0 @@
|
||||
environment:
|
||||
global:
|
||||
PROJECT_NAME: mdBook
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: stable
|
||||
# Beta channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: beta
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: beta
|
||||
# Nightly channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: nightly
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: nightly
|
||||
|
||||
# Install Rust and Cargo
|
||||
install:
|
||||
- ps: >-
|
||||
If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\msys64\mingw64\bin'
|
||||
} ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\msys64\mingw32\bin'
|
||||
}
|
||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
|
||||
- rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_CHANNEL%
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -Vv
|
||||
- cargo -V
|
||||
|
||||
build: false
|
||||
|
||||
# Equivalent to Travis' `script` phase
|
||||
test_script:
|
||||
- cargo test --all
|
||||
- cargo test --all --no-default-features
|
||||
|
||||
before_deploy:
|
||||
# Generate artifacts for release
|
||||
- cargo rustc --bin mdbook --release -- -C lto
|
||||
- mkdir staging
|
||||
- copy target\release\mdbook.exe staging
|
||||
- cd staging
|
||||
- 7z a ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip *
|
||||
- appveyor PushArtifact ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip
|
||||
|
||||
deploy:
|
||||
description: 'Windows release'
|
||||
artifact: /.*\.zip/
|
||||
auth_token:
|
||||
secure: QQhjKVyz7mpjlyGhlXytbFQQfKFQWTahHkD+B0NzIUoEVqO7ZLWjnoWasvLqW4nE
|
||||
provider: GitHub
|
||||
on:
|
||||
RUST_CHANNEL: stable
|
||||
appveyor_repo_tag: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
@@ -2,12 +2,18 @@
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David", "Michael-F-Bryan"]
|
||||
language = "en"
|
||||
|
||||
[rust]
|
||||
edition = "2018"
|
||||
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
site-url = "/mdBook/"
|
||||
|
||||
[output.html.playpen]
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
line-numbers = true
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 20
|
||||
|
||||
3
book-example/src/404.md
Normal file
3
book-example/src/404.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Document not found (404)
|
||||
|
||||
This URL is invalid, sorry. Try the search instead!
|
||||
@@ -8,9 +8,9 @@ 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-nursery/mdBook). Issues and feature
|
||||
[GitHub](https://github.com/rust-lang/mdBook). Issues and feature
|
||||
requests can be posted on the [GitHub issue
|
||||
tracker](https://github.com/rust-lang-nursery/mdBook/issues).
|
||||
tracker](https://github.com/rust-lang/mdBook/issues).
|
||||
|
||||
## API docs
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
- [clean](cli/clean.md)
|
||||
- [Format](format/README.md)
|
||||
- [SUMMARY.md](format/summary.md)
|
||||
- [Draft chapter]()
|
||||
- [Configuration](format/config.md)
|
||||
- [Theme](format/theme/README.md)
|
||||
- [index.hbs](format/theme/index-hbs.md)
|
||||
@@ -20,7 +21,7 @@
|
||||
- [Continuous Integration](continuous-integration.md)
|
||||
- [For Developers](for_developers/README.md)
|
||||
- [Preprocessors](for_developers/preprocessors.md)
|
||||
- [Alternate Backends](for_developers/backends.md)
|
||||
- [Alternative Backends](for_developers/backends.md)
|
||||
|
||||
-----------
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ capabilities first.
|
||||
## Install From Binaries
|
||||
|
||||
Precompiled binaries are provided for major platforms on a best-effort basis.
|
||||
Visit [the releases page](https://github.com/rust-lang-nursery/mdBook/releases)
|
||||
Visit [the releases page](https://github.com/rust-lang/mdBook/releases)
|
||||
to download the appropriate version for your platform.
|
||||
|
||||
## Install From Source
|
||||
@@ -18,7 +18,7 @@ mdBook can also be installed from source
|
||||
|
||||
mdBook is written in **[Rust](https://www.rust-lang.org/)** and therefore needs
|
||||
to be compiled with **Cargo**. If you haven't already installed Rust, please go
|
||||
ahead and [install it](https://www.rust-lang.org/downloads.html) now.
|
||||
ahead and [install it](https://www.rust-lang.org/tools/install) now.
|
||||
|
||||
### Install Crates.io version
|
||||
|
||||
@@ -39,14 +39,14 @@ have installed mdBook!
|
||||
|
||||
### Install Git version
|
||||
|
||||
The **[git version](https://github.com/rust-lang-nursery/mdBook)** contains all
|
||||
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-nursery/mdBook.git
|
||||
git clone --depth=1 https://github.com/rust-lang/mdBook.git
|
||||
cd mdBook
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
@@ -29,10 +29,11 @@ your default web browser after building it.
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
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`.
|
||||
|
||||
-------------------
|
||||
|
||||
***Note:*** *Make sure to run the build command in the root directory and not in
|
||||
the source directory*
|
||||
***Note:*** *The build command copies all files (excluding files with `.md` extension) from the source directory
|
||||
into the build directory.*
|
||||
|
||||
@@ -19,9 +19,9 @@ mdbook clean path/to/book
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to override the book's output
|
||||
directory, which will be deleted by this command. If not specified it will
|
||||
default to the value of the `build.build-dir` key in `book.toml`, or to `./book`
|
||||
relative to the book's root directory.
|
||||
directory, which will be deleted by this command. 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`.
|
||||
|
||||
```bash
|
||||
mdbook clean --dest-dir=path/to/book
|
||||
|
||||
@@ -19,7 +19,7 @@ book-test/
|
||||
└── SUMMARY.md
|
||||
```
|
||||
|
||||
- The `src` directory is were you write your book in markdown. It contains all
|
||||
- The `src` directory is where you write your book in markdown. It contains all
|
||||
the source files, configuration files, etc.
|
||||
|
||||
- The `book` directory is where your book is rendered. All the output is ready
|
||||
|
||||
@@ -5,6 +5,9 @@ The serve command is used to preview a book by serving it over HTTP at
|
||||
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
|
||||
@@ -34,16 +37,23 @@ configured.
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the book in your your
|
||||
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. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
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
|
||||
|
||||
***Note:*** *The `serve` command is for testing, and is not intended to be a
|
||||
complete HTTP server for a website.*
|
||||
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 `.gitignore` from book root directory is used. Global
|
||||
`$HOME/.gitignore` or `.gitignore` files in parent directories are not used._
|
||||
|
||||
@@ -48,5 +48,6 @@ comma-delimited list (`-L foo,bar`).
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
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`.
|
||||
|
||||
@@ -22,5 +22,18 @@ your default web browser.
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
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._
|
||||
|
||||
@@ -22,7 +22,7 @@ rust:
|
||||
|
||||
before_script:
|
||||
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
|
||||
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.1" mdbook)
|
||||
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.3" mdbook)
|
||||
- cargo install-update -a
|
||||
|
||||
script:
|
||||
@@ -54,3 +54,36 @@ deploy:
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
@@ -12,14 +12,14 @@ The *For Developers* chapters are here to show you the more advanced usage of
|
||||
The two main ways a developer can hook into the book's build process is via,
|
||||
|
||||
- [Preprocessors](preprocessors.md)
|
||||
- [Alternate Backends](backends.md)
|
||||
- [Alternative Backends](backends.md)
|
||||
|
||||
|
||||
## The Build Process
|
||||
|
||||
The process of rendering a book project goes through several steps.
|
||||
|
||||
1. Load the book
|
||||
1. Load the book
|
||||
- Parse the `book.toml`, falling back to the default `Config` if it doesn't
|
||||
exist
|
||||
- Load the book chapters into memory
|
||||
@@ -41,6 +41,6 @@ The easiest way to find out how to use the `mdbook` crate is by looking at the
|
||||
explanation on the configuration system.
|
||||
|
||||
|
||||
[`MDBook`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.MDBook.html
|
||||
[`MDBook`]: https://docs.rs/mdbook/*/mdbook/book/struct.MDBook.html
|
||||
[API Docs]: https://docs.rs/mdbook/*/mdbook/
|
||||
[config]: file:///home/michael/Documents/forks/mdBook/target/doc/mdbook/config/index.html
|
||||
[config]: https://docs.rs/mdbook/*/mdbook/config/index.html
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Alternate Backends
|
||||
# Alternative Backends
|
||||
|
||||
A "backend" is simply a program which `mdbook` will invoke during the book
|
||||
rendering process. This program is passed a JSON representation of the book and
|
||||
configuration information via `stdin`. Once the backend receives this
|
||||
information it is free to do whatever it wants.
|
||||
|
||||
There are already several alternate backends on GitHub which can be used as a
|
||||
There are already several alternative backends on GitHub which can be used as a
|
||||
rough example of how this is accomplished in practice.
|
||||
|
||||
- [mdbook-linkcheck] - a simple program for verifying the book doesn't contain
|
||||
@@ -14,7 +14,7 @@ rough example of how this is accomplished in practice.
|
||||
- [mdbook-test] - a program to run the book's contents through [rust-skeptic] to
|
||||
verify everything compiles and runs correctly (similar to `rustdoc --test`)
|
||||
|
||||
This page will step you through creating your own alternate backend in the form
|
||||
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.
|
||||
|
||||
@@ -24,9 +24,9 @@ no reason why it couldn't be accomplished using something like Python or Ruby.
|
||||
First you'll want to create a new binary program and add `mdbook` as a
|
||||
dependency.
|
||||
|
||||
```
|
||||
```shell
|
||||
$ cargo new --bin mdbook-wordcount
|
||||
$ cd mdbook-wordcount
|
||||
$ cd mdbook-wordcount
|
||||
$ cargo add mdbook
|
||||
```
|
||||
|
||||
@@ -52,8 +52,8 @@ fn main() {
|
||||
> **Note:** The `RenderContext` contains a `version` field. This lets backends
|
||||
figure out whether they are compatible with the version of `mdbook` it's being
|
||||
called by. This `version` comes directly from the corresponding field in
|
||||
`mdbook`'s `Cargo.toml`.
|
||||
|
||||
`mdbook`'s `Cargo.toml`.
|
||||
|
||||
It is recommended that backends use the [`semver`] crate to inspect this field
|
||||
and emit a warning if there may be a compatibility issue.
|
||||
|
||||
@@ -92,12 +92,12 @@ fn count_words(ch: &Chapter) -> usize {
|
||||
Now we've got the basics running, we want to actually use it. First, install the
|
||||
program.
|
||||
|
||||
```
|
||||
$ cargo install
|
||||
```shell
|
||||
$ cargo install --path .
|
||||
```
|
||||
|
||||
Then `cd` to the particular book you'd like to count the words of and update its
|
||||
`book.toml` file.
|
||||
`book.toml` file.
|
||||
|
||||
```diff
|
||||
[book]
|
||||
@@ -112,7 +112,7 @@ Then `cd` to the particular book you'd like to count the words of and update its
|
||||
|
||||
When it loads a book into memory, `mdbook` will inspect your `book.toml` file to
|
||||
try and figure out which backends to use by looking for all `output.*` tables.
|
||||
If none are provided it'll fall back to using the default HTML renderer.
|
||||
If none are provided it'll fall back to using the default HTML renderer.
|
||||
|
||||
Notably, this means if you want to add your own custom backend you'll also need
|
||||
to make sure to add the HTML backend, even if its table just stays empty.
|
||||
@@ -120,7 +120,7 @@ to make sure to add the HTML backend, even if its table just stays empty.
|
||||
Now you just need to build your book like normal, and everything should *Just
|
||||
Work*.
|
||||
|
||||
```
|
||||
```shell
|
||||
$ mdbook build
|
||||
...
|
||||
2018-01-16 07:31:15 [INFO] (mdbook::renderer): Invoking the "mdbook-wordcount" renderer
|
||||
@@ -140,7 +140,7 @@ Syntax highlighting: 314
|
||||
MathJax Support: 153
|
||||
Rust code specific features: 148
|
||||
For Developers: 788
|
||||
Alternate Backends: 710
|
||||
Alternative Backends: 710
|
||||
Contributors: 85
|
||||
```
|
||||
|
||||
@@ -169,7 +169,7 @@ arguments or be an interpreted script), you can use the `command` field.
|
||||
Now imagine you don't want to count the number of words on a particular chapter
|
||||
(it might be generated text/code, etc). The canonical way to do this is via the
|
||||
usual `book.toml` configuration file by adding items to your `[output.foo]`
|
||||
table.
|
||||
table.
|
||||
|
||||
The `Config` can be treated roughly as a nested hashmap which lets you call
|
||||
methods like `get()` to access the config's contents, with a
|
||||
@@ -211,13 +211,13 @@ and then add a check to make sure we skip ignored chapters.
|
||||
+ let cfg: WordcountConfig = ctx.config
|
||||
+ .get_deserialized("output.wordcount")
|
||||
+ .unwrap_or_default();
|
||||
|
||||
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
+ if cfg.ignores.contains(&ch.name) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
}
|
||||
@@ -239,17 +239,17 @@ in [`RenderContext`].
|
||||
- use std::io;
|
||||
use mdbook::renderer::RenderContext;
|
||||
use mdbook::book::{BookItem, Chapter};
|
||||
|
||||
|
||||
fn main() {
|
||||
...
|
||||
|
||||
|
||||
+ let _ = fs::create_dir_all(&ctx.destination);
|
||||
+ let mut f = File::create(ctx.destination.join("wordcounts.txt")).unwrap();
|
||||
+
|
||||
+
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
...
|
||||
|
||||
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
+ writeln!(f, "{}: {}", ch.name, num_words).unwrap();
|
||||
@@ -261,6 +261,10 @@ in [`RenderContext`].
|
||||
> **Note:** There is no guarantee that the destination directory exists or is
|
||||
> empty (`mdbook` may leave the previous contents to let backends do caching),
|
||||
> so it's always a good idea to create it with `fs::create_dir_all()`.
|
||||
>
|
||||
> If the destination directory already exists, don't assume it will be empty.
|
||||
> To allow backends to cache the results from previous runs, `mdbook` may leave
|
||||
> old content in the directory.
|
||||
|
||||
There's always the possibility that an error will occur while processing a book
|
||||
(just look at all the `unwrap()`'s we've written already), so `mdbook` will
|
||||
@@ -276,11 +280,11 @@ like this:
|
||||
|
||||
fn main() {
|
||||
...
|
||||
|
||||
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
...
|
||||
|
||||
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
writeln!(f, "{}: {}", ch.name, num_words).unwrap();
|
||||
@@ -303,8 +307,8 @@ like this:
|
||||
|
||||
Now, if we reinstall the backend and build a book,
|
||||
|
||||
```
|
||||
$ cargo install --force
|
||||
```shell
|
||||
$ cargo install --path . --force
|
||||
$ mdbook build /path/to/book
|
||||
...
|
||||
2018-01-16 21:21:39 [INFO] (mdbook::renderer): Invoking the "wordcount" renderer
|
||||
@@ -325,11 +329,41 @@ 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.
|
||||
|
||||
## Handling missing backends
|
||||
|
||||
If you enable a backend that isn't installed, the default behavior is to throw an error:
|
||||
|
||||
```text
|
||||
The command wasn't found, is the "wordcount" backend installed?
|
||||
```
|
||||
|
||||
This behavior can be changed by marking the backend as optional.
|
||||
|
||||
```diff
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David", "Michael-F-Bryan"]
|
||||
|
||||
[output.html]
|
||||
|
||||
[output.wordcount]
|
||||
command = "python /path/to/wordcount.py"
|
||||
+ optional = true
|
||||
```
|
||||
|
||||
This demotes the error to a warning, and it will instead look like this:
|
||||
|
||||
```text
|
||||
The command was not found, but was marked as optional.
|
||||
Command: wordcount
|
||||
```
|
||||
|
||||
|
||||
## Wrapping Up
|
||||
|
||||
Although contrived, hopefully this example was enough to show how you'd create
|
||||
an alternate backend for `mdbook`. If you feel it's missing something, don't
|
||||
an alternative backend for `mdbook`. If you feel it's missing something, don't
|
||||
hesitate to create an issue in the [issue tracker] so we can improve the user
|
||||
guide.
|
||||
|
||||
@@ -342,10 +376,10 @@ the source code or ask questions.
|
||||
[mdbook-epub]: https://github.com/Michael-F-Bryan/mdbook-epub
|
||||
[mdbook-test]: https://github.com/Michael-F-Bryan/mdbook-test
|
||||
[rust-skeptic]: https://github.com/budziq/rust-skeptic
|
||||
[`RenderContext`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html
|
||||
[`RenderContext::from_json()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html#method.from_json
|
||||
[`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
|
||||
[`Book`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html
|
||||
[`Book::iter()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html#method.iter
|
||||
[`Config`]: http://rust-lang-nursery.github.io/mdBook/mdbook/config/struct.Config.html
|
||||
[issue tracker]: https://github.com/rust-lang-nursery/mdBook/issues
|
||||
[`Book`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html
|
||||
[`Book::iter()`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html#method.iter
|
||||
[`Config`]: https://docs.rs/mdbook/*/mdbook/config/struct.Config.html
|
||||
[issue tracker]: https://github.com/rust-lang/mdBook/issues
|
||||
|
||||
@@ -109,7 +109,7 @@ For everything else, have a look [at the complete example][example].
|
||||
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
|
||||
[pc]: https://crates.io/crates/pulldown-cmark
|
||||
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
|
||||
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs
|
||||
[an example no-op preprocessor]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||
[example]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||
[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
|
||||
|
||||
@@ -10,13 +10,16 @@ title = "Example book"
|
||||
author = "John Doe"
|
||||
description = "The example book covers examples."
|
||||
|
||||
[rust]
|
||||
edition = "2018"
|
||||
|
||||
[build]
|
||||
build-dir = "my-example-book"
|
||||
create-missing = false
|
||||
|
||||
[preprocess.index]
|
||||
[preprocessor.index]
|
||||
|
||||
[preprocess.links]
|
||||
[preprocessor.links]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["custom.css"]
|
||||
@@ -27,7 +30,7 @@ limit-results = 15
|
||||
|
||||
## Supported configuration options
|
||||
|
||||
It is important to note that **any** relative path specified in the in the
|
||||
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.
|
||||
|
||||
@@ -42,6 +45,7 @@ This is general information about your book.
|
||||
- **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
|
||||
@@ -50,8 +54,25 @@ 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.
|
||||
|
||||
- **edition**: Rust edition to use by default for the code snippets. Default
|
||||
is "2015". Individual code blocks can be controlled with the `edition2015`
|
||||
or `edition2018` annotations, such as:
|
||||
|
||||
~~~text
|
||||
```rust,edition2015
|
||||
// This only works in 2015.
|
||||
let try = true;
|
||||
```
|
||||
~~~
|
||||
|
||||
### Build options
|
||||
|
||||
This controls the build process of your book.
|
||||
@@ -68,18 +89,18 @@ This controls the build process of your book.
|
||||
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
|
||||
- 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
|
||||
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
|
||||
`use-default-preprocessors` that `links` it will run.
|
||||
|
||||
## Configuring Preprocessors
|
||||
|
||||
The following preprocessors are available and included by default:
|
||||
|
||||
- `links`: Expand the `{{ #playpen }}` and `{{ #include }}` handlebars
|
||||
- `links`: Expand the `{{ #playground }}`, `{{ #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
|
||||
@@ -92,9 +113,9 @@ The following preprocessors are available and included by default:
|
||||
build-dir = "build"
|
||||
create-missing = false
|
||||
|
||||
[preprocess.links]
|
||||
[preprocessor.links]
|
||||
|
||||
[preprocess.index]
|
||||
[preprocessor.index]
|
||||
```
|
||||
|
||||
### Custom Preprocessor Configuration
|
||||
@@ -105,8 +126,8 @@ configuration to the preprocessor by adding key-value pairs to the table.
|
||||
|
||||
For example
|
||||
|
||||
```
|
||||
[preprocess.links]
|
||||
```toml
|
||||
[preprocessor.links]
|
||||
# set the renderers this preprocessor will run for
|
||||
renderers = ["html"]
|
||||
some_extra_feature = true
|
||||
@@ -117,7 +138,7 @@ some_extra_feature = true
|
||||
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
|
||||
```
|
||||
@@ -125,7 +146,7 @@ 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` executa`ble. If you want to use a
|
||||
`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.
|
||||
|
||||
@@ -146,8 +167,17 @@ 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:** 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
|
||||
@@ -160,15 +190,41 @@ The following configuration options are available:
|
||||
- **no-section-label:** mdBook by defaults adds section label in table of
|
||||
contents column. For example, "1.", "2.1". Set this option to true to disable
|
||||
those labels. Defaults to `false`.
|
||||
- **playpen:** A subtable for configuring various playpen settings.
|
||||
- **fold:** A subtable for configuring sidebar section-folding behavior.
|
||||
- **playground:** A subtable for configuring various playground 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`.
|
||||
- **redirect:** A subtable used for generating redirects when a page is moved.
|
||||
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`).
|
||||
- **input-404:** The name of the markdown file used for misssing 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 `/`.
|
||||
|
||||
Available configuration options for the `[output.html.playpen]` table:
|
||||
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.playground]` 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/
|
||||
|
||||
@@ -179,7 +235,7 @@ Available configuration options for the `[output.html.search]` table:
|
||||
- **teaser-word-count:** The number of words used for a search result teaser.
|
||||
Defaults to `30`.
|
||||
- **use-boolean-and:** Define the logical link between multiple search words. If
|
||||
true, all search words must appear in each result. Defaults to `true`.
|
||||
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
|
||||
@@ -203,25 +259,33 @@ title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
[build]
|
||||
build-dir = "book"
|
||||
create-missing = true
|
||||
preprocess = ["links", "index"]
|
||||
|
||||
[output.html]
|
||||
theme = "my-theme"
|
||||
default-theme = "light"
|
||||
preferred-dark-theme = "navy"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
mathjax-support = false
|
||||
copy-fonts = true
|
||||
google-analytics = "UA-123456-7"
|
||||
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"
|
||||
site-url = "/example-book/"
|
||||
input-404 = "not-found.md"
|
||||
|
||||
[output.html.playpen]
|
||||
editor = "./path/to/editor"
|
||||
[output.html.fold]
|
||||
enable = false
|
||||
level = 0
|
||||
|
||||
[output.html.playground]
|
||||
editable = false
|
||||
copy-js = true
|
||||
line-numbers = false
|
||||
|
||||
[output.html.search]
|
||||
enable = true
|
||||
searcher = "./path/to/searcher"
|
||||
limit-results = 30
|
||||
teaser-word-count = 30
|
||||
use-boolean-and = true
|
||||
@@ -231,18 +295,48 @@ boost-paragraph = 1
|
||||
expand = true
|
||||
heading-split-level = 3
|
||||
copy-js = true
|
||||
|
||||
[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"
|
||||
```
|
||||
|
||||
### 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](#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.
|
||||
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. See the [alternative backends] chapter for more detail.
|
||||
|
||||
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.
|
||||
The custom renderer has access to all the fields within its table (i.e.
|
||||
anything under `[output.foo]`). mdBook checks for two common fields:
|
||||
|
||||
- **command:** The command to execute for this custom renderer. Defaults to
|
||||
the name of the renderer with the `mdbook-` prefix (such as `mdbook-foo`).
|
||||
- **optional:** If `true`, then the command will be ignored if it is not
|
||||
installed, otherwise mdBook will fail with an error. Defaults to `false`.
|
||||
|
||||
[alternative backends]: ../for_developers/backends.md
|
||||
|
||||
## Environment Variables
|
||||
|
||||
@@ -274,11 +368,11 @@ book's title without needing to touch your `book.toml`.
|
||||
> This means, if you so desired, you could override all book metadata when
|
||||
> building the book with something like
|
||||
>
|
||||
> ```text
|
||||
> ```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.
|
||||
building.
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them
|
||||
with a `#`.
|
||||
with a `#` [in the same way that Rustdoc does][rustdoc-hide].
|
||||
|
||||
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example
|
||||
|
||||
```bash
|
||||
# fn main() {
|
||||
@@ -35,10 +37,20 @@ 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.
|
||||
|
||||
Usually, this command is used for including code snippets and examples. In this
|
||||
case, oftens one would include a specific part of the file e.g. which only
|
||||
contains the relevant lines for the example. We support four different modes of
|
||||
partial includes:
|
||||
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.
|
||||
|
||||
````hbs
|
||||
```
|
||||
\{{#include file.rs}}
|
||||
```
|
||||
````
|
||||
|
||||
## Including portions of a file
|
||||
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
|
||||
\{{#include file.rs:2}}
|
||||
@@ -53,22 +65,130 @@ the file are omitted. The third command includes all lines from line 2, i.e. the
|
||||
first line is omitted. The last command includes the excerpt of `file.rs`
|
||||
consisting of lines 2 to 10.
|
||||
|
||||
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
|
||||
any kind of commented line.
|
||||
|
||||
Consider the following file to include:
|
||||
```rs
|
||||
/* ANCHOR: all */
|
||||
|
||||
// ANCHOR: component
|
||||
struct Paddle {
|
||||
hello: f32,
|
||||
}
|
||||
// ANCHOR_END: component
|
||||
|
||||
////////// ANCHOR: system
|
||||
impl System for MySystem { ... }
|
||||
////////// ANCHOR_END: system
|
||||
|
||||
/* ANCHOR_END: all */
|
||||
```
|
||||
|
||||
Then in the book, all you have to do is:
|
||||
````hbs
|
||||
Here is a component:
|
||||
```rust,no_run,noplayground
|
||||
\{{#include file.rs:component}}
|
||||
```
|
||||
|
||||
Here is a system:
|
||||
```rust,no_run,noplayground
|
||||
\{{#include file.rs:system}}
|
||||
```
|
||||
|
||||
This is the full file.
|
||||
```rust,no_run,noplayground
|
||||
\{{#include file.rs:all}}
|
||||
```
|
||||
````
|
||||
|
||||
Lines containing anchor patterns inside the included anchor are ignored.
|
||||
|
||||
## Including a file but initially hiding all except specified lines
|
||||
|
||||
The `rustdoc_include` helper is for including code from external Rust files that contain complete
|
||||
examples, but only initially showing particular lines specified with line numbers or anchors in the
|
||||
same way as with `include`.
|
||||
|
||||
The lines not in the line number range or between the anchors will still be included, but they will
|
||||
be prefaced with `#`. This way, a reader can expand the snippet to see the complete example, and
|
||||
Rustdoc will use the complete example when you run `mdbook test`.
|
||||
|
||||
For example, consider a file named `file.rs` that contains this Rust program:
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let x = add_one(2);
|
||||
assert_eq!(x, 3);
|
||||
}
|
||||
|
||||
fn add_one(num: i32) -> i32 {
|
||||
num + 1
|
||||
}
|
||||
```
|
||||
|
||||
We can include a snippet that initially shows only line 2 by using this syntax:
|
||||
|
||||
````hbs
|
||||
To call the `add_one` function, we pass it an `i32` and bind the returned value to `x`:
|
||||
|
||||
```rust
|
||||
\{{#rustdoc_include file.rs:2}}
|
||||
```
|
||||
````
|
||||
|
||||
This would have the same effect as if we had manually inserted the code and hidden all but line 2
|
||||
using `#`:
|
||||
|
||||
````hbs
|
||||
To call the `add_one` function, we pass it an `i32` and bind the returned value to `x`:
|
||||
|
||||
```rust
|
||||
# fn main() {
|
||||
let x = add_one(2);
|
||||
# assert_eq!(x, 3);
|
||||
# }
|
||||
#
|
||||
# fn add_one(num: i32) -> i32 {
|
||||
# num + 1
|
||||
#}
|
||||
```
|
||||
````
|
||||
|
||||
That is, it looks like this (click the "expand" icon to see the rest of the file):
|
||||
|
||||
```rust
|
||||
# fn main() {
|
||||
let x = add_one(2);
|
||||
# assert_eq!(x, 3);
|
||||
# }
|
||||
#
|
||||
# fn add_one(num: i32) -> i32 {
|
||||
# num + 1
|
||||
#}
|
||||
```
|
||||
|
||||
## Inserting runnable Rust files
|
||||
|
||||
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/
|
||||
[Rust Playground]: https://play.rust-lang.org/
|
||||
|
||||
@@ -7,7 +7,7 @@ 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
|
||||
#### Structure
|
||||
|
||||
1. ***Title*** It's common practice to begin with a title, generally <code
|
||||
class="language-markdown"># Summary</code>. But it is not mandatory, the
|
||||
@@ -22,17 +22,45 @@ allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
|
||||
[Title of prefix element](relative/path/to/markdown.md)
|
||||
```
|
||||
|
||||
3. ***Numbered Chapter*** Numbered chapters are the main content of the book,
|
||||
3. ***Part Title:*** Headers can be used as a title for the following numbered
|
||||
chapters. This can be used to logically separate different sections
|
||||
of book. The title is rendered as unclickable text.
|
||||
Titles are optional, and the numbered chapters can be broken into as many
|
||||
parts as desired.
|
||||
|
||||
4. ***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)
|
||||
|
||||
# Title of Another Part
|
||||
|
||||
- [More Chapters](relative/path/to/markdown2.md)
|
||||
```
|
||||
You can either use `-` or `*` to indicate a numbered chapter.
|
||||
|
||||
4. ***Suffix Chapter*** After the numbered chapters you can add a couple of
|
||||
5. ***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.
|
||||
|
||||
#### Other elements
|
||||
|
||||
- ***Separators*** In between chapters you can add a separator. In the HTML renderer
|
||||
this will result in a line being rendered in the table of contents. A separator is
|
||||
a line containing exclusively dashes and at least three of them: `---`.
|
||||
- ***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]()
|
||||
```
|
||||
|
||||
@@ -11,16 +11,19 @@ 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
|
||||
- **_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.
|
||||
- **_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 /
|
||||
- **_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
|
||||
- **_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
|
||||
@@ -32,3 +35,10 @@ 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.
|
||||
|
||||
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`]: ../config.md#html-renderer-options
|
||||
[newer browsers]: https://caniuse.com/#feat=link-icon-svg
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
@@ -19,7 +19,7 @@ fn main() {
|
||||
}
|
||||
```</code></pre>
|
||||
|
||||
The above will result in this editable playpen:
|
||||
The above will result in this editable playground:
|
||||
|
||||
```rust,editable
|
||||
fn main() {
|
||||
@@ -28,7 +28,7 @@ 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
|
||||
|
||||
@@ -36,7 +36,7 @@ By default, the editor is the [Ace](https://ace.c9.io/) editor, but, if desired,
|
||||
the functionality may be overriden by providing a different folder:
|
||||
|
||||
```toml
|
||||
[output.html.playpen]
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
editor = "/path/to/editor"
|
||||
```
|
||||
|
||||
@@ -17,9 +17,8 @@ handlebars template you can access this information by using
|
||||
|
||||
Here is a list of the properties that are exposed:
|
||||
|
||||
- ***language*** Language of the book in the form `en`. To use in <code
|
||||
class="language-html">\<html lang="{{ language }}"></code> for example. At the
|
||||
moment it is hardcoded.
|
||||
- ***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`
|
||||
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
||||
|
||||
@@ -45,53 +44,57 @@ at your disposal.
|
||||
|
||||
### 1. toc
|
||||
|
||||
The toc helper is used like this
|
||||
The toc helper is used like this
|
||||
|
||||
```handlebars
|
||||
{{#toc}}{{/toc}}
|
||||
```
|
||||
```handlebars
|
||||
{{#toc}}{{/toc}}
|
||||
```
|
||||
|
||||
and outputs something that looks like this, depending on the structure of your book
|
||||
and outputs something that looks like this, depending on the structure of your
|
||||
book
|
||||
|
||||
```html
|
||||
<ul class="chapter">
|
||||
<li><a href="link/to/file.html">Some chapter</a></li>
|
||||
<li>
|
||||
<ul class="section">
|
||||
<li><a href="link/to/other_file.html">Some other Chapter</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
```html
|
||||
<ul class="chapter">
|
||||
<li><a href="link/to/file.html">Some chapter</a></li>
|
||||
<li>
|
||||
<ul class="section">
|
||||
<li><a href="link/to/other_file.html">Some other Chapter</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
If you would like to make a toc with another structure, you have access to the chapters property containing all the data.
|
||||
The only limitation at the moment is that you would have to do it with JavaScript instead of with a handlebars helper.
|
||||
If you would like to make a toc with another structure, you have access to the
|
||||
chapters property containing all the data. The only limitation at the moment
|
||||
is that you would have to do it with JavaScript instead of with a handlebars
|
||||
helper.
|
||||
|
||||
```html
|
||||
<script>
|
||||
var chapters = {{chapters}};
|
||||
// Processing here
|
||||
</script>
|
||||
```
|
||||
```html
|
||||
<script>
|
||||
var chapters = {{chapters}};
|
||||
// Processing here
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. previous / next
|
||||
|
||||
The previous and next helpers expose a `link` and `name` property to the previous and next chapters.
|
||||
The previous and next helpers expose a `link` and `name` property to the
|
||||
previous and next chapters.
|
||||
|
||||
They are used like this
|
||||
They are used like this
|
||||
|
||||
```handlebars
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="nav-chapters previous">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
```
|
||||
```handlebars
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="nav-chapters previous">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
```
|
||||
|
||||
The inner html will only be rendered if the previous / next chapter exists.
|
||||
Of course the inner html can be changed to your liking.
|
||||
The inner html will only be rendered if the previous / next chapter exists.
|
||||
Of course the inner html can be changed to your liking.
|
||||
|
||||
------
|
||||
|
||||
*If you would like other properties or helpers exposed, please [create a new
|
||||
issue](https://github.com/rust-lang-nursery/mdBook/issues)*
|
||||
issue](https://github.com/rust-lang/mdBook/issues)*
|
||||
|
||||
@@ -22,7 +22,7 @@ overridden with your own.
|
||||
|
||||
If you want to use another theme for `highlight.js` download it from their
|
||||
website, or make it yourself, rename it to `highlight.css` and put it in
|
||||
`src/theme` (or the equivalent if you changed your source folder)
|
||||
the `theme` folder of your book.
|
||||
|
||||
Now your theme will be used instead of the default theme.
|
||||
|
||||
@@ -62,7 +62,7 @@ everyone can benefit from it.**
|
||||
|
||||
If you think the default theme doesn't look quite right for a specific language,
|
||||
or could be improved. Feel free to [submit a new
|
||||
issue](https://github.com/rust-lang-nursery/mdBook/issues) explaining what you
|
||||
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.
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# This script takes care of building your crate and packaging it for release
|
||||
|
||||
set -ex
|
||||
|
||||
main() {
|
||||
local src=$(pwd) \
|
||||
stage=
|
||||
|
||||
case $TRAVIS_OS_NAME in
|
||||
linux)
|
||||
stage=$(mktemp -d)
|
||||
;;
|
||||
osx)
|
||||
stage=$(mktemp -d -t tmp)
|
||||
;;
|
||||
esac
|
||||
|
||||
# This will slow down the build, but is necessary to not run out of disk space
|
||||
cargo clean
|
||||
|
||||
cargo rustc --bin mdbook --target $TARGET --release -- -C lto
|
||||
|
||||
cp target/$TARGET/release/mdbook $stage/
|
||||
|
||||
cd $stage
|
||||
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
|
||||
cd $src
|
||||
|
||||
rm -rf $stage
|
||||
}
|
||||
|
||||
main
|
||||
24
ci/install-hub.sh
Executable file
24
ci/install-hub.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installs the `hub` executable into hub/bin
|
||||
set -ex
|
||||
case $1 in
|
||||
ubuntu*)
|
||||
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-linux-amd64-2.12.8.tgz -o hub.tgz
|
||||
mkdir hub
|
||||
tar -xzvf hub.tgz --strip=1 -C hub
|
||||
;;
|
||||
macos*)
|
||||
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-darwin-amd64-2.12.8.tgz -o hub.tgz
|
||||
mkdir hub
|
||||
tar -xzvf hub.tgz --strip=1 -C hub
|
||||
;;
|
||||
windows*)
|
||||
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-windows-amd64-2.12.8.zip -o hub.zip
|
||||
7z x hub.zip -ohub
|
||||
;;
|
||||
*)
|
||||
echo "OS should be first parameter, was: $1"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "##[add-path]$PWD/hub/bin"
|
||||
19
ci/install-rust.sh
Executable file
19
ci/install-rust.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install/update rust.
|
||||
# The first argument should be the toolchain to install.
|
||||
|
||||
set -ex
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "First parameter must be toolchain to install."
|
||||
exit 1
|
||||
fi
|
||||
TOOLCHAIN="$1"
|
||||
|
||||
rustup set profile minimal
|
||||
rustup component remove --toolchain=$TOOLCHAIN rust-docs || echo "already removed"
|
||||
rustup update $TOOLCHAIN
|
||||
rustup default $TOOLCHAIN
|
||||
rustup -V
|
||||
rustc -Vv
|
||||
cargo -V
|
||||
36
ci/make-release.sh
Executable file
36
ci/make-release.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the release and creates an archive and optionally deploys to GitHub.
|
||||
set -ex
|
||||
|
||||
if [[ -z "$GITHUB_REF" ]]
|
||||
then
|
||||
echo "GITHUB_REF must be set"
|
||||
exit 1
|
||||
fi
|
||||
# Strip mdbook-refs/tags/ from the start of the ref.
|
||||
TAG=${GITHUB_REF#*/tags/}
|
||||
|
||||
host=$(rustc -Vv | grep ^host: | sed -e "s/host: //g")
|
||||
cargo rustc --bin mdbook --release -- -C lto
|
||||
cd target/release
|
||||
case $1 in
|
||||
ubuntu* | macos*)
|
||||
asset="mdbook-$TAG-$host.tar.gz"
|
||||
tar czf ../../$asset mdbook
|
||||
;;
|
||||
windows*)
|
||||
asset="mdbook-$TAG-$host.zip"
|
||||
7z a ../../$asset mdbook.exe
|
||||
;;
|
||||
*)
|
||||
echo "OS should be first parameter, was: $1"
|
||||
;;
|
||||
esac
|
||||
cd ../..
|
||||
|
||||
if [[ -z "$GITHUB_TOKEN" ]]
|
||||
then
|
||||
echo "$GITHUB_TOKEN not set, skipping deploy."
|
||||
else
|
||||
hub release edit -m "" --attach $asset $TAG
|
||||
fi
|
||||
@@ -1,82 +0,0 @@
|
||||
//! An example preprocessor for removing all forms of emphasis from a markdown
|
||||
//! book.
|
||||
|
||||
extern crate mdbook;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate pulldown_cmark_to_cmark;
|
||||
|
||||
use mdbook::book::{Book, BookItem, Chapter};
|
||||
use mdbook::errors::{Error, Result};
|
||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
||||
use pulldown_cmark::{Event, Parser, Tag};
|
||||
use pulldown_cmark_to_cmark::fmt::cmark;
|
||||
|
||||
const NAME: &str = "md-links-to-html-links";
|
||||
|
||||
fn main() {
|
||||
panic!("This example is intended to be part of a library");
|
||||
}
|
||||
|
||||
struct Deemphasize;
|
||||
|
||||
impl Preprocessor for Deemphasize {
|
||||
fn name(&self) -> &str {
|
||||
NAME
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
||||
eprintln!("Running '{}' preprocessor", self.name());
|
||||
let mut num_removed_items = 0;
|
||||
|
||||
process(&mut book.sections, &mut num_removed_items)?;
|
||||
|
||||
eprintln!(
|
||||
"{}: removed {} events from markdown stream.",
|
||||
self.name(),
|
||||
num_removed_items
|
||||
);
|
||||
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
fn process<'a, I>(items: I, num_removed_items: &mut usize) -> Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = &'a mut BookItem> + 'a,
|
||||
{
|
||||
for item in items {
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
eprintln!("{}: processing chapter '{}'", NAME, chapter.name);
|
||||
|
||||
let md = remove_emphasis(num_removed_items, chapter)?;
|
||||
chapter.content = md;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_emphasis(
|
||||
num_removed_items: &mut usize,
|
||||
chapter: &mut Chapter,
|
||||
) -> Result<String> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
|
||||
let events = Parser::new(&chapter.content).filter(|e| {
|
||||
let should_keep = match *e {
|
||||
Event::Start(Tag::Emphasis)
|
||||
| Event::Start(Tag::Strong)
|
||||
| Event::End(Tag::Emphasis)
|
||||
| Event::End(Tag::Strong) => false,
|
||||
_ => true,
|
||||
};
|
||||
if !should_keep {
|
||||
*num_removed_items += 1;
|
||||
}
|
||||
should_keep
|
||||
});
|
||||
|
||||
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
|
||||
Error::from(format!("Markdown serialization failed: {}", err))
|
||||
})
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
extern crate clap;
|
||||
extern crate mdbook;
|
||||
extern crate serde_json;
|
||||
|
||||
use crate::nop_lib::Nop;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use mdbook::book::Book;
|
||||
use mdbook::errors::Error;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use std::io;
|
||||
use std::process;
|
||||
use nop_lib::Nop;
|
||||
|
||||
pub fn make_app() -> App<'static, 'static> {
|
||||
App::new("nop-preprocessor")
|
||||
@@ -16,7 +12,8 @@ pub fn make_app() -> App<'static, 'static> {
|
||||
.subcommand(
|
||||
SubCommand::with_name("supports")
|
||||
.arg(Arg::with_name("renderer").required(true))
|
||||
.about("Check whether a renderer is supported by this preprocessor"))
|
||||
.about("Check whether a renderer is supported by this preprocessor"),
|
||||
)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -27,11 +24,9 @@ fn main() {
|
||||
|
||||
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
||||
handle_supports(&preprocessor, sub_args);
|
||||
} else {
|
||||
if let Err(e) = handle_preprocessing(&preprocessor) {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
} else if let Err(e) = handle_preprocessing(&preprocessor) {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,16 +82,12 @@ mod nop_lib {
|
||||
"nop-preprocessor"
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
ctx: &PreprocessorContext,
|
||||
book: Book,
|
||||
) -> Result<Book, Error> {
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book, Error> {
|
||||
// In testing we want to tell the preprocessor to blow up by setting a
|
||||
// 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!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,4 +100,3 @@ mod nop_lib {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
139
src/book/book.rs
139
src/book/book.rs
@@ -5,8 +5,8 @@ use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
use config::BuildConfig;
|
||||
use errors::*;
|
||||
use crate::config::BuildConfig;
|
||||
use crate::errors::*;
|
||||
|
||||
/// Load a book into memory from its `src/` directory.
|
||||
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
|
||||
@@ -15,13 +15,13 @@ pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book>
|
||||
|
||||
let mut summary_content = String::new();
|
||||
File::open(summary_md)
|
||||
.chain_err(|| "Couldn't open SUMMARY.md")?
|
||||
.with_context(|| "Couldn't open SUMMARY.md")?
|
||||
.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(|| "Summary parsing failed")?;
|
||||
|
||||
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 +39,19 @@ 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)?;
|
||||
writeln!(f, "# {}", link.name)?;
|
||||
}
|
||||
}
|
||||
|
||||
items.extend(&link.nested_items);
|
||||
@@ -82,7 +84,7 @@ impl Book {
|
||||
}
|
||||
|
||||
/// Get a depth-first iterator over the items in the book.
|
||||
pub fn iter(&self) -> BookItems {
|
||||
pub fn iter(&self) -> BookItems<'_> {
|
||||
BookItems {
|
||||
items: self.sections.iter().collect(),
|
||||
}
|
||||
@@ -116,7 +118,7 @@ where
|
||||
I: IntoIterator<Item = &'a mut BookItem>,
|
||||
{
|
||||
for item in items {
|
||||
if let &mut BookItem::Chapter(ref mut ch) = item {
|
||||
if let BookItem::Chapter(ch) = item {
|
||||
for_each_mut(func, &mut ch.sub_items);
|
||||
}
|
||||
|
||||
@@ -131,6 +133,8 @@ pub enum BookItem {
|
||||
Chapter(Chapter),
|
||||
/// A section separator.
|
||||
Separator,
|
||||
/// A part title.
|
||||
PartTitle(String),
|
||||
}
|
||||
|
||||
impl From<Chapter> for BookItem {
|
||||
@@ -152,7 +156,7 @@ pub struct Chapter {
|
||||
/// Nested items.
|
||||
pub sub_items: Vec<BookItem>,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
pub path: PathBuf,
|
||||
pub path: Option<PathBuf>,
|
||||
/// An ordered list of the names of each chapter above this one, in the hierarchy.
|
||||
pub parent_names: Vec<String>,
|
||||
}
|
||||
@@ -167,19 +171,39 @@ impl Chapter {
|
||||
) -> Chapter {
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content: content,
|
||||
path: path.into(),
|
||||
parent_names: parent_names,
|
||||
content,
|
||||
path: Some(path.into()),
|
||||
parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new draft chapter that is not attached to a source markdown file and has
|
||||
/// thus no content.
|
||||
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content: String::new(),
|
||||
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 {
|
||||
match self.path {
|
||||
Some(_) => false,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the provided `Summary` to load a `Book` from disk.
|
||||
///
|
||||
/// You need to pass in the book's source directory because all the links in
|
||||
/// `SUMMARY.md` give the chapter locations relative to it.
|
||||
fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
|
||||
pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
|
||||
debug!("Loading the book from disk");
|
||||
let src_dir = src_dir.as_ref();
|
||||
|
||||
@@ -202,16 +226,17 @@ fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<
|
||||
})
|
||||
}
|
||||
|
||||
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(|c| BookItem::Chapter(c))
|
||||
load_chapter(link, src_dir, parent_names).map(BookItem::Chapter)
|
||||
}
|
||||
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,28 +245,36 @@ 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())
|
||||
})?;
|
||||
|
||||
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 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());
|
||||
@@ -286,7 +319,7 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
}
|
||||
|
||||
impl Display for Chapter {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if let Some(ref section_number) = self.number {
|
||||
write!(f, "{} ", section_number)?;
|
||||
}
|
||||
@@ -301,7 +334,7 @@ mod tests {
|
||||
use std::io::Write;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
const DUMMY_SRC: &'static str = "
|
||||
const DUMMY_SRC: &str = "
|
||||
# Dummy Chapter
|
||||
|
||||
this is some dummy text.
|
||||
@@ -317,7 +350,7 @@ And here is some \
|
||||
let chapter_path = temp.path().join("chapter_1.md");
|
||||
File::create(&chapter_path)
|
||||
.unwrap()
|
||||
.write(DUMMY_SRC.as_bytes())
|
||||
.write_all(DUMMY_SRC.as_bytes())
|
||||
.unwrap();
|
||||
|
||||
let link = Link::new("Chapter 1", chapter_path);
|
||||
@@ -333,7 +366,7 @@ And here is some \
|
||||
|
||||
File::create(&second_path)
|
||||
.unwrap()
|
||||
.write_all("Hello World!".as_bytes())
|
||||
.write_all(b"Hello World!")
|
||||
.unwrap();
|
||||
|
||||
let mut second = Link::new("Nested Chapter 1", &second_path);
|
||||
@@ -376,7 +409,7 @@ 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")),
|
||||
parent_names: vec![String::from("Chapter 1")],
|
||||
sub_items: Vec::new(),
|
||||
};
|
||||
@@ -384,7 +417,7 @@ 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")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(nested.clone()),
|
||||
@@ -408,7 +441,7 @@ 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")),
|
||||
..Default::default()
|
||||
})],
|
||||
..Default::default()
|
||||
@@ -448,7 +481,7 @@ 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")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
@@ -481,7 +514,8 @@ And here is some \
|
||||
.filter_map(|i| match *i {
|
||||
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
|
||||
_ => None,
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
let should_be: Vec<_> = vec![
|
||||
String::from("Chapter 1"),
|
||||
String::from("Hello World"),
|
||||
@@ -499,7 +533,7 @@ 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")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
@@ -536,9 +570,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()
|
||||
};
|
||||
|
||||
@@ -555,7 +590,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()
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use toml;
|
||||
|
||||
use super::MDBook;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
use theme;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use crate::theme;
|
||||
|
||||
/// A helper for setting up a new book and its directory structure.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -65,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()?;
|
||||
@@ -98,12 +97,12 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -144,7 +143,10 @@ impl BookBuilder {
|
||||
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)?;
|
||||
@@ -173,15 +175,20 @@ impl BookBuilder {
|
||||
let src_dir = self.root.join(&self.config.book.src);
|
||||
|
||||
let summary = src_dir.join("SUMMARY.md");
|
||||
let mut f = File::create(&summary).chain_err(|| "Unable to create SUMMARY.md")?;
|
||||
writeln!(f, "# Summary")?;
|
||||
writeln!(f, "")?;
|
||||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||
|
||||
let chapter_1 = src_dir.join("chapter_1.md");
|
||||
let mut f = File::create(&chapter_1).chain_err(|| "Unable to create chapter_1.md")?;
|
||||
writeln!(f, "# Chapter 1")?;
|
||||
if !summary.exists() {
|
||||
trace!("No summary found creating stub summary and chapter_1.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).with_context(|| "Unable to create chapter_1.md")?;
|
||||
writeln!(f, "# Chapter 1")?;
|
||||
} else {
|
||||
trace!("Existing summary found, no need to create stub files.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
226
src/book/mod.rs
226
src/book/mod.rs
@@ -5,6 +5,7 @@
|
||||
//!
|
||||
//! [1]: ../index.html
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod book;
|
||||
mod init;
|
||||
mod summary;
|
||||
@@ -16,16 +17,18 @@ pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::string::ToString;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use toml::Value;
|
||||
|
||||
use errors::*;
|
||||
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor,
|
||||
PreprocessorContext, CmdPreprocessor};
|
||||
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
||||
use utils;
|
||||
use crate::errors::*;
|
||||
use crate::preprocess::{
|
||||
CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
|
||||
};
|
||||
use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
|
||||
use crate::utils;
|
||||
|
||||
use config::Config;
|
||||
use crate::config::{Config, RustEdition};
|
||||
|
||||
/// The object used to manage and build a book.
|
||||
pub struct MDBook {
|
||||
@@ -35,10 +38,10 @@ pub struct MDBook {
|
||||
pub config: Config,
|
||||
/// A representation of the book's contents in memory.
|
||||
pub book: Book,
|
||||
renderers: Vec<Box<Renderer>>,
|
||||
renderers: Vec<Box<dyn Renderer>>,
|
||||
|
||||
/// List of pre-processors to be run on the book
|
||||
preprocessors: Vec<Box<Preprocessor>>,
|
||||
preprocessors: Vec<Box<dyn Preprocessor>>,
|
||||
}
|
||||
|
||||
impl MDBook {
|
||||
@@ -54,7 +57,7 @@ impl MDBook {
|
||||
warn!("This format is no longer used, so you should migrate to the");
|
||||
warn!("book.toml format.");
|
||||
warn!("Check the user guide for migration information:");
|
||||
warn!("\thttps://rust-lang-nursery.github.io/mdBook/format/config.html");
|
||||
warn!("\thttps://rust-lang.github.io/mdBook/format/config.html");
|
||||
}
|
||||
|
||||
let mut config = if config_location.exists() {
|
||||
@@ -66,7 +69,7 @@ impl MDBook {
|
||||
|
||||
config.update_from_env();
|
||||
|
||||
if log_enabled!(::log::Level::Trace) {
|
||||
if log_enabled!(log::Level::Trace) {
|
||||
for line in format!("Config: {:#?}", config).lines() {
|
||||
trace!("{}", line);
|
||||
}
|
||||
@@ -94,21 +97,42 @@ impl MDBook {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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,
|
||||
summary: Summary,
|
||||
) -> Result<MDBook> {
|
||||
let root = book_root.into();
|
||||
|
||||
let src_dir = root.join(&config.book.src);
|
||||
let book = book::load_book_from_disk(&summary, &src_dir)?;
|
||||
|
||||
let renderers = determine_renderers(&config);
|
||||
let preprocessors = determine_preprocessors(&config)?;
|
||||
|
||||
Ok(MDBook {
|
||||
root,
|
||||
config,
|
||||
book,
|
||||
renderers,
|
||||
preprocessors,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a flat depth-first iterator over the elements of the book,
|
||||
/// it returns an [BookItem enum](bookitem.html):
|
||||
/// `(section: String, bookitem: &BookItem)`
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate mdbook;
|
||||
/// # use mdbook::MDBook;
|
||||
/// # use mdbook::book::BookItem;
|
||||
/// # #[allow(unused_variables)]
|
||||
/// # fn main() {
|
||||
/// # let book = MDBook::load("mybook").unwrap();
|
||||
/// for item in book.iter() {
|
||||
/// match *item {
|
||||
/// BookItem::Chapter(ref chapter) => {},
|
||||
/// BookItem::Separator => {},
|
||||
/// BookItem::PartTitle(ref title) => {}
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
@@ -119,9 +143,8 @@ impl MDBook {
|
||||
/// // 2. Chapter 2
|
||||
/// //
|
||||
/// // etc.
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn iter(&self) -> BookItems {
|
||||
pub fn iter(&self) -> BookItems<'_> {
|
||||
self.book.iter()
|
||||
}
|
||||
|
||||
@@ -158,17 +181,18 @@ impl MDBook {
|
||||
}
|
||||
|
||||
/// Run the entire build process for a particular `Renderer`.
|
||||
fn execute_build_process(&self, renderer: &Renderer) -> Result<()> {
|
||||
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
||||
let mut preprocessed_book = self.book.clone();
|
||||
let preprocess_ctx = PreprocessorContext::new(self.root.clone(),
|
||||
self.config.clone(),
|
||||
renderer.name().to_string());
|
||||
let preprocess_ctx = PreprocessorContext::new(
|
||||
self.root.clone(),
|
||||
self.config.clone(),
|
||||
renderer.name().to_string(),
|
||||
);
|
||||
|
||||
for preprocessor in &self.preprocessors {
|
||||
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
|
||||
debug!("Running the {} preprocessor.", preprocessor.name());
|
||||
preprocessed_book =
|
||||
preprocessor.run(&preprocess_ctx, preprocessed_book)?;
|
||||
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,23 +202,9 @@ impl MDBook {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
preprocessed_book: &Book,
|
||||
renderer: &Renderer,
|
||||
) -> Result<()> {
|
||||
fn render(&self, preprocessed_book: &Book, renderer: &dyn Renderer) -> Result<()> {
|
||||
let name = renderer.name();
|
||||
let build_dir = self.build_dir_for(name);
|
||||
if build_dir.exists() {
|
||||
debug!(
|
||||
"Cleaning build dir for the \"{}\" renderer ({})",
|
||||
name,
|
||||
build_dir.display()
|
||||
);
|
||||
|
||||
utils::fs::remove_dir_content(&build_dir)
|
||||
.chain_err(|| "Unable to clear output directory")?;
|
||||
}
|
||||
|
||||
let render_context = RenderContext::new(
|
||||
self.root.clone(),
|
||||
@@ -205,7 +215,7 @@ impl MDBook {
|
||||
|
||||
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.
|
||||
@@ -217,7 +227,7 @@ impl MDBook {
|
||||
}
|
||||
|
||||
/// Register a [`Preprocessor`](../preprocess/trait.Preprocessor.html) to be used when rendering the book.
|
||||
pub fn with_preprecessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
||||
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
||||
self.preprocessors.push(Box::new(preprocessor));
|
||||
self
|
||||
}
|
||||
@@ -233,9 +243,8 @@ impl MDBook {
|
||||
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
||||
|
||||
// FIXME: Is "test" the proper renderer name to use here?
|
||||
let preprocess_context = PreprocessorContext::new(self.root.clone(),
|
||||
self.config.clone(),
|
||||
"test".to_string());
|
||||
let preprocess_context =
|
||||
PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
|
||||
|
||||
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
|
||||
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
||||
@@ -243,29 +252,43 @@ impl MDBook {
|
||||
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
if !ch.path.as_os_str().is_empty() {
|
||||
let path = self.source_dir().join(&ch.path);
|
||||
let content = utils::fs::file_to_string(&path)?;
|
||||
info!("Testing file: {:?}", path);
|
||||
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(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"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"rustdoc returned an error:\n\
|
||||
\n--- stdout\n{}\n--- stderr\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -320,19 +343,19 @@ impl MDBook {
|
||||
}
|
||||
|
||||
/// Look at the `Config` and try to figure out what renderers to use.
|
||||
fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
|
||||
let mut renderers: Vec<Box<Renderer>> = Vec::new();
|
||||
fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
|
||||
let mut renderers = Vec::new();
|
||||
|
||||
if let Some(output_table) = config.get("output").and_then(|o| o.as_table()) {
|
||||
for (key, table) in output_table.iter() {
|
||||
// the "html" backend has its own Renderer
|
||||
if let Some(output_table) = config.get("output").and_then(Value::as_table) {
|
||||
renderers.extend(output_table.iter().map(|(key, table)| {
|
||||
if key == "html" {
|
||||
renderers.push(Box::new(HtmlHandlebars::new()));
|
||||
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
|
||||
} else if key == "markdown" {
|
||||
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
|
||||
} else {
|
||||
let renderer = interpret_custom_renderer(key, table);
|
||||
renderers.push(renderer);
|
||||
interpret_custom_renderer(key, table)
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// if we couldn't find anything, add the HTML renderer as a default
|
||||
@@ -343,37 +366,31 @@ fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
|
||||
renderers
|
||||
}
|
||||
|
||||
fn default_preprocessors() -> Vec<Box<Preprocessor>> {
|
||||
fn default_preprocessors() -> Vec<Box<dyn Preprocessor>> {
|
||||
vec![
|
||||
Box::new(LinkPreprocessor::new()),
|
||||
Box::new(IndexPreprocessor::new()),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_default_preprocessor(pre: &Preprocessor) -> bool {
|
||||
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
|
||||
let name = pre.name();
|
||||
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
|
||||
}
|
||||
|
||||
/// Look at the `MDBook` and try to figure out what preprocessors to run.
|
||||
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
|
||||
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
|
||||
let mut preprocessors = Vec::new();
|
||||
|
||||
if config.build.use_default_preprocessors {
|
||||
preprocessors.extend(default_preprocessors());
|
||||
}
|
||||
|
||||
if let Some(preprocessor_table) =
|
||||
config.get("preprocessor").and_then(|v| v.as_table())
|
||||
{
|
||||
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()))
|
||||
}
|
||||
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
|
||||
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
|
||||
name => preprocessors.push(interpret_custom_preprocessor(
|
||||
name,
|
||||
&preprocessor_table[name],
|
||||
@@ -385,17 +402,14 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
|
||||
Ok(preprocessors)
|
||||
}
|
||||
|
||||
fn interpret_custom_preprocessor(
|
||||
key: &str,
|
||||
table: &Value,
|
||||
) -> Box<CmdPreprocessor> {
|
||||
fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocessor> {
|
||||
let command = table
|
||||
.get("command")
|
||||
.and_then(|c| c.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.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()))
|
||||
Box::new(CmdPreprocessor::new(key.to_string(), command))
|
||||
}
|
||||
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
@@ -403,13 +417,12 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
// prepended by "mdbook-"
|
||||
let table_dot_command = table
|
||||
.get("command")
|
||||
.and_then(|c| c.as_str())
|
||||
.map(|s| s.to_string());
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
|
||||
let command =
|
||||
table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
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
|
||||
@@ -418,7 +431,11 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
///
|
||||
/// The `build.use-default-preprocessors` config option can be used to ensure
|
||||
/// default preprocessors always run if they support the renderer.
|
||||
fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg: &Config) -> bool {
|
||||
fn preprocessor_should_run(
|
||||
preprocessor: &dyn Preprocessor,
|
||||
renderer: &dyn Renderer,
|
||||
cfg: &Config,
|
||||
) -> bool {
|
||||
// default preprocessors should be run by default (if supported)
|
||||
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
|
||||
return preprocessor.supports_renderer(renderer.name());
|
||||
@@ -428,18 +445,19 @@ fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg
|
||||
let renderer_name = renderer.name();
|
||||
|
||||
if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
|
||||
return explicit_renderers.into_iter()
|
||||
.filter_map(|val| val.as_str())
|
||||
return explicit_renderers
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.any(|name| name == renderer_name);
|
||||
}
|
||||
|
||||
preprocessor.supports_renderer(renderer_name)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use toml::value::{Table, Value};
|
||||
|
||||
#[test]
|
||||
@@ -507,7 +525,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn can_determine_third_party_preprocessors() {
|
||||
let cfg_str: &'static str = r#"
|
||||
let cfg_str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
|
||||
@@ -539,17 +557,14 @@ 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 = interpret_custom_preprocessor("random", &Value::Table(random.clone()));
|
||||
|
||||
assert_eq!(random.cmd(), "python random.py");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_respects_preprocessor_selection() {
|
||||
let cfg_str: &'static str = r#"
|
||||
let cfg_str = r#"
|
||||
[preprocessor.links]
|
||||
renderers = ["html"]
|
||||
"#;
|
||||
@@ -557,11 +572,12 @@ mod tests {
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// double-check that we can access preprocessor.links.renderers[0]
|
||||
let html = cfg.get_preprocessor("links")
|
||||
let html = cfg
|
||||
.get_preprocessor("links")
|
||||
.and_then(|links| links.get("renderers"))
|
||||
.and_then(|renderers| renderers.as_array())
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|renderers| renderers.get(0))
|
||||
.and_then(|renderer| renderer.as_str())
|
||||
.and_then(Value::as_str)
|
||||
.unwrap();
|
||||
assert_eq!(html, "html");
|
||||
let html_renderer = HtmlHandlebars::default();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use errors::*;
|
||||
use crate::errors::*;
|
||||
use memchr::{self, Memchr};
|
||||
use pulldown_cmark::{self, Event, Tag};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
@@ -25,12 +25,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 +60,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 +76,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 +88,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 +99,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 +113,8 @@ pub enum SummaryItem {
|
||||
Link(Link),
|
||||
/// A separator (`---`).
|
||||
Separator,
|
||||
/// A part title.
|
||||
PartTitle(String),
|
||||
}
|
||||
|
||||
impl SummaryItem {
|
||||
@@ -134,12 +141,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,7 +161,12 @@ impl From<Link> for SummaryItem {
|
||||
/// > match the following regex: "[^<>\n[]]+".
|
||||
struct SummaryParser<'a> {
|
||||
src: &'a str,
|
||||
stream: pulldown_cmark::Parser<'a>,
|
||||
stream: pulldown_cmark::OffsetIter<'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
|
||||
@@ -174,7 +187,7 @@ macro_rules! collect_events {
|
||||
let mut events = Vec::new();
|
||||
|
||||
loop {
|
||||
let event = $stream.next();
|
||||
let event = $stream.next().map(|(ev, _range)| ev);
|
||||
trace!("Next event: {:?}", event);
|
||||
|
||||
match event {
|
||||
@@ -195,24 +208,24 @@ macro_rules! collect_events {
|
||||
}
|
||||
|
||||
impl<'a> SummaryParser<'a> {
|
||||
fn new(text: &str) -> SummaryParser {
|
||||
let pulldown_parser = pulldown_cmark::Parser::new(text);
|
||||
fn new(text: &str) -> SummaryParser<'_> {
|
||||
let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter();
|
||||
|
||||
SummaryParser {
|
||||
src: text,
|
||||
stream: pulldown_parser,
|
||||
offset: 0,
|
||||
back: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current line and column to give the user more useful error
|
||||
/// messages.
|
||||
fn current_location(&self) -> (usize, usize) {
|
||||
let byte_offset = self.stream.get_offset();
|
||||
|
||||
let previous_text = self.src[..byte_offset].as_bytes();
|
||||
let previous_text = self.src[..self.offset].as_bytes();
|
||||
let line = Memchr::new(b'\n', previous_text).count() + 1;
|
||||
let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0);
|
||||
let col = self.src[start_of_line..byte_offset].chars().count();
|
||||
let col = self.src[start_of_line..self.offset].chars().count();
|
||||
|
||||
(line, col)
|
||||
}
|
||||
@@ -223,13 +236,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 +252,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,20 +262,22 @@ 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(1))) => {
|
||||
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(href, _))) => {
|
||||
let link = self.parse_link(href.to_string())?;
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => {
|
||||
let link = self.parse_link(href.to_string());
|
||||
items.push(SummaryItem::Link(link));
|
||||
}
|
||||
Some(Event::Start(Tag::Rule)) => items.push(SummaryItem::Separator),
|
||||
Some(Event::Rule) => items.push(SummaryItem::Separator),
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
@@ -272,57 +286,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(1))) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(1));
|
||||
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 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: 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(1))) => {
|
||||
// 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)) => {
|
||||
if other_tag == Tag::Rule {
|
||||
items.push(SummaryItem::Separator);
|
||||
}
|
||||
trace!("Skipping contents of {:?}", other_tag);
|
||||
|
||||
// Skip over the contents of this tag
|
||||
@@ -331,29 +399,42 @@ impl<'a> SummaryParser<'a> {
|
||||
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();
|
||||
let next = self.back.take().or_else(|| {
|
||||
self.stream.next().map(|(ev, range)| {
|
||||
self.offset = range.start;
|
||||
ev
|
||||
})
|
||||
});
|
||||
|
||||
trace!("Next event: {:?}", next);
|
||||
|
||||
next
|
||||
@@ -370,6 +451,10 @@ impl<'a> SummaryParser<'a> {
|
||||
items.push(item);
|
||||
}
|
||||
Some(Event::Start(Tag::List(..))) => {
|
||||
// Skip this tag after comment bacause it is not nested.
|
||||
if items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// recurse to parse the nested list
|
||||
let (_, last_item) = get_last_link(&mut items)?;
|
||||
let last_item_number = last_item
|
||||
@@ -398,8 +483,8 @@ impl<'a> SummaryParser<'a> {
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Paragraph)) => continue,
|
||||
Some(Event::Start(Tag::Link(href, _))) => {
|
||||
let mut link = self.parse_link(href.to_string())?;
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => {
|
||||
let mut link = self.parse_link(href.to_string());
|
||||
|
||||
let mut number = parent.clone();
|
||||
number.0.push(num_existing_items as u32 + 1);
|
||||
@@ -407,7 +492,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);
|
||||
@@ -426,19 +514,24 @@ 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::Header(1))) = self.next_event() {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Heading(1))) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Header(1));
|
||||
Some(stringify_events(tags))
|
||||
} else {
|
||||
None
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(1));
|
||||
Some(stringify_events(tags))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -464,21 +557,21 @@ 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
|
||||
/// plain text.
|
||||
fn stringify_events(events: Vec<Event>) -> String {
|
||||
fn stringify_events(events: Vec<Event<'_>>) -> String {
|
||||
events
|
||||
.into_iter()
|
||||
.filter_map(|t| match t {
|
||||
Event::Text(text) => Some(text.into_owned()),
|
||||
Event::Text(text) | Event::Code(text) => Some(text.into_string()),
|
||||
_ => None,
|
||||
}).collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
|
||||
@@ -487,7 +580,7 @@ fn stringify_events(events: Vec<Event>) -> String {
|
||||
pub struct SectionNumber(pub Vec<u32>);
|
||||
|
||||
impl Display for SectionNumber {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if self.0.is_empty() {
|
||||
write!(f, "0")
|
||||
} else {
|
||||
@@ -577,17 +670,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);
|
||||
@@ -598,7 +690,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);
|
||||
@@ -610,7 +701,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());
|
||||
@@ -621,19 +711,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(href, _))) => href.to_string(),
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -642,16 +732,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);
|
||||
}
|
||||
@@ -663,27 +753,92 @@ 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(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
|
||||
let got = parser.parse_numbered().unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_numbered_chapters_separated_by_comment() {
|
||||
let src = "- [First](./first.md)\n<!-- this is a comment -->\n- [Second](./second.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(),
|
||||
}),
|
||||
];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.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);
|
||||
}
|
||||
@@ -698,68 +853,76 @@ 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-nursery/mdBook/issues/779
|
||||
/// Regression test for https://github.com/rust-lang/mdBook/issues/779
|
||||
/// Ensure section numbers are correctly incremented after a horizontal separator.
|
||||
#[test]
|
||||
fn keep_numbering_after_separator() {
|
||||
let src = "- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
|
||||
let src =
|
||||
"- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
use {get_book_dir, open};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
@@ -9,11 +9,14 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Builds a book from its markdown files")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
).arg_from_usage(
|
||||
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_from_usage("-o, --open 'Opens the compiled book in a web browser'")
|
||||
}
|
||||
|
||||
// Build command implementation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::get_book_dir;
|
||||
use anyhow::Context;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::MDBook;
|
||||
use std::fs;
|
||||
|
||||
@@ -10,15 +10,18 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Deletes a built book")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
).arg_from_usage(
|
||||
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)'",
|
||||
)
|
||||
}
|
||||
|
||||
// Clean command implementation
|
||||
pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
|
||||
pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::load(&book_dir)?;
|
||||
|
||||
@@ -26,7 +29,11 @@ pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
|
||||
Some(dest_dir) => dest_dir.into(),
|
||||
None => book.root.join(&book.config.build.build_dir),
|
||||
};
|
||||
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;
|
||||
|
||||
if dir_to_remove.exists() {
|
||||
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 get_book_dir;
|
||||
use mdbook::config;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
@@ -12,8 +12,10 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("init")
|
||||
.about("Creates the boilerplate structure and files for a new book")
|
||||
// the {n} denotes a newline which will properly aligned in all help messages
|
||||
.arg_from_usage("[dir] 'Directory to create the book in{n}\
|
||||
(Defaults to the Current Directory when omitted)'")
|
||||
.arg_from_usage(
|
||||
"[dir] 'Directory to create the book in{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
||||
.arg_from_usage("--force 'Skips confirmation prompts'")
|
||||
}
|
||||
|
||||
156
src/cmd/serve.rs
156
src/cmd/serve.rs
@@ -1,20 +1,21 @@
|
||||
extern crate iron;
|
||||
extern crate staticfile;
|
||||
extern crate ws;
|
||||
|
||||
use self::iron::{
|
||||
status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set,
|
||||
};
|
||||
#[cfg(feature = "watch")]
|
||||
use super::watch;
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
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;
|
||||
use {get_book_dir, open};
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::broadcast;
|
||||
use warp::ws::Message;
|
||||
use warp::Filter;
|
||||
|
||||
struct ErrorRecover;
|
||||
/// 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> {
|
||||
@@ -22,7 +23,8 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Serves a book at http://localhost:3000, and rebuilds it on changes")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
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}\
|
||||
@@ -46,64 +48,53 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.empty_values(false)
|
||||
.help("Port to use for HTTP connections"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("websocket-hostname")
|
||||
.long("websocket-hostname")
|
||||
.takes_value(true)
|
||||
.empty_values(false)
|
||||
.help(
|
||||
"Hostname to connect to for WebSockets connections (Defaults to the HTTP hostname)",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("websocket-port")
|
||||
.short("w")
|
||||
.long("websocket-port")
|
||||
.takes_value(true)
|
||||
.default_value("3001")
|
||||
.empty_values(false)
|
||||
.help("Port to use for WebSockets livereload connections"),
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Opens the book server in a web browser'")
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
// 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-address").unwrap_or(hostname);
|
||||
let open_browser = args.is_present("open");
|
||||
|
||||
let address = format!("{}:{}", hostname, port);
|
||||
let ws_address = format!("{}:{}", hostname, ws_port);
|
||||
|
||||
let livereload_url = format!("ws://{}:{}", public_address, ws_port);
|
||||
book.config
|
||||
.set("output.html.livereload-url", &livereload_url)?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
|
||||
let livereload_url = format!("ws://{}/{}", address, LIVE_RELOAD_ENDPOINT);
|
||||
let update_config = |book: &mut MDBook| {
|
||||
book.config
|
||||
.set("output.html.livereload-url", &livereload_url)
|
||||
.expect("livereload-url 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(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);
|
||||
@@ -114,36 +105,61 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
watch::trigger_on_change(&mut book, move |path, book_dir| {
|
||||
info!("File changed: {:?}", path);
|
||||
watch::trigger_on_change(&book, move |paths, book_dir| {
|
||||
info!("Files changed: {:?}", paths);
|
||||
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 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),
|
||||
}
|
||||
}
|
||||
#[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());
|
||||
|
||||
// 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);
|
||||
warp::serve(routes).run(address).await;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::get_book_dir;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
|
||||
@@ -9,7 +9,8 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Tests that a book's Rust code samples compile")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
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}\
|
||||
@@ -30,7 +31,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let library_paths: Vec<&str> = args
|
||||
.values_of("library-path")
|
||||
.map(|v| v.collect())
|
||||
.map(std::iter::Iterator::collect)
|
||||
.unwrap_or_default();
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
122
src/cmd/watch.rs
122
src/cmd/watch.rs
@@ -1,14 +1,13 @@
|
||||
extern crate notify;
|
||||
|
||||
use self::notify::Watcher;
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
use std::path::Path;
|
||||
use notify::Watcher;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use {get_book_dir, open};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
@@ -16,26 +15,39 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Watches a book's files and rebuilds it on changes")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
).arg_from_usage(
|
||||
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_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::load(&book_dir)?;
|
||||
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"));
|
||||
}
|
||||
|
||||
trigger_on_change(&book, |path, book_dir| {
|
||||
info!("File changed: {:?}\nBuilding book...\n", path);
|
||||
let result = MDBook::load(&book_dir).and_then(|b| b.build());
|
||||
trigger_on_change(&book, |paths, book_dir| {
|
||||
info!("Files changed: {:?}\nBuilding book...\n", paths);
|
||||
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");
|
||||
@@ -46,13 +58,60 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_ignored_files(book_root: &PathBuf, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
if paths.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
match find_gitignore(book_root) {
|
||||
Some(gitignore_path) => {
|
||||
match gitignore::File::new(gitignore_path.as_path()) {
|
||||
Ok(exclusion_checker) => filter_ignored_files(exclusion_checker, paths),
|
||||
Err(_) => {
|
||||
// We're unable to read the .gitignore file, so we'll silently allow everything.
|
||||
// Please see discussion: https://github.com/rust-lang/mdBook/pull/1051
|
||||
paths.iter().map(|path| path.to_path_buf()).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// There is no .gitignore file.
|
||||
paths.iter().map(|path| path.to_path_buf()).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_gitignore(book_root: &PathBuf) -> Option<PathBuf> {
|
||||
book_root
|
||||
.ancestors()
|
||||
.map(|p| p.join(".gitignore"))
|
||||
.find(|p| p.exists())
|
||||
}
|
||||
|
||||
fn filter_ignored_files(exclusion_checker: gitignore::File, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
paths
|
||||
.iter()
|
||||
.filter(|path| match exclusion_checker.is_excluded(path) {
|
||||
Ok(exclude) => !exclude,
|
||||
Err(error) => {
|
||||
warn!(
|
||||
"Unable to determine if {:?} is excluded: {:?}. Including it.",
|
||||
&path, error
|
||||
);
|
||||
true
|
||||
}
|
||||
})
|
||||
.map(|path| path.to_path_buf())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calls the closure when a book source file is changed, blocking indefinitely.
|
||||
pub fn trigger_on_change<F>(book: &MDBook, closure: F)
|
||||
where
|
||||
F: Fn(&Path, &Path),
|
||||
F: Fn(Vec<PathBuf>, &Path),
|
||||
{
|
||||
use self::notify::DebouncedEvent::*;
|
||||
use self::notify::RecursiveMode::*;
|
||||
use notify::DebouncedEvent::*;
|
||||
use notify::RecursiveMode::*;
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
@@ -61,14 +120,14 @@ where
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
error!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
::std::process::exit(1)
|
||||
std::process::exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.source_dir(), Recursive) {
|
||||
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
|
||||
::std::process::exit(1);
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let _ = watcher.watch(book.theme_dir(), Recursive);
|
||||
@@ -78,13 +137,28 @@ where
|
||||
|
||||
info!("Listening for changes...");
|
||||
|
||||
for event in rx.iter() {
|
||||
debug!("Received filesystem event: {:?}", event);
|
||||
match event {
|
||||
Create(path) | Write(path) | Remove(path) | Rename(_, path) => {
|
||||
closure(&path, &book.root);
|
||||
}
|
||||
_ => {}
|
||||
loop {
|
||||
let first_event = rx.recv().unwrap();
|
||||
sleep(Duration::from_millis(50));
|
||||
let other_events = rx.try_iter();
|
||||
|
||||
let all_events = std::iter::once(first_event).chain(other_events);
|
||||
|
||||
let paths = all_events
|
||||
.filter_map(|event| {
|
||||
debug!("Received filesystem event: {:?}", event);
|
||||
|
||||
match event {
|
||||
Create(path) | Write(path) | Remove(path) | Rename(_, path) => Some(path),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let paths = remove_ignored_files(&book.root, &paths[..]);
|
||||
|
||||
if !paths.is_empty() {
|
||||
closure(paths, &book.root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
436
src/config.rs
436
src/config.rs
@@ -3,16 +3,15 @@
|
||||
//! The main entrypoint of the `config` module is the `Config` struct. This acts
|
||||
//! essentially as a bag of configuration information, with a couple
|
||||
//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support
|
||||
//! for arbitrary data which is exposed to plugins and alternate backends.
|
||||
//! for arbitrary data which is exposed to plugins and alternative backends.
|
||||
//!
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! # extern crate mdbook;
|
||||
//! # use mdbook::errors::*;
|
||||
//! # extern crate toml;
|
||||
//! use std::path::PathBuf;
|
||||
//! use std::str::FromStr;
|
||||
//! use mdbook::Config;
|
||||
//! use toml::Value;
|
||||
//!
|
||||
@@ -41,28 +40,27 @@
|
||||
//! cfg.set("output.html.theme", "./themes");
|
||||
//!
|
||||
//! // then load it again, automatically deserializing to a `PathBuf`.
|
||||
//! let got: PathBuf = cfg.get_deserialized("output.html.theme")?;
|
||||
//! assert_eq!(got, PathBuf::from("./themes"));
|
||||
//! let got: Option<PathBuf> = cfg.get_deserialized_opt("output.html.theme")?;
|
||||
//! assert_eq!(got, Some(PathBuf::from("./themes")));
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # fn main() { run().unwrap() }
|
||||
//! # run().unwrap()
|
||||
//! ```
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
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 errors::*;
|
||||
use crate::errors::*;
|
||||
use crate::utils::{self, toml_ext::TomlExt};
|
||||
|
||||
/// The overall configuration object for MDBook, essentially an in-memory
|
||||
/// representation of `book.toml`.
|
||||
@@ -72,22 +70,28 @@ pub struct Config {
|
||||
pub book: BookConfig,
|
||||
/// Information about the build environment.
|
||||
pub build: BuildConfig,
|
||||
/// Information about Rust language support.
|
||||
pub rust: RustConfig,
|
||||
rest: Value,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load a `Config` from some string.
|
||||
pub fn from_str(src: &str) -> Result<Config> {
|
||||
toml::from_str(src).chain_err(|| Error::from("Invalid configuration file"))
|
||||
}
|
||||
impl FromStr for Config {
|
||||
type Err = Error;
|
||||
|
||||
/// Load a `Config` from some string.
|
||||
fn from_str(src: &str) -> Result<Self> {
|
||||
toml::from_str(src).with_context(|| "Invalid configuration file")
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load the configuration file from disk.
|
||||
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
|
||||
let mut buffer = String::new();
|
||||
File::open(config_file)
|
||||
.chain_err(|| "Unable to open the configuration file")?
|
||||
.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)
|
||||
}
|
||||
@@ -118,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
|
||||
/// > ```
|
||||
///
|
||||
@@ -128,16 +132,25 @@ impl Config {
|
||||
pub fn update_from_env(&mut self) {
|
||||
debug!("Updating the config from environment variables");
|
||||
|
||||
let overrides = env::vars().filter_map(|(key, value)| match parse_env(&key) {
|
||||
Some(index) => Some((index, value)),
|
||||
None => None,
|
||||
});
|
||||
let overrides =
|
||||
env::vars().filter_map(|(key, value)| parse_env(&key).map(|index| (index, value)));
|
||||
|
||||
for (key, value) in overrides {
|
||||
trace!("{} => {}", key, value);
|
||||
let parsed_value = serde_json::from_str(&value)
|
||||
.unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -145,21 +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> {
|
||||
match self.rest.read(key) {
|
||||
Ok(inner) => inner,
|
||||
Err(_) => None,
|
||||
}
|
||||
self.rest.read(key)
|
||||
}
|
||||
|
||||
/// Fetch a value from the `Config` so you can mutate it.
|
||||
pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Value> {
|
||||
match self.rest.read_mut(key) {
|
||||
Ok(inner) => inner,
|
||||
Err(_) => None,
|
||||
}
|
||||
pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
|
||||
self.rest.read_mut(key)
|
||||
}
|
||||
|
||||
/// Convenience method for getting the html renderer's configuration.
|
||||
@@ -170,22 +177,44 @@ impl Config {
|
||||
/// HTML renderer is refactored to be less coupled to `mdbook` internals.
|
||||
#[doc(hidden)]
|
||||
pub fn html_config(&self) -> Option<HtmlConfig> {
|
||||
self.get_deserialized("output.html").ok()
|
||||
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);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deprecated, use get_deserialized_opt instead.
|
||||
#[deprecated = "use get_deserialized_opt instead"]
|
||||
pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
|
||||
let name = name.as_ref();
|
||||
match self.get_deserialized_opt(name)? {
|
||||
Some(value) => Ok(value),
|
||||
None => bail!("Key not found, {:?}", name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to fetch a value from the config and deserialize it
|
||||
/// into some arbitrary type.
|
||||
pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
|
||||
pub fn get_deserialized_opt<'de, T: Deserialize<'de>, S: AsRef<str>>(
|
||||
&self,
|
||||
name: S,
|
||||
) -> Result<Option<T>> {
|
||||
let name = name.as_ref();
|
||||
|
||||
if let Some(value) = self.get(name) {
|
||||
value
|
||||
.clone()
|
||||
.try_into()
|
||||
.chain_err(|| "Couldn't deserialize the value")
|
||||
} else {
|
||||
bail!("Key not found, {:?}", name)
|
||||
}
|
||||
self.get(name)
|
||||
.map(|value| {
|
||||
value
|
||||
.clone()
|
||||
.try_into()
|
||||
.with_context(|| "Couldn't deserialize the value")
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// Set a config key, clobbering any existing values along the way.
|
||||
@@ -195,15 +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);
|
||||
} else {
|
||||
self.rest.insert(index, value)?;
|
||||
self.rest.insert(index, value);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -212,13 +241,13 @@ impl Config {
|
||||
/// Get the table associated with a particular renderer.
|
||||
pub fn get_renderer<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
|
||||
let key = format!("output.{}", index.as_ref());
|
||||
self.get(&key).and_then(|v| v.as_table())
|
||||
self.get(&key).and_then(Value::as_table)
|
||||
}
|
||||
|
||||
/// Get the table associated with a particular preprocessor.
|
||||
pub fn get_preprocessor<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
|
||||
let key = format!("preprocessor.{}", index.as_ref());
|
||||
self.get(&key).and_then(|v| v.as_table())
|
||||
self.get(&key).and_then(Value::as_table)
|
||||
}
|
||||
|
||||
fn from_legacy(mut table: Value) -> Config {
|
||||
@@ -244,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;
|
||||
}
|
||||
@@ -260,12 +289,13 @@ 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> {
|
||||
fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
|
||||
let raw = Value::deserialize(de)?;
|
||||
|
||||
if is_legacy_format(&raw) {
|
||||
@@ -276,7 +306,7 @@ impl<'de> Deserialize<'de> for Config {
|
||||
warn!("`description` under a table called `[book]`, move the `destination` entry");
|
||||
warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
|
||||
warn!("`[build]`, and it should all work.");
|
||||
warn!("Documentation: http://rust-lang-nursery.github.io/mdBook/format/config.html");
|
||||
warn!("Documentation: http://rust-lang.github.io/mdBook/format/config.html");
|
||||
return Ok(Config::from_legacy(raw));
|
||||
}
|
||||
|
||||
@@ -300,28 +330,33 @@ impl<'de> Deserialize<'de> for Config {
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let rust: RustConfig = table
|
||||
.remove("rust")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Config {
|
||||
book: book,
|
||||
build: build,
|
||||
book,
|
||||
build,
|
||||
rust,
|
||||
rest: Value::Table(table),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Config {
|
||||
fn serialize<S: Serializer>(&self, s: S) -> ::std::result::Result<S::Ok, S::Error> {
|
||||
use serde::ser::Error;
|
||||
|
||||
fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::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.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)
|
||||
}
|
||||
}
|
||||
@@ -348,7 +383,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;
|
||||
}
|
||||
}
|
||||
@@ -371,6 +406,8 @@ pub struct BookConfig {
|
||||
pub src: PathBuf,
|
||||
/// Does this book support more than one language?
|
||||
pub multilingual: bool,
|
||||
/// The main language of the book.
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for BookConfig {
|
||||
@@ -381,6 +418,7 @@ impl Default for BookConfig {
|
||||
description: None,
|
||||
src: PathBuf::from("src"),
|
||||
multilingual: false,
|
||||
language: Some(String::from("en")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -391,7 +429,7 @@ impl Default for BookConfig {
|
||||
pub struct BuildConfig {
|
||||
/// Where to put built artefacts relative to the book's root directory.
|
||||
pub build_dir: PathBuf,
|
||||
/// Should non-existent markdown files specified in `SETTINGS.md` be created
|
||||
/// Should non-existent markdown files specified in `SUMMARY.md` be created
|
||||
/// if they don't exist?
|
||||
pub create_missing: bool,
|
||||
/// Should the default preprocessors always be used when they are
|
||||
@@ -409,16 +447,42 @@ 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 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.
|
||||
pub theme: Option<PathBuf>,
|
||||
/// 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 '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>`.
|
||||
@@ -426,8 +490,24 @@ pub struct HtmlConfig {
|
||||
/// Additional JS scripts to include at the bottom of the rendered page's
|
||||
/// `<body>`.
|
||||
pub additional_js: Vec<PathBuf>,
|
||||
/// Playpen settings.
|
||||
pub playpen: Playpen,
|
||||
/// Fold settings.
|
||||
pub fold: Fold,
|
||||
/// Playground settings.
|
||||
#[serde(alias = "playpen")]
|
||||
pub playground: Playground,
|
||||
/// Don't render section labels.
|
||||
pub no_section_label: bool,
|
||||
/// Search settings. If `None`, the default will be used.
|
||||
pub search: Option<Search>,
|
||||
/// Git repository url. If `None`, the git button will not be shown.
|
||||
pub git_repository_url: Option<String>,
|
||||
/// FontAwesome icon class to use for the Git repository link.
|
||||
/// Defaults to `fa-github` if `None`.
|
||||
pub git_repository_icon: Option<String>,
|
||||
/// 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>,
|
||||
/// 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
|
||||
@@ -436,10 +516,35 @@ pub struct HtmlConfig {
|
||||
/// This config item *should not be edited* by the end user.
|
||||
#[doc(hidden)]
|
||||
pub livereload_url: Option<String>,
|
||||
/// Should section labels be rendered?
|
||||
pub no_section_label: bool,
|
||||
/// Search settings. If `None`, the default will be used.
|
||||
pub search: Option<Search>,
|
||||
/// 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(),
|
||||
no_section_label: false,
|
||||
search: None,
|
||||
git_repository_url: None,
|
||||
git_repository_icon: None,
|
||||
input_404: None,
|
||||
site_url: None,
|
||||
livereload_url: None,
|
||||
redirect: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HtmlConfig {
|
||||
@@ -453,22 +558,40 @@ impl HtmlConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the the HTML renderer handles the playpen.
|
||||
/// Configuration for how to fold chapters of sidebar.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Fold {
|
||||
/// When off, all folds are open. Default: `false`.
|
||||
pub enable: bool,
|
||||
/// The higher the more folded regions are open. When level is 0, all folds
|
||||
/// are closed.
|
||||
/// Default: `0`.
|
||||
pub level: u8,
|
||||
}
|
||||
|
||||
/// 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 playground snippets. Default: `false`.
|
||||
pub line_numbers: 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,7 +607,7 @@ pub struct Search {
|
||||
/// The number of words used for a search result teaser. Default: `30`.
|
||||
pub teaser_word_count: u32,
|
||||
/// Define the logical link between multiple search words.
|
||||
/// If true, all search words must appear in each result. Default: `true`.
|
||||
/// If true, all search words must appear in each result. Default: `false`.
|
||||
pub use_boolean_and: bool,
|
||||
/// Boost factor for the search result score if a search word appears in the header.
|
||||
/// Default: `2`.
|
||||
@@ -533,12 +656,10 @@ trait Updateable<'de>: Serialize + Deserialize<'de> {
|
||||
fn update_value<S: Serialize>(&mut self, key: &str, value: S) {
|
||||
let mut raw = Value::try_from(&self).expect("unreachable");
|
||||
|
||||
{
|
||||
if let Ok(value) = Value::try_from(value) {
|
||||
let _ = raw.insert(key, value);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if let Ok(value) = Value::try_from(value) {
|
||||
let _ = raw.insert(key, value);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(updated) = raw.try_into() {
|
||||
@@ -552,14 +673,16 @@ 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: &'static str = r#"
|
||||
const COMPLEX_CONFIG: &str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
|
||||
description = "A completely useless book"
|
||||
multilingual = true
|
||||
src = "source"
|
||||
language = "ja"
|
||||
|
||||
[build]
|
||||
build-dir = "outputs"
|
||||
@@ -568,17 +691,24 @@ mod tests {
|
||||
|
||||
[output.html]
|
||||
theme = "./themedir"
|
||||
default-theme = "rust"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["./foo/bar/baz.css"]
|
||||
git-repository-url = "https://foo.com/"
|
||||
git-repository-icon = "fa-code-fork"
|
||||
|
||||
[output.html.playpen]
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
editor = "ace"
|
||||
|
||||
[preprocess.first]
|
||||
[output.html.redirect]
|
||||
"index.html" = "overview.html"
|
||||
"nexted/page.md" = "https://rust-lang.org/"
|
||||
|
||||
[preprocess.second]
|
||||
[preprocessor.first]
|
||||
|
||||
[preprocessor.second]
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
@@ -591,23 +721,38 @@ mod tests {
|
||||
description: Some(String::from("A completely useless book")),
|
||||
multilingual: true,
|
||||
src: PathBuf::from("source"),
|
||||
..Default::default()
|
||||
language: Some(String::from("ja")),
|
||||
};
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("outputs"),
|
||||
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,
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
|
||||
theme: Some(PathBuf::from("./themedir")),
|
||||
playpen: playpen_should_be,
|
||||
default_theme: Some(String::from("rust")),
|
||||
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()
|
||||
};
|
||||
|
||||
@@ -615,9 +760,62 @@ 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 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 load_arbitrary_output_type() {
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
@@ -641,14 +839,17 @@ mod tests {
|
||||
};
|
||||
|
||||
let cfg = Config::from_str(src).unwrap();
|
||||
let got: RandomOutput = cfg.get_deserialized("output.random").unwrap();
|
||||
let got: RandomOutput = cfg.get_deserialized_opt("output.random").unwrap().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
let baz: Vec<bool> = cfg.get_deserialized("output.random.baz").unwrap();
|
||||
let got_baz: Vec<bool> = cfg
|
||||
.get_deserialized_opt("output.random.baz")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let baz_should_be = vec![true, true, false];
|
||||
|
||||
assert_eq!(baz, baz_should_be);
|
||||
assert_eq!(got_baz, baz_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -657,7 +858,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);
|
||||
@@ -725,7 +926,7 @@ mod tests {
|
||||
assert!(cfg.get(key).is_none());
|
||||
cfg.set(key, value).unwrap();
|
||||
|
||||
let got: String = cfg.get_deserialized(key).unwrap();
|
||||
let got: String = cfg.get_deserialized_opt(key).unwrap().unwrap();
|
||||
assert_eq!(got, value);
|
||||
}
|
||||
|
||||
@@ -740,7 +941,7 @@ mod tests {
|
||||
|
||||
for (src, should_be) in inputs {
|
||||
let got = parse_env(src);
|
||||
let should_be = should_be.map(|s| s.to_string());
|
||||
let should_be = should_be.map(ToString::to_string);
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
@@ -766,10 +967,14 @@ mod tests {
|
||||
|
||||
cfg.update_from_env();
|
||||
|
||||
assert_eq!(cfg.get_deserialized::<String, _>(key).unwrap(), value);
|
||||
assert_eq!(
|
||||
cfg.get_deserialized_opt::<String, _>(key).unwrap().unwrap(),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::approx_constant)]
|
||||
fn update_config_using_env_var_and_complex_value() {
|
||||
let mut cfg = Config::default();
|
||||
let key = "foo-bar.baz";
|
||||
@@ -784,7 +989,9 @@ mod tests {
|
||||
cfg.update_from_env();
|
||||
|
||||
assert_eq!(
|
||||
cfg.get_deserialized::<serde_json::Value, _>(key).unwrap(),
|
||||
cfg.get_deserialized_opt::<serde_json::Value, _>(key)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
value
|
||||
);
|
||||
}
|
||||
@@ -801,4 +1008,31 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
70
src/lib.rs
70
src/lib.rs
@@ -75,33 +75,23 @@
|
||||
//! directly, making deserializing the `RenderContext` easy and giving you
|
||||
//! access to the various methods for working with the [`Config`].
|
||||
//!
|
||||
//! [user guide]: https://rust-lang-nursery.github.io/mdBook/
|
||||
//! [user guide]: https://rust-lang.github.io/mdBook/
|
||||
//! [`RenderContext`]: renderer/struct.RenderContext.html
|
||||
//! [relevant chapter]: https://rust-lang-nursery.github.io/mdBook/for_developers/backends.html
|
||||
//! [relevant chapter]: https://rust-lang.github.io/mdBook/for_developers/backends.html
|
||||
//! [`Config`]: config/struct.Config.html
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(clippy::comparison_chain)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
extern crate handlebars;
|
||||
extern crate itertools;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate memchr;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate regex;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
extern crate shlex;
|
||||
extern crate tempfile;
|
||||
extern crate toml;
|
||||
extern crate toml_query;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
@@ -120,53 +110,13 @@ pub mod utils;
|
||||
/// compatibility checks.
|
||||
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub use book::BookItem;
|
||||
pub use book::MDBook;
|
||||
pub use config::Config;
|
||||
pub use renderer::Renderer;
|
||||
pub use crate::book::BookItem;
|
||||
pub use crate::book::MDBook;
|
||||
pub use crate::config::Config;
|
||||
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"];
|
||||
}
|
||||
|
||||
links {
|
||||
TomlQuery(::toml_query::error::Error, ::toml_query::error::ErrorKind) #[doc = "A TomlQuery error"];
|
||||
}
|
||||
|
||||
errors {
|
||||
/// A subprocess exited with an unsuccessful return code.
|
||||
Subprocess(message: String, output: ::std::process::Output) {
|
||||
description("A subprocess failed")
|
||||
display("{}: {}", message, String::from_utf8_lossy(&output.stdout))
|
||||
}
|
||||
|
||||
/// An error was encountered while parsing the `SUMMARY.md` file.
|
||||
ParseError(line: usize, col: usize, message: String) {
|
||||
description("A SUMMARY.md parsing error")
|
||||
display("Error at line {}, column {}: {}", line, col, message)
|
||||
}
|
||||
|
||||
/// The user tried to use a reserved filename.
|
||||
ReservedFilenameError(filename: PathBuf) {
|
||||
description("Reserved Filename")
|
||||
display("{} is reserved for internal use", filename.display())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Box to halve the size of Error
|
||||
impl From<::handlebars::TemplateError> for Error {
|
||||
fn from(e: ::handlebars::TemplateError) -> Error {
|
||||
From::from(Box::new(e))
|
||||
}
|
||||
}
|
||||
pub(crate) use anyhow::{bail, ensure, Context};
|
||||
pub use anyhow::{Error, Result};
|
||||
}
|
||||
|
||||
19
src/main.rs
19
src/main.rs
@@ -1,12 +1,7 @@
|
||||
extern crate chrono;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate env_logger;
|
||||
extern crate error_chain;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate mdbook;
|
||||
extern crate open;
|
||||
|
||||
use chrono::Local;
|
||||
use clap::{App, AppSettings, ArgMatches};
|
||||
@@ -20,22 +15,22 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
mod cmd;
|
||||
|
||||
const NAME: &'static str = "mdBook";
|
||||
const VERSION: &'static str = concat!("v", crate_version!());
|
||||
const VERSION: &str = concat!("v", crate_version!());
|
||||
|
||||
fn main() {
|
||||
init_logger();
|
||||
|
||||
// Create a list of valid arguments and sub-commands
|
||||
let app = App::new(NAME)
|
||||
.about("Creates a book from markdown files")
|
||||
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-nursery/mdBook",
|
||||
The source code for mdBook is available at: https://github.com/rust-lang/mdBook",
|
||||
)
|
||||
.subcommand(cmd::init::make_subcommand())
|
||||
.subcommand(cmd::build::make_subcommand())
|
||||
@@ -63,7 +58,7 @@ fn main() {
|
||||
if let Err(e) = res {
|
||||
utils::log_backtrace(&e);
|
||||
|
||||
::std::process::exit(101);
|
||||
std::process::exit(101);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +77,7 @@ fn init_logger() {
|
||||
});
|
||||
|
||||
if let Ok(var) = env::var("RUST_LOG") {
|
||||
builder.parse(&var);
|
||||
builder.parse_filters(&var);
|
||||
} else {
|
||||
// if no RUST_LOG provided, default to logging at the Info level
|
||||
builder.filter(None, LevelFilter::Info);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use book::Book;
|
||||
use errors::*;
|
||||
use serde_json;
|
||||
use crate::book::Book;
|
||||
use crate::errors::*;
|
||||
use shlex::Shlex;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
@@ -43,19 +42,11 @@ 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")
|
||||
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
|
||||
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,
|
||||
) {
|
||||
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) {
|
||||
@@ -65,9 +56,13 @@ impl CmdPreprocessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_input<W: Write>(&self, writer: W, book: &Book, ctx: &PreprocessorContext) -> Result<()> {
|
||||
serde_json::to_writer(writer, &(ctx, book))
|
||||
.map_err(Into::into)
|
||||
fn write_input<W: Write>(
|
||||
&self,
|
||||
writer: W,
|
||||
book: &Book,
|
||||
ctx: &PreprocessorContext,
|
||||
) -> Result<()> {
|
||||
serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// The command this `Preprocessor` will invoke.
|
||||
@@ -105,27 +100,44 @@ impl Preprocessor for CmdPreprocessor {
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.chain_err(|| format!("Unable to start the \"{}\" preprocessor. Is it installed?", self.name()))?;
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Unable to start the \"{}\" preprocessor. Is it installed?",
|
||||
self.name()
|
||||
)
|
||||
})?;
|
||||
|
||||
self.write_input_to_child(&mut child, &book, ctx);
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.chain_err(|| "Error waiting for the preprocessor to complete")?;
|
||||
.with_context(|| "Error waiting for the preprocessor to complete")?;
|
||||
|
||||
trace!("{} exited with output: {:?}", self.cmd, output);
|
||||
ensure!(output.status.success(), "The preprocessor exited unsuccessfully");
|
||||
ensure!(
|
||||
output.status.success(),
|
||||
"The preprocessor exited unsuccessfully"
|
||||
);
|
||||
|
||||
serde_json::from_slice(&output.stdout).chain_err(|| "Unable to parse the preprocessed book")
|
||||
serde_json::from_slice(&output.stdout)
|
||||
.with_context(|| "Unable to parse the preprocessed book")
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
debug!("Checking if the \"{}\" preprocessor supports \"{}\"", self.name(), renderer);
|
||||
debug!(
|
||||
"Checking if the \"{}\" preprocessor supports \"{}\"",
|
||||
self.name(),
|
||||
renderer
|
||||
);
|
||||
|
||||
let mut cmd = match self.command() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!("Unable to create the command for the \"{}\" preprocessor, {}", self.name(), e);
|
||||
warn!(
|
||||
"Unable to create the command for the \"{}\" preprocessor, {}",
|
||||
self.name(),
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -152,3 +164,34 @@ impl Preprocessor for CmdPreprocessor {
|
||||
outcome.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::MDBook;
|
||||
use std::path::Path;
|
||||
|
||||
fn book_example() -> MDBook {
|
||||
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
|
||||
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 ctx = PreprocessorContext::new(
|
||||
md.root.clone(),
|
||||
md.config.clone(),
|
||||
"some-renderer".to_string(),
|
||||
);
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
|
||||
|
||||
let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
|
||||
|
||||
assert_eq!(got_book, md.book);
|
||||
assert_eq!(got_ctx, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use regex::Regex;
|
||||
use std::path::Path;
|
||||
|
||||
use errors::*;
|
||||
use crate::errors::*;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use book::{Book, BookItem};
|
||||
use crate::book::{Book, BookItem};
|
||||
|
||||
/// A preprocessor for converting file name `README.md` to `index.md` since
|
||||
/// `README.md` is the de facto index file in a markdown-based documentation.
|
||||
/// `README.md` is the de facto index file in markdown-based documentation.
|
||||
#[derive(Default)]
|
||||
pub struct IndexPreprocessor;
|
||||
|
||||
impl IndexPreprocessor {
|
||||
@@ -28,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -45,7 +48,10 @@ impl Preprocessor for IndexPreprocessor {
|
||||
|
||||
fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
|
||||
let file_name = readme_path.as_ref().file_name().unwrap_or_default();
|
||||
let parent_dir = index_path.as_ref().parent().unwrap_or(index_path.as_ref());
|
||||
let parent_dir = index_path
|
||||
.as_ref()
|
||||
.parent()
|
||||
.unwrap_or_else(|| index_path.as_ref());
|
||||
warn!(
|
||||
"It seems that there are both {:?} and index.md under \"{}\".",
|
||||
file_name,
|
||||
@@ -67,7 +73,7 @@ fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
|
||||
RE.is_match(
|
||||
path.as_ref()
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
use errors::*;
|
||||
use crate::errors::*;
|
||||
use crate::utils::{
|
||||
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
|
||||
take_rustdoc_include_lines,
|
||||
};
|
||||
use regex::{CaptureMatches, Captures, Regex};
|
||||
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
|
||||
use std::fs;
|
||||
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
|
||||
use std::path::{Path, PathBuf};
|
||||
use utils::fs::file_to_string;
|
||||
use utils::take_lines;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use book::{Book, BookItem};
|
||||
use crate::book::{Book, BookItem};
|
||||
|
||||
const ESCAPE_CHAR: char = '\\';
|
||||
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
|
||||
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
|
||||
/// helpers in a chapter.
|
||||
/// A preprocessor for expanding helpers in a chapter. Supported helpers are:
|
||||
///
|
||||
/// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular
|
||||
///. lines, or only between the specified anchors.
|
||||
/// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines
|
||||
///. 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.
|
||||
/// - `{{# playground}}` - Insert runnable Rust files
|
||||
#[derive(Default)]
|
||||
pub struct LinkPreprocessor;
|
||||
|
||||
impl LinkPreprocessor {
|
||||
@@ -34,14 +45,15 @@ 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 content = replace_all(&ch.content, base, chapter_path, 0);
|
||||
ch.content = content;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -62,19 +74,14 @@ where
|
||||
let mut previous_end_index = 0;
|
||||
let mut replaced = String::new();
|
||||
|
||||
for playpen in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..playpen.start_index]);
|
||||
for link in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..link.start_index]);
|
||||
|
||||
match playpen.render_with_path(&path) {
|
||||
match link.render_with_path(&path) {
|
||||
Ok(new_content) => {
|
||||
if depth < MAX_LINK_NESTED_DEPTH {
|
||||
if let Some(rel_path) = playpen.link.relative_path(path) {
|
||||
replaced.push_str(&replace_all(
|
||||
&new_content,
|
||||
rel_path,
|
||||
source,
|
||||
depth + 1,
|
||||
));
|
||||
if let Some(rel_path) = link.link_type.relative_path(path) {
|
||||
replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1));
|
||||
} else {
|
||||
replaced.push_str(&new_content);
|
||||
}
|
||||
@@ -84,17 +91,17 @@ where
|
||||
source.display()
|
||||
);
|
||||
}
|
||||
previous_end_index = playpen.end_index;
|
||||
previous_end_index = link.end_index;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error updating \"{}\", {}", playpen.link_text, e);
|
||||
for cause in e.iter().skip(1) {
|
||||
error!("Error updating \"{}\", {}", link.link_text, e);
|
||||
for cause in e.chain().skip(1) {
|
||||
warn!("Caused By: {}", cause);
|
||||
}
|
||||
|
||||
// This should make sure we include the raw `{{# ... }}` snippet
|
||||
// in the page content if there are any errors.
|
||||
previous_end_index = playpen.start_index;
|
||||
previous_end_index = link.start_index;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,11 +113,68 @@ where
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum LinkType<'a> {
|
||||
Escaped,
|
||||
IncludeRange(PathBuf, Range<usize>),
|
||||
IncludeRangeFrom(PathBuf, RangeFrom<usize>),
|
||||
IncludeRangeTo(PathBuf, RangeTo<usize>),
|
||||
IncludeRangeFull(PathBuf, RangeFull),
|
||||
Playpen(PathBuf, Vec<&'a str>),
|
||||
Include(PathBuf, RangeOrAnchor),
|
||||
Playground(PathBuf, Vec<&'a str>),
|
||||
RustdocInclude(PathBuf, RangeOrAnchor),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum RangeOrAnchor {
|
||||
Range(LineRange),
|
||||
Anchor(String),
|
||||
}
|
||||
|
||||
// A range of lines specified with some include directive.
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum LineRange {
|
||||
Range(Range<usize>),
|
||||
RangeFrom(RangeFrom<usize>),
|
||||
RangeTo(RangeTo<usize>),
|
||||
RangeFull(RangeFull),
|
||||
}
|
||||
|
||||
impl RangeBounds<usize> for LineRange {
|
||||
fn start_bound(&self) -> Bound<&usize> {
|
||||
match self {
|
||||
LineRange::Range(r) => r.start_bound(),
|
||||
LineRange::RangeFrom(r) => r.start_bound(),
|
||||
LineRange::RangeTo(r) => r.start_bound(),
|
||||
LineRange::RangeFull(r) => r.start_bound(),
|
||||
}
|
||||
}
|
||||
|
||||
fn end_bound(&self) -> Bound<&usize> {
|
||||
match self {
|
||||
LineRange::Range(r) => r.end_bound(),
|
||||
LineRange::RangeFrom(r) => r.end_bound(),
|
||||
LineRange::RangeTo(r) => r.end_bound(),
|
||||
LineRange::RangeFull(r) => r.end_bound(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Range<usize>> for LineRange {
|
||||
fn from(r: Range<usize>) -> LineRange {
|
||||
LineRange::Range(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RangeFrom<usize>> for LineRange {
|
||||
fn from(r: RangeFrom<usize>) -> LineRange {
|
||||
LineRange::RangeFrom(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RangeTo<usize>> for LineRange {
|
||||
fn from(r: RangeTo<usize>) -> LineRange {
|
||||
LineRange::RangeTo(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RangeFull> for LineRange {
|
||||
fn from(r: RangeFull) -> LineRange {
|
||||
LineRange::RangeFull(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> LinkType<'a> {
|
||||
@@ -118,19 +182,9 @@ impl<'a> LinkType<'a> {
|
||||
let base = base.as_ref();
|
||||
match self {
|
||||
LinkType::Escaped => None,
|
||||
LinkType::IncludeRange(p, _) => {
|
||||
Some(return_relative_path(base, &p))
|
||||
}
|
||||
LinkType::IncludeRangeFrom(p, _) => {
|
||||
Some(return_relative_path(base, &p))
|
||||
}
|
||||
LinkType::IncludeRangeTo(p, _) => {
|
||||
Some(return_relative_path(base, &p))
|
||||
}
|
||||
LinkType::IncludeRangeFull(p, _) => {
|
||||
Some(return_relative_path(base, &p))
|
||||
}
|
||||
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Include(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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,50 +196,59 @@ fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
fn parse_include_path(path: &str) -> LinkType<'static> {
|
||||
let mut parts = path.split(':');
|
||||
let path = parts.next().unwrap().into();
|
||||
// subtract 1 since line numbers usually begin with 1
|
||||
let start = parts
|
||||
.next()
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.map(|val| val.saturating_sub(1));
|
||||
fn parse_range_or_anchor(parts: Option<&str>) -> RangeOrAnchor {
|
||||
let mut parts = parts.unwrap_or("").splitn(3, ':').fuse();
|
||||
|
||||
let next_element = parts.next();
|
||||
let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) {
|
||||
// subtract 1 since line numbers usually begin with 1
|
||||
Some(value.saturating_sub(1))
|
||||
} else if let Some("") = next_element {
|
||||
None
|
||||
} else if let Some(anchor) = next_element {
|
||||
return RangeOrAnchor::Anchor(String::from(anchor));
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let end = parts.next();
|
||||
let has_end = end.is_some();
|
||||
let end = end.and_then(|s| s.parse::<usize>().ok());
|
||||
match start {
|
||||
Some(start) => match end {
|
||||
Some(end) => LinkType::IncludeRange(
|
||||
path,
|
||||
Range {
|
||||
start: start,
|
||||
end: end,
|
||||
},
|
||||
),
|
||||
None => if has_end {
|
||||
LinkType::IncludeRangeFrom(path, RangeFrom { start: start })
|
||||
} else {
|
||||
LinkType::IncludeRange(
|
||||
path,
|
||||
Range {
|
||||
start: start,
|
||||
end: start + 1,
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
None => match end {
|
||||
Some(end) => LinkType::IncludeRangeTo(path, RangeTo { end: end }),
|
||||
None => LinkType::IncludeRangeFull(path, RangeFull),
|
||||
},
|
||||
// If `end` is empty string or any other value that can't be parsed as a usize, treat this
|
||||
// include as a range with only a start bound. However, if end isn't specified, include only
|
||||
// the single line specified by `start`.
|
||||
let end = end.map(|s| s.parse::<usize>());
|
||||
|
||||
match (start, end) {
|
||||
(Some(start), Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(start..end)),
|
||||
(Some(start), Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(start..)),
|
||||
(Some(start), None) => RangeOrAnchor::Range(LineRange::from(start..start + 1)),
|
||||
(None, Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(..end)),
|
||||
(None, None) | (None, Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(RangeFull)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_include_path(path: &str) -> LinkType<'static> {
|
||||
let mut parts = path.splitn(2, ':');
|
||||
|
||||
let path = parts.next().unwrap().into();
|
||||
let range_or_anchor = parse_range_or_anchor(parts.next());
|
||||
|
||||
LinkType::Include(path, range_or_anchor)
|
||||
}
|
||||
|
||||
fn parse_rustdoc_include_path(path: &str) -> LinkType<'static> {
|
||||
let mut parts = path.splitn(2, ':');
|
||||
|
||||
let path = parts.next().unwrap().into();
|
||||
let range_or_anchor = parse_range_or_anchor(parts.next());
|
||||
|
||||
LinkType::RustdocInclude(path, range_or_anchor)
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
struct Link<'a> {
|
||||
start_index: usize,
|
||||
end_index: usize,
|
||||
link: LinkType<'a>,
|
||||
link_type: LinkType<'a>,
|
||||
link_text: &'a str,
|
||||
}
|
||||
|
||||
@@ -199,25 +262,30 @@ impl<'a> Link<'a> {
|
||||
|
||||
match (typ.as_str(), file_arg) {
|
||||
("include", Some(pth)) => Some(parse_include_path(pth)),
|
||||
("playground", Some(pth)) => Some(LinkType::Playground(pth.into(), props)),
|
||||
("playpen", Some(pth)) => {
|
||||
Some(LinkType::Playpen(pth.into(), props))
|
||||
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,
|
||||
}
|
||||
}
|
||||
(Some(mat), None, None)
|
||||
if mat.as_str().starts_with(ESCAPE_CHAR) =>
|
||||
{
|
||||
(Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => {
|
||||
Some(LinkType::Escaped)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
link_type.and_then(|lnk| {
|
||||
link_type.and_then(|lnk_type| {
|
||||
cap.get(0).map(|mat| Link {
|
||||
start_index: mat.start(),
|
||||
end_index: mat.end(),
|
||||
link: lnk,
|
||||
link_type: lnk_type,
|
||||
link_text: mat.as_str(),
|
||||
})
|
||||
})
|
||||
@@ -225,15 +293,18 @@ impl<'a> Link<'a> {
|
||||
|
||||
fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
|
||||
let base = base.as_ref();
|
||||
match self.link {
|
||||
match self.link_type {
|
||||
// omit the escape char
|
||||
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
||||
LinkType::IncludeRange(ref pat, ref range) => {
|
||||
LinkType::Include(ref pat, ref range_or_anchor) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
file_to_string(&target)
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| {
|
||||
fs::read_to_string(&target)
|
||||
.map(|s| match range_or_anchor {
|
||||
RangeOrAnchor::Range(range) => take_lines(&s, range.clone()),
|
||||
RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor),
|
||||
})
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
@@ -241,12 +312,19 @@ impl<'a> Link<'a> {
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeRangeFrom(ref pat, ref range) => {
|
||||
LinkType::RustdocInclude(ref pat, ref range_or_anchor) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
file_to_string(&target)
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| {
|
||||
fs::read_to_string(&target)
|
||||
.map(|s| match range_or_anchor {
|
||||
RangeOrAnchor::Range(range) => {
|
||||
take_rustdoc_include_lines(&s, range.clone())
|
||||
}
|
||||
RangeOrAnchor::Anchor(anchor) => {
|
||||
take_rustdoc_include_anchored_lines(&s, anchor)
|
||||
}
|
||||
})
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
@@ -254,39 +332,16 @@ impl<'a> Link<'a> {
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeRangeTo(ref pat, ref range) => {
|
||||
LinkType::Playground(ref pat, ref attrs) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
file_to_string(&target)
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeRangeFull(ref pat, _) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
file_to_string(&target).chain_err(|| {
|
||||
format!("Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display())
|
||||
})
|
||||
}
|
||||
LinkType::Playpen(ref pat, ref attrs) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
let contents =
|
||||
file_to_string(&target).chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display()
|
||||
)
|
||||
})?;
|
||||
let contents = fs::read_to_string(&target).with_context(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display()
|
||||
)
|
||||
})?;
|
||||
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
||||
Ok(format!(
|
||||
"```{}{}\n{}\n```\n",
|
||||
@@ -313,20 +368,21 @@ impl<'a> Iterator for LinkIter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_links(contents: &str) -> LinkIter {
|
||||
fn find_links(contents: &str) -> LinkIter<'_> {
|
||||
// lazily compute following regex
|
||||
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(
|
||||
r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
|
||||
\s*\}\} # whitespace and link closing parens"
|
||||
).unwrap();
|
||||
r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9_]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([a-zA-Z0-9\s_.\-:/\\\+]+) # link target path and space separated properties
|
||||
\s*\}\} # whitespace and link closing parens"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
LinkIter(RE.captures_iter(contents))
|
||||
}
|
||||
@@ -358,7 +414,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![]);
|
||||
@@ -368,19 +424,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);
|
||||
@@ -390,20 +446,38 @@ mod tests {
|
||||
vec![
|
||||
Link {
|
||||
start_index: 22,
|
||||
end_index: 42,
|
||||
link: 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: 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}}...";
|
||||
@@ -414,7 +488,10 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 48,
|
||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(9..20))
|
||||
),
|
||||
link_text: "{{#include file.rs:10:20}}",
|
||||
}]
|
||||
);
|
||||
@@ -430,7 +507,10 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 45,
|
||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(9..10))
|
||||
),
|
||||
link_text: "{{#include file.rs:10}}",
|
||||
}]
|
||||
);
|
||||
@@ -446,7 +526,10 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(9..))
|
||||
),
|
||||
link_text: "{{#include file.rs:10:}}",
|
||||
}]
|
||||
);
|
||||
@@ -462,7 +545,10 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..20))
|
||||
),
|
||||
link_text: "{{#include file.rs::20}}",
|
||||
}]
|
||||
);
|
||||
@@ -478,7 +564,10 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 44,
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..))
|
||||
),
|
||||
link_text: "{{#include file.rs::}}",
|
||||
}]
|
||||
);
|
||||
@@ -494,15 +583,37 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 42,
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..))
|
||||
),
|
||||
link_text: "{{#include file.rs}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_with_anchor() {
|
||||
let s = "Some random text with {{#include file.rs:anchor}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 49,
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Anchor(String::from("anchor"))
|
||||
),
|
||||
link_text: "{{#include file.rs:anchor}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[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);
|
||||
@@ -510,18 +621,19 @@ mod tests {
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: LinkType::Escaped,
|
||||
link_text: "\\{{#playpen file.rs editable}}",
|
||||
start_index: 41,
|
||||
end_index: 74,
|
||||
link_type: LinkType::Escaped,
|
||||
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);
|
||||
@@ -529,23 +641,19 @@ mod tests {
|
||||
res,
|
||||
vec![
|
||||
Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: 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: 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}}",
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -553,8 +661,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<_>>();
|
||||
@@ -563,33 +672,215 @@ mod tests {
|
||||
assert_eq!(
|
||||
res[0],
|
||||
Link {
|
||||
start_index: 38,
|
||||
end_index: 58,
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
start_index: 41,
|
||||
end_index: 61,
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..))
|
||||
),
|
||||
link_text: "{{#include file.rs}}",
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
res[1],
|
||||
Link {
|
||||
start_index: 63,
|
||||
end_index: 112,
|
||||
link: LinkType::Escaped,
|
||||
start_index: 66,
|
||||
end_index: 115,
|
||||
link_type: LinkType::Escaped,
|
||||
link_text: "\\{{#contents are insignifficant in escaped link}}",
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
res[2],
|
||||
Link {
|
||||
start_index: 130,
|
||||
end_index: 177,
|
||||
link: 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}}",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_without_colon_includes_all() {
|
||||
let link_type = parse_include_path("arbitrary");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_nothing_after_colon_includes_all() {
|
||||
let link_type = parse_include_path("arbitrary:");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_two_colons_includes_all() {
|
||||
let link_type = parse_include_path("arbitrary::");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_garbage_after_two_colons_includes_all() {
|
||||
let link_type = parse_include_path("arbitrary::NaN");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_one_number_after_colon_only_that_line() {
|
||||
let link_type = parse_include_path("arbitrary:5");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(4..5))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_one_based_start_becomes_zero_based() {
|
||||
let link_type = parse_include_path("arbitrary:1");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(0..1))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_zero_based_start_stays_zero_based_but_is_probably_an_error() {
|
||||
let link_type = parse_include_path("arbitrary:0");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(0..1))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_start_only_range() {
|
||||
let link_type = parse_include_path("arbitrary:5:");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(4..))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_start_with_garbage_interpreted_as_start_only_range() {
|
||||
let link_type = parse_include_path("arbitrary:5:NaN");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(4..))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_end_only_range() {
|
||||
let link_type = parse_include_path("arbitrary::5");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(..5))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_start_and_end_range() {
|
||||
let link_type = parse_include_path("arbitrary:5:10");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(4..10))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_negative_interpreted_as_anchor() {
|
||||
let link_type = parse_include_path("arbitrary:-5");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Anchor("-5".to_string())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_floating_point_interpreted_as_anchor() {
|
||||
let link_type = parse_include_path("arbitrary:-5.7");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Anchor("-5.7".to_string())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_anchor_followed_by_colon() {
|
||||
let link_type = parse_include_path("arbitrary:some-anchor:this-gets-ignored");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Anchor("some-anchor".to_string())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_more_than_three_colons_ignores_everything_after_third_colon() {
|
||||
let link_type = parse_include_path("arbitrary:5:10:17:anything:");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(4..10))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
//! Book preprocessing.
|
||||
|
||||
pub use self::cmd::CmdPreprocessor;
|
||||
pub use self::index::IndexPreprocessor;
|
||||
pub use self::links::LinkPreprocessor;
|
||||
pub use self::cmd::CmdPreprocessor;
|
||||
|
||||
mod cmd;
|
||||
mod index;
|
||||
mod links;
|
||||
mod cmd;
|
||||
|
||||
use book::Book;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct PreprocessorContext {
|
||||
pub renderer: String,
|
||||
/// The calling `mdbook` version.
|
||||
pub mdbook_version: String,
|
||||
#[serde(skip)]
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
@@ -36,7 +37,7 @@ impl PreprocessorContext {
|
||||
root,
|
||||
config,
|
||||
renderer,
|
||||
mdbook_version: ::MDBOOK_VERSION.to_string(),
|
||||
mdbook_version: crate::MDBOOK_VERSION.to_string(),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use book::{Book, BookItem};
|
||||
use config::{Config, HtmlConfig, Playpen};
|
||||
use errors::*;
|
||||
use renderer::html_handlebars::helpers;
|
||||
use renderer::{RenderContext, Renderer};
|
||||
use theme::{self, playpen_editor, Theme};
|
||||
use utils;
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::config::{Config, HtmlConfig, Playground, RustEdition};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
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};
|
||||
use serde_json;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HtmlHandlebars;
|
||||
@@ -26,83 +27,148 @@ impl HtmlHandlebars {
|
||||
fn render_item(
|
||||
&self,
|
||||
item: &BookItem,
|
||||
mut ctx: RenderItemContext,
|
||||
mut ctx: RenderItemContext<'_>,
|
||||
print_content: &mut String,
|
||||
) -> Result<()> {
|
||||
// FIXME: This should be made DRY-er and rely less on mutable state
|
||||
match *item {
|
||||
BookItem::Chapter(ref ch) => {
|
||||
let content = ch.content.clone();
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
print_content.push_str(&content);
|
||||
|
||||
// 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");
|
||||
let (ch, path) = match item {
|
||||
BookItem::Chapter(ch) if !ch.is_draft_chapter() => (ch, ch.path.as_ref().unwrap()),
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
// "print.html" is used for the print page.
|
||||
if ch.path == Path::new("print.md") {
|
||||
bail!(ErrorKind::ReservedFilenameError(ch.path.clone()));
|
||||
};
|
||||
let content = ch.content.clone();
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
|
||||
// Non-lexical lifetimes needed :'(
|
||||
let title: String;
|
||||
{
|
||||
let book_title = ctx
|
||||
.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
title = ch.name.clone() + " - " + book_title;
|
||||
}
|
||||
let fixed_content = utils::render_markdown_with_path(
|
||||
&ch.content,
|
||||
ctx.html_config.curly_quotes,
|
||||
Some(&path),
|
||||
);
|
||||
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)),
|
||||
);
|
||||
// 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 = match book_title {
|
||||
"" => ch.name.clone(),
|
||||
_ => ch.name.clone() + " - " + book_title,
|
||||
};
|
||||
|
||||
if ctx.is_index {
|
||||
ctx.data.insert("path".to_owned(), json!("index.html"));
|
||||
ctx.data.insert("path_to_root".to_owned(), json!(""));
|
||||
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
|
||||
let rendered_index =
|
||||
self.post_process(rendered_index, &ctx.html_config.playpen);
|
||||
debug!("Creating index.html from {}", path);
|
||||
utils::fs::write_file(
|
||||
&ctx.destination,
|
||||
"index.html",
|
||||
rendered_index.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
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(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
|
||||
fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String {
|
||||
fn render_404(
|
||||
&self,
|
||||
ctx: &RenderContext,
|
||||
html_config: &HtmlConfig,
|
||||
src_dir: &PathBuf,
|
||||
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 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,
|
||||
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
|
||||
}
|
||||
@@ -113,7 +179,7 @@ impl HtmlHandlebars {
|
||||
theme: &Theme,
|
||||
html_config: &HtmlConfig,
|
||||
) -> Result<()> {
|
||||
use utils::fs::write_file;
|
||||
use crate::utils::fs::write_file;
|
||||
|
||||
write_file(
|
||||
destination,
|
||||
@@ -126,7 +192,12 @@ impl HtmlHandlebars {
|
||||
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
|
||||
write_file(destination, "css/print.css", &theme.print_css)?;
|
||||
write_file(destination, "css/variables.css", &theme.variables_css)?;
|
||||
write_file(destination, "favicon.png", &theme.favicon)?;
|
||||
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)?;
|
||||
@@ -167,20 +238,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,
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -205,7 +294,7 @@ impl HtmlHandlebars {
|
||||
);
|
||||
}
|
||||
|
||||
fn register_hbs_helpers(&self, handlebars: &mut Handlebars, html_config: &HtmlConfig) {
|
||||
fn register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig) {
|
||||
handlebars.register_helper(
|
||||
"toc",
|
||||
Box::new(helpers::toc::RenderToc {
|
||||
@@ -214,6 +303,7 @@ impl HtmlHandlebars {
|
||||
);
|
||||
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
|
||||
handlebars.register_helper("next", Box::new(helpers::navigation::next));
|
||||
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
|
||||
}
|
||||
|
||||
/// Copy across any additional CSS and JavaScript files which the book
|
||||
@@ -233,7 +323,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 {} -> {}",
|
||||
@@ -241,7 +331,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(),
|
||||
@@ -252,6 +342,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
|
||||
@@ -283,6 +435,12 @@ impl Renderer for HtmlHandlebars {
|
||||
let src_dir = ctx.root.join(&ctx.config.book.src);
|
||||
let destination = &ctx.destination;
|
||||
let book = &ctx.book;
|
||||
let build_dir = ctx.root.join(&ctx.config.build.build_dir);
|
||||
|
||||
if destination.exists() {
|
||||
utils::fs::remove_dir_content(destination)
|
||||
.with_context(|| "Unable to remove stale HTML output")?;
|
||||
}
|
||||
|
||||
trace!("render");
|
||||
let mut handlebars = Handlebars::new();
|
||||
@@ -307,6 +465,13 @@ 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())?)?;
|
||||
|
||||
@@ -319,7 +484,7 @@ impl Renderer for HtmlHandlebars {
|
||||
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() {
|
||||
@@ -327,13 +492,19 @@ impl Renderer for HtmlHandlebars {
|
||||
handlebars: &handlebars,
|
||||
destination: destination.to_path_buf(),
|
||||
data: data.clone(),
|
||||
is_index: is_index,
|
||||
is_index,
|
||||
html_config: html_config.clone(),
|
||||
edition: ctx.config.rust.edition,
|
||||
};
|
||||
self.render_item(item, ctx, &mut print_content)?;
|
||||
is_index = false;
|
||||
}
|
||||
|
||||
// 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
|
||||
self.configure_print_version(&mut data, &print_content);
|
||||
if let Some(ref title) = ctx.config.book.title {
|
||||
@@ -344,16 +515,17 @@ impl Renderer for HtmlHandlebars {
|
||||
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 ✓");
|
||||
|
||||
debug!("Copy static files");
|
||||
self.copy_static_files(&destination, &theme, &html_config)
|
||||
.chain_err(|| "Unable to copy across static files")?;
|
||||
.with_context(|| "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")?;
|
||||
.with_context(|| "Unable to copy across additional CSS and JS")?;
|
||||
|
||||
// Render search index
|
||||
#[cfg(feature = "search")]
|
||||
@@ -364,8 +536,11 @@ impl Renderer for HtmlHandlebars {
|
||||
}
|
||||
}
|
||||
|
||||
// Copy all remaining files
|
||||
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
|
||||
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"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -378,10 +553,12 @@ fn make_data(
|
||||
html_config: &HtmlConfig,
|
||||
) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
trace!("make_data");
|
||||
let html = config.html_config().unwrap_or_default();
|
||||
|
||||
let mut data = serde_json::Map::new();
|
||||
data.insert("language".to_owned(), json!("en"));
|
||||
data.insert(
|
||||
"language".to_owned(),
|
||||
json!(config.book.language.clone().unwrap_or_default()),
|
||||
);
|
||||
data.insert(
|
||||
"book_title".to_owned(),
|
||||
json!(config.book.title.clone().unwrap_or_default()),
|
||||
@@ -390,24 +567,42 @@ 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));
|
||||
}
|
||||
|
||||
let default_theme = match html_config.default_theme {
|
||||
Some(ref theme) => theme.to_lowercase(),
|
||||
None => "light".to_string(),
|
||||
};
|
||||
data.insert("default_theme".to_owned(), json!(default_theme));
|
||||
|
||||
let preferred_dark_theme = match html_config.preferred_dark_theme {
|
||||
Some(ref theme) => theme.to_lowercase(),
|
||||
None => "navy".to_string(),
|
||||
};
|
||||
data.insert(
|
||||
"preferred_dark_theme".to_owned(),
|
||||
json!(preferred_dark_theme),
|
||||
);
|
||||
|
||||
// Add google analytics tag
|
||||
if let Some(ref ga) = config.html_config().and_then(|html| html.google_analytics) {
|
||||
if let Some(ref ga) = html_config.google_analytics {
|
||||
data.insert("google_analytics".to_owned(), json!(ga));
|
||||
}
|
||||
|
||||
if html.mathjax_support {
|
||||
if html_config.mathjax_support {
|
||||
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.additional_css.is_empty() {
|
||||
if !html_config.additional_css.is_empty() {
|
||||
let mut css = Vec::new();
|
||||
for style in &html.additional_css {
|
||||
for style in &html_config.additional_css {
|
||||
match style.strip_prefix(root) {
|
||||
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => css.push(style.to_str().expect("Could not convert to str")),
|
||||
@@ -417,26 +612,29 @@ fn make_data(
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional script
|
||||
if !html.additional_js.is_empty() {
|
||||
if !html_config.additional_js.is_empty() {
|
||||
let mut js = Vec::new();
|
||||
for script in &html.additional_js {
|
||||
for script in &html_config.additional_js {
|
||||
match script.strip_prefix(root) {
|
||||
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => js.push(
|
||||
script
|
||||
.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str"),
|
||||
),
|
||||
Err(_) => js.push(script.to_str().expect("Could not convert to str")),
|
||||
}
|
||||
}
|
||||
data.insert("additional_js".to_owned(), json!(js));
|
||||
}
|
||||
|
||||
if html.playpen.editable && html.playpen.copy_js {
|
||||
data.insert("playpen_js".to_owned(), json!(true));
|
||||
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.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)));
|
||||
|
||||
let search = html_config.search.clone();
|
||||
if cfg!(feature = "search") {
|
||||
@@ -454,6 +652,16 @@ fn make_data(
|
||||
)
|
||||
}
|
||||
|
||||
if let Some(ref git_repository_url) = html_config.git_repository_url {
|
||||
data.insert("git_repository_url".to_owned(), json!(git_repository_url));
|
||||
}
|
||||
|
||||
let git_repository_icon = match html_config.git_repository_icon {
|
||||
Some(ref git_repository_icon) => git_repository_icon,
|
||||
None => "fa-github",
|
||||
};
|
||||
data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
|
||||
|
||||
let mut chapters = vec![];
|
||||
|
||||
for item in book.iter() {
|
||||
@@ -461,17 +669,26 @@ 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()));
|
||||
}
|
||||
|
||||
chapter.insert(
|
||||
"has_sub_items".to_owned(),
|
||||
json!((!ch.sub_items.is_empty()).to_string()),
|
||||
);
|
||||
|
||||
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_"));
|
||||
@@ -487,25 +704,26 @@ fn make_data(
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Goes through the rendered HTML, making sure all header tags are wrapped in
|
||||
/// an anchor so people can link to sections directly.
|
||||
/// 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();
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let level = caps[1]
|
||||
.parse()
|
||||
.expect("Regex should ensure we only ever get numbers here");
|
||||
|
||||
wrap_header_with_link(level, &caps[2], &mut id_counter)
|
||||
}).into_owned()
|
||||
insert_link_into_header(level, &caps[2], &mut id_counter)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Wraps a single header tag with a link, making sure each tag gets its own
|
||||
/// Insert a sinle link into a header, making sure each link gets its own
|
||||
/// unique ID by appending an auto-incremented number (if necessary).
|
||||
fn wrap_header_with_link(
|
||||
fn insert_link_into_header(
|
||||
level: usize,
|
||||
content: &str,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
@@ -522,7 +740,7 @@ fn wrap_header_with_link(
|
||||
*id_count += 1;
|
||||
|
||||
format!(
|
||||
r##"<a class="header" href="#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
|
||||
r##"<h{level}><a class="header" href="#{id}" id="{id}">{text}</a></h{level}>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content
|
||||
@@ -540,7 +758,7 @@ fn wrap_header_with_link(
|
||||
fn fix_code_blocks(html: &str) -> String {
|
||||
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let before = &caps[1];
|
||||
let classes = &caps[2].replace(",", " ");
|
||||
let after = &caps[3];
|
||||
@@ -551,43 +769,107 @@ fn fix_code_blocks(html: &str) -> String {
|
||||
classes = classes,
|
||||
after = after
|
||||
)
|
||||
}).into_owned()
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||
fn add_playground_pre(
|
||||
html: &str,
|
||||
playground_config: &Playground,
|
||||
edition: Option<RustEdition>,
|
||||
) -> String {
|
||||
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
|
||||
if (classes.contains("language-rust")
|
||||
&& !classes.contains("ignore")
|
||||
&& !classes.contains("noplaypen"))
|
||||
|| classes.contains("mdbook-runnable")
|
||||
{
|
||||
// wrap the contents in an external pre block
|
||||
if playpen_config.editable && classes.contains("editable")
|
||||
|| text.contains("fn main")
|
||||
|| text.contains("quick_main!")
|
||||
if classes.contains("language-rust") {
|
||||
if (!classes.contains("ignore")
|
||||
&& !classes.contains("noplayground")
|
||||
&& !classes.contains("noplaypen"))
|
||||
|| classes.contains("mdbook-runnable")
|
||||
{
|
||||
format!("<pre class=\"playpen\">{}</pre>", text)
|
||||
} else {
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
let contains_e2015 = classes.contains("edition2015");
|
||||
let contains_e2018 = classes.contains("edition2018");
|
||||
let edition_class = if contains_e2015 || contains_e2018 {
|
||||
// the user forced edition, we should not overwrite it
|
||||
""
|
||||
} else {
|
||||
match edition {
|
||||
Some(RustEdition::E2015) => " edition2015",
|
||||
Some(RustEdition::E2018) => " edition2018",
|
||||
None => "",
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the contents in an external pre block
|
||||
format!(
|
||||
"<pre class=\"playpen\"><code class=\"{}\">\n# \
|
||||
#![allow(unused_variables)]\n{}#fn main() {{\n{}#}}</code></pre>",
|
||||
classes, attrs, code
|
||||
"<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
|
||||
classes,
|
||||
edition_class,
|
||||
{
|
||||
let content: Cow<'_, str> = if playground_config.editable
|
||||
&& classes.contains("editable")
|
||||
|| text.contains("fn main")
|
||||
|| text.contains("quick_main!")
|
||||
{
|
||||
code.into()
|
||||
} else {
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
|
||||
format!(
|
||||
"\n# #![allow(unused)]\n{}#fn main() {{\n{}#}}",
|
||||
attrs, code
|
||||
)
|
||||
.into()
|
||||
};
|
||||
hide_lines(&content)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
format!("<code class=\"{}\">{}</code>", classes, hide_lines(code))
|
||||
}
|
||||
} else {
|
||||
// not language-rust, so no-op
|
||||
text.to_owned()
|
||||
}
|
||||
}).into_owned()
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap();
|
||||
}
|
||||
|
||||
fn hide_lines(content: &str) -> String {
|
||||
let mut result = String::with_capacity(content.len());
|
||||
for line in content.lines() {
|
||||
if let Some(caps) = BORING_LINES_REGEX.captures(line) {
|
||||
if &caps[2] == "#" {
|
||||
result += &caps[1];
|
||||
result += &caps[2];
|
||||
result += &caps[3];
|
||||
result += "\n";
|
||||
continue;
|
||||
} else if &caps[2] != "!" && &caps[2] != "[" {
|
||||
result += "<span class=\"boring\">";
|
||||
result += &caps[1];
|
||||
if &caps[2] != " " {
|
||||
result += &caps[2];
|
||||
}
|
||||
result += &caps[3];
|
||||
result += "\n";
|
||||
result += "</span>";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result += line;
|
||||
result += "\n";
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn partition_source(s: &str) -> (String, String) {
|
||||
@@ -597,7 +879,7 @@ fn partition_source(s: &str) -> (String, String) {
|
||||
|
||||
for line in s.lines() {
|
||||
let trimline = line.trim();
|
||||
let header = trimline.chars().all(|c| c.is_whitespace()) || trimline.starts_with("#![");
|
||||
let header = trimline.chars().all(char::is_whitespace) || trimline.starts_with("#![");
|
||||
if !header || after_header {
|
||||
after_header = true;
|
||||
after.push_str(line);
|
||||
@@ -612,11 +894,12 @@ fn partition_source(s: &str) -> (String, String) {
|
||||
}
|
||||
|
||||
struct RenderItemContext<'a> {
|
||||
handlebars: &'a Handlebars,
|
||||
handlebars: &'a Handlebars<'a>,
|
||||
destination: PathBuf,
|
||||
data: serde_json::Map<String, serde_json::Value>,
|
||||
is_index: bool,
|
||||
html_config: HtmlConfig,
|
||||
edition: Option<RustEdition>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -628,27 +911,27 @@ mod tests {
|
||||
let inputs = vec![
|
||||
(
|
||||
"blah blah <h1>Foo</h1>",
|
||||
r##"blah blah <a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
|
||||
r##"blah blah <h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1>",
|
||||
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
|
||||
r##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
|
||||
),
|
||||
(
|
||||
"<h3>Foo^bar</h3>",
|
||||
r##"<a class="header" href="#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
|
||||
r##"<h3><a class="header" href="#foobar" id="foobar">Foo^bar</a></h3>"##,
|
||||
),
|
||||
(
|
||||
"<h4></h4>",
|
||||
r##"<a class="header" href="#" id=""><h4></h4></a>"##,
|
||||
r##"<h4><a class="header" href="#" id=""></a></h4>"##,
|
||||
),
|
||||
(
|
||||
"<h4><em>Hï</em></h4>",
|
||||
r##"<a class="header" href="#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
|
||||
r##"<h4><a class="header" href="#hï" id="hï"><em>Hï</em></a></h4>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1><h3>Foo</h3>",
|
||||
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a><a class="header" href="#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
|
||||
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>"##,
|
||||
),
|
||||
];
|
||||
|
||||
@@ -657,4 +940,83 @@ mod tests {
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_playground() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n<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\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
|
||||
"<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=\"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=\"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=\"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_playground_pre(
|
||||
src,
|
||||
&Playground {
|
||||
editable: true,
|
||||
..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\">\n<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\">\n<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod navigation;
|
||||
pub mod theme;
|
||||
pub mod toc;
|
||||
|
||||
@@ -2,9 +2,8 @@ use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
|
||||
use serde_json;
|
||||
|
||||
use utils;
|
||||
use crate::utils;
|
||||
|
||||
type StringMap = BTreeMap<String, String>;
|
||||
|
||||
@@ -18,13 +17,13 @@ impl Target {
|
||||
/// Returns target if found.
|
||||
fn find(
|
||||
&self,
|
||||
base_path: &String,
|
||||
current_path: &String,
|
||||
base_path: &str,
|
||||
current_path: &str,
|
||||
current_item: &StringMap,
|
||||
previous_item: &StringMap,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
match self {
|
||||
&Target::Next => {
|
||||
match *self {
|
||||
Target::Next => {
|
||||
let previous_path = previous_item
|
||||
.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
|
||||
@@ -34,7 +33,7 @@ impl Target {
|
||||
}
|
||||
}
|
||||
|
||||
&Target::Previous => {
|
||||
Target::Previous => {
|
||||
if current_path == base_path {
|
||||
return Ok(Some(previous_item.clone()));
|
||||
}
|
||||
@@ -47,22 +46,43 @@ impl Target {
|
||||
|
||||
fn find_chapter(
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
target: Target,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
debug!("Get data from context");
|
||||
|
||||
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
|
||||
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
|
||||
let base_path = rc
|
||||
.evaluate_absolute(ctx, "path", true)?
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
if !rc.evaluate(ctx, "@root/is_index")?.is_missing() {
|
||||
// Special case for index.md which may be a synthetic page.
|
||||
// Target::find won't match because there is no page with the path
|
||||
// "index.md" (unless there really is an index.md in SUMMARY.md).
|
||||
match target {
|
||||
Target::Previous => return Ok(None),
|
||||
Target::Next => match chapters
|
||||
.iter()
|
||||
.filter(|chapter| {
|
||||
// Skip things like "spacer"
|
||||
chapter.contains_key("path")
|
||||
})
|
||||
.nth(1)
|
||||
{
|
||||
Some(chapter) => return Ok(Some(chapter.clone())),
|
||||
None => return Ok(None),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let mut previous: Option<StringMap> = None;
|
||||
|
||||
debug!("Search for chapter");
|
||||
@@ -86,18 +106,19 @@ fn find_chapter(
|
||||
}
|
||||
|
||||
fn render(
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
_h: &Helper<'_, '_>,
|
||||
r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
out: &mut dyn Output,
|
||||
chapter: &StringMap,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("Creating BTreeMap to inject in context");
|
||||
|
||||
let mut context = BTreeMap::new();
|
||||
let base_path = rc
|
||||
.evaluate_absolute(ctx, "path", false)?
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
@@ -128,7 +149,7 @@ fn render(
|
||||
_h.template()
|
||||
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
||||
.and_then(|t| {
|
||||
let mut local_rc = rc.new_for_block();
|
||||
let mut local_rc = rc.clone();
|
||||
let local_ctx = Context::wraps(&context)?;
|
||||
t.render(r, &local_ctx, &mut local_rc, out)
|
||||
})?;
|
||||
@@ -137,11 +158,11 @@ fn render(
|
||||
}
|
||||
|
||||
pub fn previous(
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
_h: &Helper<'_, '_>,
|
||||
r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("previous (handlebars helper)");
|
||||
|
||||
@@ -153,11 +174,11 @@ pub fn previous(
|
||||
}
|
||||
|
||||
pub fn next(
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
_h: &Helper<'_, '_>,
|
||||
r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("next (handlebars helper)");
|
||||
|
||||
@@ -172,29 +193,29 @@ pub fn next(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
static TEMPLATE: &'static str =
|
||||
static TEMPLATE: &str =
|
||||
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
|
||||
|
||||
#[test]
|
||||
fn test_next_previous() {
|
||||
let data = json!({
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
@@ -209,23 +230,23 @@ mod tests {
|
||||
#[test]
|
||||
fn test_first() {
|
||||
let data = json!({
|
||||
"name": "one",
|
||||
"path": "one.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
"name": "one",
|
||||
"path": "one.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
@@ -239,23 +260,23 @@ mod tests {
|
||||
#[test]
|
||||
fn test_last() {
|
||||
let data = json!({
|
||||
"name": "three",
|
||||
"path": "three.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
"name": "three",
|
||||
"path": "three.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
|
||||
28
src/renderer/html_handlebars/helpers/theme.rs
Normal file
28
src/renderer/html_handlebars/helpers/theme.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
|
||||
|
||||
pub fn theme_option(
|
||||
h: &Helper<'_, '_>,
|
||||
_r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("theme_option (handlebars helper)");
|
||||
|
||||
let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
|
||||
RenderError::new("Param 0 with String type is required for theme_option helper.")
|
||||
})?;
|
||||
|
||||
let default_theme = rc.evaluate(ctx, "@root/default_theme")?;
|
||||
let default_theme_name = default_theme
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `default_theme`, string expected"))?;
|
||||
|
||||
out.write(param)?;
|
||||
if param.to_lowercase() == default_theme_name.to_lowercase() {
|
||||
out.write(" (default)")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use utils;
|
||||
use crate::utils;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
|
||||
use pulldown_cmark::{html, Event, Parser, Tag};
|
||||
use serde_json;
|
||||
use pulldown_cmark::{html, Event, Parser};
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -16,25 +15,45 @@ pub struct RenderToc {
|
||||
impl HelperDef for RenderToc {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
_h: &Helper,
|
||||
_: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
_h: &Helper<'reg, 'rc>,
|
||||
_r: &'reg Handlebars<'_>,
|
||||
ctx: &'rc Context,
|
||||
rc: &mut RenderContext<'reg, 'rc>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
|
||||
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.as_json().clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
let current = rc
|
||||
.evaluate_absolute(ctx, "path", true)?
|
||||
let current_path = rc
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
let current_section = rc
|
||||
.evaluate(ctx, "@root/section")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_default();
|
||||
|
||||
let fold_enable = rc
|
||||
.evaluate(ctx, "@root/fold_enable")?
|
||||
.as_json()
|
||||
.as_bool()
|
||||
.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_else(|| RenderError::new("Type error for `fold_level`, u64 expected"))?;
|
||||
|
||||
out.write("<ol class=\"chapter\">")?;
|
||||
|
||||
let mut current_level = 1;
|
||||
@@ -46,32 +65,46 @@ impl HelperDef for RenderToc {
|
||||
continue;
|
||||
}
|
||||
|
||||
let level = if let Some(s) = item.get("section") {
|
||||
s.matches('.').count()
|
||||
let (section, level) = if let Some(s) = item.get("section") {
|
||||
(s.as_str(), s.matches('.').count())
|
||||
} else {
|
||||
1
|
||||
("", 1)
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
out.write("<li>")?;
|
||||
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;
|
||||
}
|
||||
out.write("<li>")?;
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
} else {
|
||||
out.write("<li")?;
|
||||
if item.get("section").is_none() {
|
||||
out.write(" class=\"affix\"")?;
|
||||
}
|
||||
out.write(">")?;
|
||||
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(title)?;
|
||||
out.write("</li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Link
|
||||
@@ -87,11 +120,11 @@ impl HelperDef for RenderToc {
|
||||
.replace("\\", "/");
|
||||
|
||||
// Add link
|
||||
out.write(&utils::fs::path_to_root(¤t))?;
|
||||
out.write(&utils::fs::path_to_root(¤t_path))?;
|
||||
out.write(&tmp)?;
|
||||
out.write("\"")?;
|
||||
|
||||
if path == ¤t {
|
||||
if path == ¤t_path {
|
||||
out.write(" class=\"active\"")?;
|
||||
}
|
||||
|
||||
@@ -118,10 +151,7 @@ impl HelperDef for RenderToc {
|
||||
|
||||
// filter all events that are not inline code blocks
|
||||
let parser = Parser::new(name).filter(|event| match *event {
|
||||
Event::Start(Tag::Code)
|
||||
| Event::End(Tag::Code)
|
||||
| Event::InlineHtml(_)
|
||||
| Event::Text(_) => true,
|
||||
Event::Code(_) | Event::Html(_) | Event::Text(_) => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
@@ -137,6 +167,13 @@ impl HelperDef for RenderToc {
|
||||
out.write("</a>")?;
|
||||
}
|
||||
|
||||
// Render expand/collapse toggle
|
||||
if let Some(flag) = item.get("has_sub_items") {
|
||||
let has_sub_items = flag.parse::<bool>().unwrap_or_default();
|
||||
if fold_enable && has_sub_items {
|
||||
out.write("<a class=\"toggle\"><div>❱</div></a>")?;
|
||||
}
|
||||
}
|
||||
out.write("</li>")?;
|
||||
}
|
||||
while current_level > 1 {
|
||||
@@ -149,3 +186,19 @@ impl HelperDef for RenderToc {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn write_li_open_tag(
|
||||
out: &mut dyn Output,
|
||||
is_expanded: bool,
|
||||
is_affix: bool,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let mut li = String::from("<li class=\"chapter-item ");
|
||||
if is_expanded {
|
||||
li.push_str("expanded ");
|
||||
}
|
||||
if is_affix {
|
||||
li.push_str("affix ");
|
||||
}
|
||||
li.push_str("\">");
|
||||
out.write(&li)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
extern crate ammonia;
|
||||
extern crate elasticlunr;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use self::elasticlunr::Index;
|
||||
use elasticlunr::Index;
|
||||
use pulldown_cmark::*;
|
||||
use serde_json;
|
||||
|
||||
use book::{Book, BookItem};
|
||||
use config::Search;
|
||||
use errors::*;
|
||||
use theme::searcher;
|
||||
use utils;
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::config::Search;
|
||||
use crate::errors::*;
|
||||
use crate::theme::searcher;
|
||||
use crate::utils;
|
||||
|
||||
/// Creates all files required for search.
|
||||
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
|
||||
@@ -35,7 +31,7 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
"searchindex.js",
|
||||
format!("window.search = {};", index).as_bytes(),
|
||||
format!("Object.assign(window.search, {});", index).as_bytes(),
|
||||
)?;
|
||||
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
||||
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
||||
@@ -54,7 +50,7 @@ fn add_doc(
|
||||
section_id: &Option<String>,
|
||||
items: &[&str],
|
||||
) {
|
||||
let url = if let &Some(ref id) = section_id {
|
||||
let url = if let Some(ref id) = *section_id {
|
||||
Cow::Owned(format!("{}#{}", anchor_base, id))
|
||||
} else {
|
||||
Cow::Borrowed(anchor_base)
|
||||
@@ -74,35 +70,36 @@ fn render_item(
|
||||
doc_urls: &mut Vec<String>,
|
||||
item: &BookItem,
|
||||
) -> Result<()> {
|
||||
let chapter = match item {
|
||||
&BookItem::Chapter(ref ch) => ch,
|
||||
let chapter = match *item {
|
||||
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 opts = Options::empty();
|
||||
opts.insert(OPTION_ENABLE_TABLES);
|
||||
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
||||
let p = Parser::new_ext(&chapter.content, opts);
|
||||
let mut p = utils::new_cmark_parser(&chapter.content).peekable();
|
||||
|
||||
let mut in_header = false;
|
||||
let max_section_depth = search_config.heading_split_level as i32;
|
||||
let mut in_heading = false;
|
||||
let max_section_depth = u32::from(search_config.heading_split_level);
|
||||
let mut section_id = None;
|
||||
let mut heading = String::new();
|
||||
let mut body = String::new();
|
||||
let mut breadcrumbs = chapter.parent_names.clone();
|
||||
let mut footnote_numbers = HashMap::new();
|
||||
|
||||
for event in p {
|
||||
while let Some(event) = p.next() {
|
||||
match event {
|
||||
Event::Start(Tag::Header(i)) if i <= max_section_depth => {
|
||||
if heading.len() > 0 {
|
||||
// Section finished, the next header is following now
|
||||
Event::Start(Tag::Heading(i)) if i <= 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
|
||||
add_doc(
|
||||
index,
|
||||
@@ -117,10 +114,10 @@ fn render_item(
|
||||
breadcrumbs.pop();
|
||||
}
|
||||
|
||||
in_header = true;
|
||||
in_heading = true;
|
||||
}
|
||||
Event::End(Tag::Header(i)) if i <= max_section_depth => {
|
||||
in_header = false;
|
||||
Event::End(Tag::Heading(i)) if i <= max_section_depth => {
|
||||
in_heading = false;
|
||||
section_id = Some(utils::id_from_content(&heading));
|
||||
breadcrumbs.push(heading.clone());
|
||||
}
|
||||
@@ -128,34 +125,45 @@ fn render_item(
|
||||
let number = footnote_numbers.len() + 1;
|
||||
footnote_numbers.entry(name).or_insert(number);
|
||||
}
|
||||
Event::Start(_) | Event::End(_) | Event::SoftBreak | Event::HardBreak => {
|
||||
Event::Html(html) => {
|
||||
let mut html_block = html.into_string();
|
||||
|
||||
// As of pulldown_cmark 0.6, html events are no longer contained
|
||||
// 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);
|
||||
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
|
||||
// to ensure words don't get merged together
|
||||
if in_header {
|
||||
if in_heading {
|
||||
heading.push(' ');
|
||||
} else {
|
||||
body.push(' ');
|
||||
}
|
||||
}
|
||||
Event::Text(text) => {
|
||||
if in_header {
|
||||
Event::Text(text) | Event::Code(text) => {
|
||||
if in_heading {
|
||||
heading.push_str(&text);
|
||||
} else {
|
||||
body.push_str(&text);
|
||||
}
|
||||
}
|
||||
Event::Html(html) | Event::InlineHtml(html) => {
|
||||
body.push_str(&clean_html(&html));
|
||||
}
|
||||
Event::FootnoteReference(name) => {
|
||||
let len = footnote_numbers.len() + 1;
|
||||
let number = footnote_numbers.entry(name).or_insert(len);
|
||||
body.push_str(&format!(" [{}] ", number));
|
||||
}
|
||||
Event::TaskListMarker(_checked) => {}
|
||||
}
|
||||
}
|
||||
|
||||
if heading.len() > 0 {
|
||||
if !heading.is_empty() {
|
||||
// Make sure the last section is added to the index
|
||||
add_doc(
|
||||
index,
|
||||
@@ -170,7 +178,7 @@ fn render_item(
|
||||
}
|
||||
|
||||
fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
|
||||
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
|
||||
use elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
52
src/renderer/markdown_renderer.rs
Normal file
52
src/renderer/markdown_renderer.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use crate::book::BookItem;
|
||||
use crate::errors::*;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::utils;
|
||||
|
||||
use std::fs;
|
||||
|
||||
#[derive(Default)]
|
||||
/// A renderer to output the Markdown after the preprocessors have run. Mostly useful
|
||||
/// when debugging preprocessors.
|
||||
pub struct MarkdownRenderer;
|
||||
|
||||
impl MarkdownRenderer {
|
||||
/// Create a new `MarkdownRenderer` instance.
|
||||
pub fn new() -> Self {
|
||||
MarkdownRenderer
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for MarkdownRenderer {
|
||||
fn name(&self) -> &str {
|
||||
"markdown"
|
||||
}
|
||||
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||
let destination = &ctx.destination;
|
||||
let book = &ctx.book;
|
||||
|
||||
if destination.exists() {
|
||||
utils::fs::remove_dir_content(destination)
|
||||
.with_context(|| "Unable to remove stale Markdown output")?;
|
||||
}
|
||||
|
||||
trace!("markdown render");
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
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)
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -8,23 +8,25 @@
|
||||
//!
|
||||
//! The definition for [RenderContext] may be useful though.
|
||||
//!
|
||||
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/lib/index.html
|
||||
//! [For Developers]: https://rust-lang.github.io/mdBook/for_developers/index.html
|
||||
//! [RenderContext]: struct.RenderContext.html
|
||||
|
||||
pub use self::html_handlebars::HtmlHandlebars;
|
||||
pub use self::markdown_renderer::MarkdownRenderer;
|
||||
|
||||
mod html_handlebars;
|
||||
mod markdown_renderer;
|
||||
|
||||
use serde_json;
|
||||
use shlex::Shlex;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::io::{self, ErrorKind, Read};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use book::Book;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use toml::Value;
|
||||
|
||||
/// An arbitrary `mdbook` backend.
|
||||
///
|
||||
@@ -64,6 +66,7 @@ pub struct RenderContext {
|
||||
/// renderers to cache intermediate results, this directory is not
|
||||
/// guaranteed to be empty or even exist.
|
||||
pub destination: PathBuf,
|
||||
#[serde(skip)]
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
@@ -75,9 +78,9 @@ impl RenderContext {
|
||||
Q: Into<PathBuf>,
|
||||
{
|
||||
RenderContext {
|
||||
book: book,
|
||||
config: config,
|
||||
version: ::MDBOOK_VERSION.to_string(),
|
||||
book,
|
||||
config,
|
||||
version: crate::MDBOOK_VERSION.to_string(),
|
||||
root: root.into(),
|
||||
destination: destination.into(),
|
||||
__non_exhaustive: (),
|
||||
@@ -91,7 +94,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`")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +150,38 @@ 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 `{}` wasn't found, is the `{}` backend installed?",
|
||||
self.cmd, self.name
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error).with_context(|| "Unable to start the backend")?
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for CmdRenderer {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
@@ -166,34 +201,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);
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 348 KiB After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,8 +12,7 @@ Original by Dempfi (https://github.com/dempfi/ayu)
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote,
|
||||
.hljs-meta {
|
||||
.hljs-quote {
|
||||
color: #5c6773;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -30,6 +29,7 @@ Original by Dempfi (https://github.com/dempfi/ayu)
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
@@ -69,3 +69,11 @@ Original by Dempfi (https://github.com/dempfi/ayu)
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #91b362;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #d96c75;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -16,9 +16,6 @@ function playpen_text(playpen) {
|
||||
}
|
||||
|
||||
(function codeSnippets() {
|
||||
// Hide Rust code lines prepended with a specific character
|
||||
var hiding_character = "#";
|
||||
|
||||
function fetch_with_timeout(url, options, timeout = 6000) {
|
||||
return Promise.race([
|
||||
fetch(url, options),
|
||||
@@ -26,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",
|
||||
@@ -39,21 +36,30 @@ 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({
|
||||
name: "run",
|
||||
bindKey: {
|
||||
win: "Ctrl-Enter",
|
||||
mac: "Ctrl-Enter"
|
||||
},
|
||||
exec: _editor => run_rust_code(playground_block)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -71,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;
|
||||
@@ -100,12 +106,16 @@ 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";
|
||||
|
||||
var params = {
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
code: text
|
||||
code: text,
|
||||
edition: edition
|
||||
};
|
||||
|
||||
if (text.indexOf("#![feature") !== -1) {
|
||||
@@ -133,6 +143,11 @@ function playpen_text(playpen) {
|
||||
languages: [], // Languages used for auto-detection
|
||||
});
|
||||
|
||||
let code_nodes = Array
|
||||
.from(document.querySelectorAll('code'))
|
||||
// Don't highlight `inline code` blocks in headers.
|
||||
.filter(function (node) {return !node.parentElement.classList.contains("header"); });
|
||||
|
||||
if (window.ace) {
|
||||
// language-rust class needs to be removed for editable
|
||||
// blocks or highlightjs will capture events
|
||||
@@ -144,111 +159,71 @@ function playpen_text(playpen) {
|
||||
.from(document.querySelectorAll('code:not(.editable)'))
|
||||
.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
} else {
|
||||
Array
|
||||
.from(document.querySelectorAll('code'))
|
||||
.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
}
|
||||
|
||||
// Adding the hljs class gives code blocks the color css
|
||||
// even if highlighting doesn't apply
|
||||
Array
|
||||
.from(document.querySelectorAll('code'))
|
||||
.forEach(function (block) { block.classList.add('hljs'); });
|
||||
code_nodes.forEach(function (block) { block.classList.add('hljs'); });
|
||||
|
||||
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
|
||||
|
||||
var code_block = block;
|
||||
var pre_block = block.parentNode;
|
||||
// hide lines
|
||||
var lines = code_block.innerHTML.split("\n");
|
||||
var first_non_hidden_line = false;
|
||||
var lines_hidden = false;
|
||||
var trimmed_line = "";
|
||||
|
||||
for (var n = 0; n < lines.length; n++) {
|
||||
trimmed_line = lines[n].trim();
|
||||
if (trimmed_line[0] == hiding_character && trimmed_line[1] != hiding_character) {
|
||||
if (first_non_hidden_line) {
|
||||
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)# ?/, "$1") + "</span>";
|
||||
}
|
||||
else {
|
||||
lines[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)# ?/, "$1") + "\n" + "</span>";
|
||||
}
|
||||
lines_hidden = true;
|
||||
}
|
||||
else if (first_non_hidden_line) {
|
||||
lines[n] = "\n" + lines[n];
|
||||
}
|
||||
else {
|
||||
first_non_hidden_line = true;
|
||||
}
|
||||
if (trimmed_line[0] == hiding_character && trimmed_line[1] == hiding_character) {
|
||||
lines[n] = lines[n].replace("##", "#")
|
||||
}
|
||||
}
|
||||
code_block.innerHTML = lines.join("");
|
||||
|
||||
var lines = Array.from(block.querySelectorAll('.boring'));
|
||||
// If no lines were hidden, return
|
||||
if (!lines_hidden) { return; }
|
||||
if (!lines.length) { return; }
|
||||
block.classList.add("hide-boring");
|
||||
|
||||
var buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
buttons.innerHTML = "<button class=\"fa fa-expand\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
|
||||
|
||||
// add expand button
|
||||
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')) {
|
||||
var lines = pre_block.querySelectorAll('span.hidden');
|
||||
|
||||
e.target.classList.remove('fa-expand');
|
||||
e.target.classList.add('fa-compress');
|
||||
e.target.title = 'Hide lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
|
||||
Array.from(lines).forEach(function (line) {
|
||||
line.classList.remove('hidden');
|
||||
line.classList.add('unhidden');
|
||||
});
|
||||
block.classList.remove('hide-boring');
|
||||
} else if (e.target.classList.contains('fa-compress')) {
|
||||
var lines = pre_block.querySelectorAll('span.unhidden');
|
||||
|
||||
e.target.classList.remove('fa-compress');
|
||||
e.target.classList.add('fa-expand');
|
||||
e.target.title = 'Show hidden lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
|
||||
Array.from(lines).forEach(function (line) {
|
||||
line.classList.remove('unhidden');
|
||||
line.classList.add('hidden');
|
||||
});
|
||||
block.classList.add('hide-boring');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
|
||||
var pre_block = block.parentNode;
|
||||
if (!pre_block.classList.contains('playpen')) {
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
if (window.playground_copyable) {
|
||||
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
|
||||
var pre_block = block.parentNode;
|
||||
if (!pre_block.classList.contains('playground')) {
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
var clipButton = document.createElement('button');
|
||||
clipButton.className = 'fa fa-copy clip-button';
|
||||
clipButton.title = 'Copy to clipboard';
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
|
||||
|
||||
buttons.insertBefore(clipButton, buttons.firstChild);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var clipButton = document.createElement('button');
|
||||
clipButton.className = 'fa fa-copy clip-button';
|
||||
clipButton.title = 'Copy to clipboard';
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
|
||||
|
||||
buttons.insertBefore(clipButton, buttons.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
// Process playpen code blocks
|
||||
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) {
|
||||
@@ -263,19 +238,21 @@ function playpen_text(playpen) {
|
||||
runCodeButton.title = 'Run this code';
|
||||
runCodeButton.setAttribute('aria-label', runCodeButton.title);
|
||||
|
||||
var copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
|
||||
buttons.insertBefore(runCodeButton, buttons.firstChild);
|
||||
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
|
||||
|
||||
runCodeButton.addEventListener('click', function (e) {
|
||||
run_rust_code(pre_block);
|
||||
});
|
||||
|
||||
if (window.playground_copyable) {
|
||||
var copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
|
||||
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
|
||||
}
|
||||
|
||||
let code_block = pre_block.querySelector("code");
|
||||
if (window.ace && code_block.classList.contains("editable")) {
|
||||
var undoChangesButton = document.createElement('button');
|
||||
@@ -308,7 +285,7 @@ function playpen_text(playpen) {
|
||||
function showThemes() {
|
||||
themePopup.style.display = 'block';
|
||||
themeToggleButton.setAttribute('aria-expanded', true);
|
||||
themePopup.querySelector("button#" + document.body.className).focus();
|
||||
themePopup.querySelector("button#" + get_theme()).focus();
|
||||
}
|
||||
|
||||
function hideThemes() {
|
||||
@@ -317,7 +294,17 @@ function playpen_text(playpen) {
|
||||
themeToggleButton.focus();
|
||||
}
|
||||
|
||||
function set_theme(theme) {
|
||||
function get_theme() {
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
|
||||
if (theme === null || theme === undefined) {
|
||||
return default_theme;
|
||||
} else {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
||||
function set_theme(theme, store = true) {
|
||||
let ace_theme;
|
||||
|
||||
if (theme == 'coal' || theme == 'navy') {
|
||||
@@ -330,13 +317,11 @@ function playpen_text(playpen) {
|
||||
stylesheets.ayuHighlight.disabled = false;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = true;
|
||||
|
||||
ace_theme = "ace/theme/tomorrow_night";
|
||||
} else {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = false;
|
||||
|
||||
ace_theme = "ace/theme/dawn";
|
||||
}
|
||||
|
||||
@@ -350,23 +335,20 @@ function playpen_text(playpen) {
|
||||
});
|
||||
}
|
||||
|
||||
var previousTheme;
|
||||
try { previousTheme = localStorage.getItem('mdbook-theme'); } catch (e) { }
|
||||
if (previousTheme === null || previousTheme === undefined) { previousTheme = 'light'; }
|
||||
var previousTheme = get_theme();
|
||||
|
||||
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
|
||||
if (store) {
|
||||
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
|
||||
}
|
||||
|
||||
document.body.className = theme;
|
||||
html.classList.remove(previousTheme);
|
||||
html.classList.add(theme);
|
||||
}
|
||||
|
||||
// Set theme
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||
var theme = get_theme();
|
||||
|
||||
set_theme(theme);
|
||||
set_theme(theme, false);
|
||||
|
||||
themeToggleButton.addEventListener('click', function () {
|
||||
if (themePopup.style.display === 'block') {
|
||||
@@ -388,7 +370,7 @@ function playpen_text(playpen) {
|
||||
}
|
||||
});
|
||||
|
||||
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang-nursery/mdBook/issues/628
|
||||
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628
|
||||
document.addEventListener('click', function(e) {
|
||||
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
|
||||
hideThemes();
|
||||
@@ -435,6 +417,7 @@ function playpen_text(playpen) {
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
var sidebarToggleButton = document.getElementById("sidebar-toggle");
|
||||
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
|
||||
var firstContact = null;
|
||||
|
||||
function showSidebar() {
|
||||
@@ -448,6 +431,17 @@ function playpen_text(playpen) {
|
||||
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
|
||||
}
|
||||
|
||||
|
||||
var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
|
||||
|
||||
function toggleSection(ev) {
|
||||
ev.currentTarget.parentElement.classList.toggle('expanded');
|
||||
}
|
||||
|
||||
Array.from(sidebarAnchorToggles).forEach(function (el) {
|
||||
el.addEventListener('click', toggleSection);
|
||||
});
|
||||
|
||||
function hideSidebar() {
|
||||
html.classList.remove('sidebar-visible')
|
||||
html.classList.add('sidebar-hidden');
|
||||
@@ -462,6 +456,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();
|
||||
@@ -474,6 +473,32 @@ function playpen_text(playpen) {
|
||||
}
|
||||
});
|
||||
|
||||
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
|
||||
|
||||
function initResize(e) {
|
||||
window.addEventListener('mousemove', resize, false);
|
||||
window.addEventListener('mouseup', stopResize, false);
|
||||
html.classList.add('sidebar-resizing');
|
||||
}
|
||||
function resize(e) {
|
||||
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) {
|
||||
html.classList.remove('sidebar-resizing');
|
||||
window.removeEventListener('mousemove', resize, false);
|
||||
window.removeEventListener('mouseup', stopResize, false);
|
||||
}
|
||||
|
||||
document.addEventListener('touchstart', function (e) {
|
||||
firstContact = {
|
||||
x: e.touches[0].clientX,
|
||||
@@ -500,9 +525,10 @@ function playpen_text(playpen) {
|
||||
}, { passive: true });
|
||||
|
||||
// Scroll sidebar to current active section
|
||||
var activeSection = sidebar.querySelector(".active");
|
||||
var activeSection = document.getElementById("sidebar").querySelector(".active");
|
||||
if (activeSection) {
|
||||
sidebar.scrollTop = activeSection.offsetTop;
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -543,11 +569,11 @@ function playpen_text(playpen) {
|
||||
elem.className = 'fa fa-copy tooltipped';
|
||||
}
|
||||
|
||||
var clipboardSnippets = new Clipboard('.clip-button', {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -575,26 +601,60 @@ function playpen_text(playpen) {
|
||||
});
|
||||
})();
|
||||
|
||||
(function autoHideMenu() {
|
||||
(function controllMenu() {
|
||||
var menu = document.getElementById('menu-bar');
|
||||
|
||||
var previousScrollTop = document.scrollingElement.scrollTop;
|
||||
|
||||
document.addEventListener('scroll', function () {
|
||||
if (menu.classList.contains('folded') && document.scrollingElement.scrollTop < previousScrollTop) {
|
||||
menu.classList.remove('folded');
|
||||
} else if (!menu.classList.contains('folded') && document.scrollingElement.scrollTop > previousScrollTop) {
|
||||
menu.classList.add('folded');
|
||||
}
|
||||
|
||||
if (!menu.classList.contains('bordered') && document.scrollingElement.scrollTop > 0) {
|
||||
menu.classList.add('bordered');
|
||||
}
|
||||
|
||||
if (menu.classList.contains('bordered') && document.scrollingElement.scrollTop === 0) {
|
||||
menu.classList.remove('bordered');
|
||||
}
|
||||
|
||||
previousScrollTop = document.scrollingElement.scrollTop;
|
||||
}, { passive: true });
|
||||
(function controllPosition() {
|
||||
var scrollTop = document.scrollingElement.scrollTop;
|
||||
var prevScrollTop = scrollTop;
|
||||
var minMenuY = -menu.clientHeight - 50;
|
||||
// When the script loads, the page can be at any scroll (e.g. if you reforesh it).
|
||||
menu.style.top = scrollTop + 'px';
|
||||
// Same as parseInt(menu.style.top.slice(0, -2), but faster
|
||||
var topCache = menu.style.top.slice(0, -2);
|
||||
menu.classList.remove('sticky');
|
||||
var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
|
||||
document.addEventListener('scroll', function () {
|
||||
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
|
||||
// `null` means that it doesn't need to be updated
|
||||
var nextSticky = null;
|
||||
var nextTop = null;
|
||||
var scrollDown = scrollTop > prevScrollTop;
|
||||
var menuPosAbsoluteY = topCache - scrollTop;
|
||||
if (scrollDown) {
|
||||
nextSticky = false;
|
||||
if (menuPosAbsoluteY > 0) {
|
||||
nextTop = prevScrollTop;
|
||||
}
|
||||
} else {
|
||||
if (menuPosAbsoluteY > 0) {
|
||||
nextSticky = true;
|
||||
} else if (menuPosAbsoluteY < minMenuY) {
|
||||
nextTop = prevScrollTop + minMenuY;
|
||||
}
|
||||
}
|
||||
if (nextSticky === true && stickyCache === false) {
|
||||
menu.classList.add('sticky');
|
||||
stickyCache = true;
|
||||
} else if (nextSticky === false && stickyCache === true) {
|
||||
menu.classList.remove('sticky');
|
||||
stickyCache = false;
|
||||
}
|
||||
if (nextTop !== null) {
|
||||
menu.style.top = nextTop + 'px';
|
||||
topCache = nextTop;
|
||||
}
|
||||
prevScrollTop = scrollTop;
|
||||
}, { passive: true });
|
||||
})();
|
||||
(function controllBorder() {
|
||||
menu.classList.remove('bordered');
|
||||
document.addEventListener('scroll', function () {
|
||||
if (menu.offsetTop === 0) {
|
||||
menu.classList.remove('bordered');
|
||||
} else {
|
||||
menu.classList.add('bordered');
|
||||
}
|
||||
}, { passive: true });
|
||||
})();
|
||||
})();
|
||||
|
||||
6
src/theme/clipboard.min.js
vendored
6
src/theme/clipboard.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -8,7 +8,9 @@
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar);
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-color: var(--scrollbar) var(--bg);
|
||||
}
|
||||
#searchresults a,
|
||||
.content a:link,
|
||||
a:visited,
|
||||
@@ -18,14 +20,13 @@ a > .hljs {
|
||||
|
||||
/* Menu Bar */
|
||||
|
||||
#menu-bar {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
#menu-bar,
|
||||
#menu-bar-hover-placeholder {
|
||||
z-index: 101;
|
||||
margin: auto calc(0px - var(--page-padding));
|
||||
}
|
||||
#menu-bar > #menu-bar-sticky-container {
|
||||
#menu-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: var(--bg);
|
||||
@@ -33,17 +34,28 @@ a > .hljs {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
.js #menu-bar > #menu-bar-sticky-container {
|
||||
transition: transform 0.3s;
|
||||
#menu-bar.sticky,
|
||||
.js #menu-bar-hover-placeholder:hover + #menu-bar,
|
||||
.js #menu-bar:hover,
|
||||
.js.sidebar-visible #menu-bar {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0 !important;
|
||||
}
|
||||
#menu-bar.bordered > #menu-bar-sticky-container {
|
||||
#menu-bar-hover-placeholder {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
height: var(--menu-bar-height);
|
||||
}
|
||||
#menu-bar.bordered {
|
||||
border-bottom-color: var(--table-border-color);
|
||||
}
|
||||
#menu-bar i, #menu-bar .icon-button {
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
z-index: 10;
|
||||
line-height: 50px;
|
||||
line-height: var(--menu-bar-height);
|
||||
cursor: pointer;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
@@ -63,27 +75,26 @@ a > .hljs {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#print-button {
|
||||
.right-buttons {
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
|
||||
transform: translateY(-60px);
|
||||
.right-buttons a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.left-buttons {
|
||||
display: flex;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.no-js .left-buttons {
|
||||
.no-js .left-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
display: inline-block;
|
||||
font-weight: 200;
|
||||
font-size: 20px;
|
||||
line-height: 50px;
|
||||
font-size: 2rem;
|
||||
line-height: var(--menu-bar-height);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
@@ -121,7 +132,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||
text-decoration: none;
|
||||
|
||||
position: fixed;
|
||||
top: 50px; /* Height of menu-bar */
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
max-width: 150px;
|
||||
@@ -132,17 +143,21 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||
align-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
transition: color 0.5s;
|
||||
transition: color 0.5s, background-color 0.5s;
|
||||
}
|
||||
|
||||
.nav-chapters:hover { text-decoration: none; }
|
||||
.nav-chapters:hover {
|
||||
text-decoration: none;
|
||||
background-color: var(--theme-hover);
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
margin-top: 50px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-nav-chapters {
|
||||
.mobile-nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
@@ -173,11 +188,14 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||
/* Inline code */
|
||||
|
||||
:not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
display: inline;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:not(pre):not(a) > .hljs {
|
||||
color: var(--inline-code-color);
|
||||
overflow-x: initial;
|
||||
}
|
||||
|
||||
a:hover > .hljs {
|
||||
@@ -298,8 +316,6 @@ ul#searchresults span.teaser em {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--sidebar-width);
|
||||
overflow-y: auto;
|
||||
padding: 10px 10px;
|
||||
font-size: 0.875em;
|
||||
box-sizing: border-box;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -307,12 +323,39 @@ ul#searchresults span.teaser em {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.js .sidebar {
|
||||
.sidebar-resizing {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.js:not(.sidebar-resizing) .sidebar {
|
||||
transition: transform 0.3s; /* Animation: slide away */
|
||||
}
|
||||
.sidebar code {
|
||||
line-height: 2em;
|
||||
}
|
||||
.sidebar .sidebar-scrollbox {
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
.sidebar .sidebar-resize-handle {
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
width: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.js .sidebar .sidebar-resize-handle {
|
||||
cursor: col-resize;
|
||||
width: 5px;
|
||||
}
|
||||
.sidebar-hidden .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width)));
|
||||
}
|
||||
@@ -338,22 +381,57 @@ ul#searchresults span.teaser em {
|
||||
padding-left: 0;
|
||||
line-height: 2.2em;
|
||||
}
|
||||
|
||||
.chapter ol {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chapter li {
|
||||
display: flex;
|
||||
color: var(--sidebar-non-existant);
|
||||
}
|
||||
.chapter li a {
|
||||
color: var(--sidebar-fg);
|
||||
display: block;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.chapter li a:hover { text-decoration: none }
|
||||
.chapter li .active,
|
||||
a:hover {
|
||||
/* Animate color change */
|
||||
|
||||
.chapter li a:hover {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.chapter li a.active {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.chapter li > a.toggle {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
padding: 0 10px;
|
||||
user-select: none;
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.chapter li > a.toggle div {
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
/* collapse the section */
|
||||
.chapter li:not(.expanded) + li > ol {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chapter li.chapter-item {
|
||||
line-height: 1.5em;
|
||||
margin-top: 0.6em;
|
||||
}
|
||||
|
||||
.chapter li.expanded > a.toggle div {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
@@ -363,7 +441,7 @@ a:hover {
|
||||
background-color: var(--sidebar-spacer);
|
||||
}
|
||||
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
.chapter li a { padding: 5px 0; }
|
||||
.spacer { margin: 10px 0; }
|
||||
}
|
||||
@@ -379,7 +457,7 @@ a:hover {
|
||||
.theme-popup {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50px;
|
||||
top: var(--menu-bar-height);
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7em;
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
@import 'variables.css';
|
||||
|
||||
:root {
|
||||
/* Browser default font-size is 16px, this way 1 rem = 10px */
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
color: var(--fg);
|
||||
@@ -11,47 +16,61 @@ html {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-size: 1.6rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important;
|
||||
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
|
||||
}
|
||||
|
||||
/* Don't change font size in headers. */
|
||||
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
.left { float: left; }
|
||||
.right { float: right; }
|
||||
.hidden { display: none; }
|
||||
.play-button.hidden { display: none; }
|
||||
.boring { opacity: 0.6; }
|
||||
.hide-boring .boring { display: none; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
h2, h3 { margin-top: 2.5em; }
|
||||
h4, h5 { margin-top: 2em; }
|
||||
|
||||
.header + .header h3,
|
||||
.header + .header h4,
|
||||
.header + .header h5 {
|
||||
.header + .header h5 {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
a.header:target h1:before,
|
||||
a.header:target h2:before,
|
||||
a.header:target h3:before,
|
||||
a.header:target h4:before {
|
||||
h1 a.header:target::before,
|
||||
h2 a.header:target::before,
|
||||
h3 a.header:target::before,
|
||||
h4 a.header: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 {
|
||||
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
|
||||
}
|
||||
|
||||
.page {
|
||||
outline: 0;
|
||||
padding: 0 var(--page-padding);
|
||||
margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
|
||||
}
|
||||
.page-wrapper {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.js .page-wrapper {
|
||||
.js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
|
||||
@@ -65,6 +84,9 @@ a.header:target h4:before {
|
||||
margin-right: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
.content p { line-height: 1.45em; }
|
||||
.content ol { line-height: 1.45em; }
|
||||
.content ul { line-height: 1.45em; }
|
||||
.content a { text-decoration: none; }
|
||||
.content a:hover { text-decoration: underline; }
|
||||
.content img { max-width: 100%; }
|
||||
@@ -92,6 +114,9 @@ table thead td {
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
table thead th {
|
||||
padding: 3px 20px;
|
||||
}
|
||||
table thead tr {
|
||||
border: 1px var(--table-header-bg) solid;
|
||||
}
|
||||
@@ -141,4 +166,9 @@ blockquote {
|
||||
.tooltipped .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
||||
.chapter li.part-title {
|
||||
color: var(--sidebar-fg);
|
||||
margin: 5px 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
--sidebar-width: 300px;
|
||||
--page-padding: 15px;
|
||||
--content-max-width: 750px;
|
||||
--menu-bar-height: 50px;
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
@@ -208,3 +209,45 @@
|
||||
--searchresults-li-bg: #dec2a2;
|
||||
--search-mark-bg: #e69f67;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.light.no-js {
|
||||
--bg: hsl(200, 7%, 8%);
|
||||
--fg: #98a3ad;
|
||||
|
||||
--sidebar-bg: #292c2f;
|
||||
--sidebar-fg: #a1adb8;
|
||||
--sidebar-non-existant: #505254;
|
||||
--sidebar-active: #3473ad;
|
||||
--sidebar-spacer: #393939;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #43484d;
|
||||
--icons-hover: #b3c0cc;
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;;
|
||||
|
||||
--theme-popup-bg: #141617;
|
||||
--theme-popup-border: #43484d;
|
||||
--theme-hover: #1f2124;
|
||||
|
||||
--quote-bg: hsl(234, 21%, 18%);
|
||||
--quote-border: hsl(234, 21%, 23%);
|
||||
|
||||
--table-border-color: hsl(200, 7%, 13%);
|
||||
--table-header-bg: hsl(200, 7%, 28%);
|
||||
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||
|
||||
--searchbar-border-color: #aaa;
|
||||
--searchbar-bg: #b7b7b7;
|
||||
--searchbar-fg: #000;
|
||||
--searchbar-shadow-color: #aaa;
|
||||
--searchresults-header-fg: #666;
|
||||
--searchresults-border-color: #98a3ad;
|
||||
--searchresults-li-bg: #2b2b2f;
|
||||
--search-mark-bg: #355c7d;
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
101
src/theme/fonts/fonts.css
Normal file
101
src/theme/fonts/fonts.css
Normal file
@@ -0,0 +1,101 @@
|
||||
/* 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: local('Source Code Pro Medium'), local('SourceCodePro-Medium'),
|
||||
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.
BIN
src/theme/fonts/open-sans-v17-all-charsets-800.woff2
Normal file
BIN
src/theme/fonts/open-sans-v17-all-charsets-800.woff2
Normal file
Binary file not shown.
BIN
src/theme/fonts/open-sans-v17-all-charsets-800italic.woff2
Normal file
BIN
src/theme/fonts/open-sans-v17-all-charsets-800italic.woff2
Normal file
Binary file not shown.
BIN
src/theme/fonts/open-sans-v17-all-charsets-italic.woff2
Normal file
BIN
src/theme/fonts/open-sans-v17-all-charsets-italic.woff2
Normal file
Binary file not shown.
BIN
src/theme/fonts/open-sans-v17-all-charsets-regular.woff2
Normal file
BIN
src/theme/fonts/open-sans-v17-all-charsets-regular.woff2
Normal file
Binary file not shown.
BIN
src/theme/fonts/source-code-pro-v11-all-charsets-500.woff2
Normal file
BIN
src/theme/fonts/source-code-pro-v11-all-charsets-500.woff2
Normal file
Binary file not shown.
1
src/theme/head.hbs
Normal file
1
src/theme/head.hbs
Normal file
@@ -0,0 +1 @@
|
||||
{{!-- Put your head HTML text here --}}
|
||||
@@ -67,3 +67,13 @@
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #22863a;
|
||||
background-color: #f0fff4;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #b31d28;
|
||||
background-color: #ffeef0;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,15 +1,27 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}" class="sidebar-visible no-js">
|
||||
<html lang="{{ language }}" class="sidebar-visible no-js {{ default_theme }}">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
{{#if is_print }}
|
||||
<meta name="robots" content="noindex" />
|
||||
{{/if}}
|
||||
{{#if base_url}}
|
||||
<base href="{{ base_url }}">
|
||||
{{/if}}
|
||||
|
||||
|
||||
<!-- Custom HTML head -->
|
||||
{{> head}}
|
||||
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="description" content="{{ description }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
<link rel="shortcut icon" href="{{ path_to_root }}{{ favicon }}">
|
||||
<link rel="icon" href="{{ path_to_root }}favicon.svg">
|
||||
<link rel="shortcut icon" href="{{ path_to_root }}favicon.png">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
|
||||
@@ -17,8 +29,9 @@
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
|
||||
{{#if copy_fonts}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
|
||||
{{/if}}
|
||||
|
||||
<!-- Highlight.js Stylesheets -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}highlight.css">
|
||||
@@ -35,9 +48,12 @@
|
||||
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{{/if}}
|
||||
</head>
|
||||
<body class="light">
|
||||
<body>
|
||||
<!-- Provide site root to javascript -->
|
||||
<script type="text/javascript">var path_to_root = "{{ path_to_root }}";</script>
|
||||
<script type="text/javascript">
|
||||
var path_to_root = "{{ path_to_root }}";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
|
||||
</script>
|
||||
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script type="text/javascript">
|
||||
@@ -58,10 +74,13 @@
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
<script type="text/javascript">
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||
document.body.className = theme;
|
||||
document.querySelector('html').className = theme + ' js';
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||
var html = document.querySelector('html');
|
||||
html.classList.remove('no-js')
|
||||
html.classList.remove('{{ default_theme }}')
|
||||
html.classList.add(theme);
|
||||
html.classList.add('js');
|
||||
</script>
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
@@ -77,43 +96,50 @@
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
{{#toc}}{{/toc}}
|
||||
<div class="sidebar-scrollbox">
|
||||
{{#toc}}{{/toc}}
|
||||
</div>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||
</nav>
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page">
|
||||
{{> header}}
|
||||
<div id="menu-bar" class="menu-bar">
|
||||
<div id="menu-bar-sticky-container">
|
||||
<div class="left-buttons">
|
||||
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
</button>
|
||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="light">Light <span class="default">(default)</span></button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||||
</ul>
|
||||
{{#if search_enabled}}
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div id="menu-bar-hover-placeholder"></div>
|
||||
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||
<div class="left-buttons">
|
||||
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
</button>
|
||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="light">{{ theme_option "Light" }}</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="rust">{{ theme_option "Rust" }}</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="coal">{{ theme_option "Coal" }}</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="navy">{{ theme_option "Navy" }}</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="ayu">{{ theme_option "Ayu" }}</button></li>
|
||||
</ul>
|
||||
{{#if search_enabled}}
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
|
||||
<div class="right-buttons">
|
||||
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
|
||||
<i id="print-button" class="fa fa-print"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-buttons">
|
||||
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
|
||||
<i id="print-button" class="fa fa-print"></i>
|
||||
</a>
|
||||
{{#if git_repository_url}}
|
||||
<a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
|
||||
<i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -165,13 +191,13 @@
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
{{#previous}}
|
||||
<a href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<a rel="prev" href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
|
||||
{{#next}}
|
||||
<a href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<a rel="next" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
{{/next}}
|
||||
@@ -186,7 +212,7 @@
|
||||
socket.onmessage = function (event) {
|
||||
if (event.data === "reload") {
|
||||
socket.close();
|
||||
location.reload(true); // force reload from server (not from cache)
|
||||
location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -215,7 +241,19 @@
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playpen_js}}
|
||||
{{#if playground_line_numbers}}
|
||||
<script type="text/javascript">
|
||||
window.playground_line_numbers = true;
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_copyable}}
|
||||
<script type="text/javascript">
|
||||
window.playground_copyable = true;
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_js}}
|
||||
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script>
|
||||
|
||||
121
src/theme/mod.rs
121
src/theme/mod.rs
@@ -1,6 +1,8 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
pub mod playpen_editor;
|
||||
pub mod playground_editor;
|
||||
|
||||
pub mod fonts;
|
||||
|
||||
#[cfg(feature = "search")]
|
||||
pub mod searcher;
|
||||
@@ -9,33 +11,32 @@ use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
||||
use errors::*;
|
||||
use crate::errors::*;
|
||||
|
||||
pub static INDEX: &'static [u8] = include_bytes!("index.hbs");
|
||||
pub static HEADER: &'static [u8] = include_bytes!("header.hbs");
|
||||
pub static CHROME_CSS: &'static [u8] = include_bytes!("css/chrome.css");
|
||||
pub static GENERAL_CSS: &'static [u8] = include_bytes!("css/general.css");
|
||||
pub static PRINT_CSS: &'static [u8] = include_bytes!("css/print.css");
|
||||
pub static VARIABLES_CSS: &'static [u8] = include_bytes!("css/variables.css");
|
||||
pub static FAVICON: &'static [u8] = include_bytes!("favicon.png");
|
||||
pub static JS: &'static [u8] = include_bytes!("book.js");
|
||||
pub static HIGHLIGHT_JS: &'static [u8] = include_bytes!("highlight.js");
|
||||
pub static TOMORROW_NIGHT_CSS: &'static [u8] = include_bytes!("tomorrow-night.css");
|
||||
pub static HIGHLIGHT_CSS: &'static [u8] = include_bytes!("highlight.css");
|
||||
pub static AYU_HIGHLIGHT_CSS: &'static [u8] = include_bytes!("ayu-highlight.css");
|
||||
pub static CLIPBOARD_JS: &'static [u8] = include_bytes!("clipboard.min.js");
|
||||
pub static FONT_AWESOME: &'static [u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
|
||||
pub static FONT_AWESOME_EOT: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
|
||||
pub static FONT_AWESOME_SVG: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
|
||||
pub static FONT_AWESOME_WOFF2: &'static [u8] =
|
||||
pub static INDEX: &[u8] = include_bytes!("index.hbs");
|
||||
pub static HEAD: &[u8] = include_bytes!("head.hbs");
|
||||
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
|
||||
pub static HEADER: &[u8] = include_bytes!("header.hbs");
|
||||
pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
|
||||
pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
|
||||
pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
|
||||
pub static VARIABLES_CSS: &[u8] = include_bytes!("css/variables.css");
|
||||
pub static FAVICON_PNG: &[u8] = include_bytes!("favicon.png");
|
||||
pub static FAVICON_SVG: &[u8] = include_bytes!("favicon.svg");
|
||||
pub static JS: &[u8] = include_bytes!("book.js");
|
||||
pub static HIGHLIGHT_JS: &[u8] = include_bytes!("highlight.js");
|
||||
pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("tomorrow-night.css");
|
||||
pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("highlight.css");
|
||||
pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("ayu-highlight.css");
|
||||
pub static CLIPBOARD_JS: &[u8] = include_bytes!("clipboard.min.js");
|
||||
pub static FONT_AWESOME: &[u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
|
||||
pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
|
||||
pub static FONT_AWESOME_SVG: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
|
||||
pub static FONT_AWESOME_WOFF2: &[u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff2");
|
||||
pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
|
||||
pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
|
||||
|
||||
/// The `Theme` struct should be used instead of the static variables because
|
||||
/// the `new()` method will look if the user has a theme directory in their
|
||||
@@ -46,12 +47,15 @@ pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("FontAwesome/fonts/F
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Theme {
|
||||
pub index: Vec<u8>,
|
||||
pub head: Vec<u8>,
|
||||
pub redirect: Vec<u8>,
|
||||
pub header: Vec<u8>,
|
||||
pub chrome_css: Vec<u8>,
|
||||
pub general_css: Vec<u8>,
|
||||
pub print_css: Vec<u8>,
|
||||
pub variables_css: Vec<u8>,
|
||||
pub favicon: Vec<u8>,
|
||||
pub favicon_png: Option<Vec<u8>>,
|
||||
pub favicon_svg: Option<Vec<u8>>,
|
||||
pub js: Vec<u8>,
|
||||
pub highlight_css: Vec<u8>,
|
||||
pub tomorrow_night_css: Vec<u8>,
|
||||
@@ -76,6 +80,8 @@ impl Theme {
|
||||
{
|
||||
let files = vec![
|
||||
(theme_dir.join("index.hbs"), &mut theme.index),
|
||||
(theme_dir.join("head.hbs"), &mut theme.head),
|
||||
(theme_dir.join("redirect.hbs"), &mut theme.redirect),
|
||||
(theme_dir.join("header.hbs"), &mut theme.header),
|
||||
(theme_dir.join("book.js"), &mut theme.js),
|
||||
(theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
|
||||
@@ -85,7 +91,6 @@ impl Theme {
|
||||
theme_dir.join("css/variables.css"),
|
||||
&mut theme.variables_css,
|
||||
),
|
||||
(theme_dir.join("favicon.png"), &mut theme.favicon),
|
||||
(theme_dir.join("highlight.js"), &mut theme.highlight_js),
|
||||
(theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
|
||||
(theme_dir.join("highlight.css"), &mut theme.highlight_css),
|
||||
@@ -99,13 +104,36 @@ impl Theme {
|
||||
),
|
||||
];
|
||||
|
||||
for (filename, dest) in files {
|
||||
let load_with_warn = |filename: &Path, dest| {
|
||||
if !filename.exists() {
|
||||
continue;
|
||||
// Don't warn if the file doesn't exist.
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Err(e) = load_file_contents(&filename, dest) {
|
||||
if let Err(e) = load_file_contents(filename, dest) {
|
||||
warn!("Couldn't load custom file, {}: {}", filename.display(), e);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
for (filename, dest) in files {
|
||||
load_with_warn(&filename, dest);
|
||||
}
|
||||
|
||||
// If the user overrides one favicon, but not the other, do not
|
||||
// copy the default for the other.
|
||||
let favicon_png = &mut theme.favicon_png.as_mut().unwrap();
|
||||
let png = load_with_warn(&theme_dir.join("favicon.png"), favicon_png);
|
||||
let favicon_svg = &mut theme.favicon_svg.as_mut().unwrap();
|
||||
let svg = load_with_warn(&theme_dir.join("favicon.svg"), favicon_svg);
|
||||
match (png, svg) {
|
||||
(true, true) | (false, false) => {}
|
||||
(true, false) => {
|
||||
theme.favicon_svg = None;
|
||||
}
|
||||
(false, true) => {
|
||||
theme.favicon_png = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,12 +146,15 @@ impl Default for Theme {
|
||||
fn default() -> Theme {
|
||||
Theme {
|
||||
index: INDEX.to_owned(),
|
||||
head: HEAD.to_owned(),
|
||||
redirect: REDIRECT.to_owned(),
|
||||
header: HEADER.to_owned(),
|
||||
chrome_css: CHROME_CSS.to_owned(),
|
||||
general_css: GENERAL_CSS.to_owned(),
|
||||
print_css: PRINT_CSS.to_owned(),
|
||||
variables_css: VARIABLES_CSS.to_owned(),
|
||||
favicon: FAVICON.to_owned(),
|
||||
favicon_png: Some(FAVICON_PNG.to_owned()),
|
||||
favicon_svg: Some(FAVICON_SVG.to_owned()),
|
||||
js: JS.to_owned(),
|
||||
highlight_css: HIGHLIGHT_CSS.to_owned(),
|
||||
tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
|
||||
@@ -172,9 +203,13 @@ mod tests {
|
||||
fn theme_dir_overrides_defaults() {
|
||||
let files = [
|
||||
"index.hbs",
|
||||
"head.hbs",
|
||||
"redirect.hbs",
|
||||
"header.hbs",
|
||||
"favicon.png",
|
||||
"favicon.svg",
|
||||
"css/chrome.css",
|
||||
"css/fonts.css",
|
||||
"css/general.css",
|
||||
"css/print.css",
|
||||
"css/variables.css",
|
||||
@@ -198,12 +233,15 @@ mod tests {
|
||||
|
||||
let empty = Theme {
|
||||
index: Vec::new(),
|
||||
head: Vec::new(),
|
||||
redirect: Vec::new(),
|
||||
header: Vec::new(),
|
||||
chrome_css: Vec::new(),
|
||||
general_css: Vec::new(),
|
||||
print_css: Vec::new(),
|
||||
variables_css: Vec::new(),
|
||||
favicon: Vec::new(),
|
||||
favicon_png: Some(Vec::new()),
|
||||
favicon_svg: Some(Vec::new()),
|
||||
js: Vec::new(),
|
||||
highlight_css: Vec::new(),
|
||||
tomorrow_night_css: Vec::new(),
|
||||
@@ -214,4 +252,19 @@ mod tests {
|
||||
|
||||
assert_eq!(got, empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn favicon_override() {
|
||||
let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
|
||||
fs::write(temp.path().join("favicon.png"), "1234").unwrap();
|
||||
let got = Theme::new(temp.path());
|
||||
assert_eq!(got.favicon_png.as_ref().unwrap(), b"1234");
|
||||
assert_eq!(got.favicon_svg, None);
|
||||
|
||||
let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
|
||||
fs::write(temp.path().join("favicon.svg"), "4567").unwrap();
|
||||
let got = Theme::new(temp.path());
|
||||
assert_eq!(got.favicon_png, None);
|
||||
assert_eq!(got.favicon_svg.as_ref().unwrap(), b"4567");
|
||||
}
|
||||
}
|
||||
|
||||
43
src/theme/playground_editor/ace.js
Normal file
43
src/theme/playground_editor/ace.js
Normal file
File diff suppressed because one or more lines are too long
@@ -6,14 +6,16 @@ window.editors = [];
|
||||
}
|
||||
|
||||
Array.from(document.querySelectorAll('.editable')).forEach(function(editable) {
|
||||
let display_line_numbers = window.playground_line_numbers || false;
|
||||
|
||||
let editor = ace.edit(editable);
|
||||
editor.setOptions({
|
||||
highlightActiveLine: false,
|
||||
showPrintMargin: false,
|
||||
showLineNumbers: false,
|
||||
showGutter: false,
|
||||
showLineNumbers: display_line_numbers,
|
||||
showGutter: display_line_numbers,
|
||||
maxLines: Infinity,
|
||||
fontSize: "0.875em" // please adjust the font size of the code in general.styl
|
||||
fontSize: "0.875em" // please adjust the font size of the code in general.css
|
||||
});
|
||||
|
||||
editor.$blockScrolling = Infinity;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user